Code Monkey home page Code Monkey logo

melnor-bluetooth's Introduction

Melnor Bluetooth

PyPI Codecov branch GitHub Workflow Status (branch) PyPI - Downloads

Melnor Bluetooth is a reverse engineered implementation of the Bluetooth protocol for all "smart" bluetooth-enabled watering valves under the Melnor, EcoAquastar, Eden, and other brands.

The library should run on MacOS, Linux, or Windows. It's primarily developed on MacOS and other platforms likely have bugs. PRs and tests are welcome to improve quality across all platforms.

Getting Started

CLI

A simple CLI has been provided for basic debugging purposes. It's not intended for any real use and isn't suitable for running a valve in the real world.

This project uses poetry for dependency management and building. Running this project locally is as simple as the following steps:

  1. Clone the repository
  2. poetry install
  3. poetry run cli.py

The python API has been designed to be as easy to use as possible. A few examples are provided below:

Read battery state

import asyncio

from bleak import BleakScanner  # type: ignore - bleak has bad export types

from melnor_bluetooth.device import Device

ADDRESS = "00:00:00:00:00"  # fill with your device mac address


async def main():

    ble_device = await BleakScanner.find_device_by_address(ADDRESS)
    if ble_device is not None:
        device = Device(ble_device)
        await device.connect()
        await device.fetch_state()

        print(device.battery_level)

        await device.disconnect()


asyncio.run(main())

Turn on a zone

import asyncio

from bleak import BleakScanner  # type: ignore - bleak has bad export types

from melnor_bluetooth.device import Device

address = "00:00:00:00:00"  # fill with your device mac address


async def main():
    ble_device = await BleakScanner.find_device_by_address(ADDRESS)
    if ble_device is not None:
        device = Device(ble_device)
        await device.connect()
        await device.fetch_state()

        device.zone1.is_watering = True

        await device.push_state()
        await device.disconnect()


asyncio.run(main())

melnor-bluetooth's People

Contributors

bdraco avatar vanstinator avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

melnor-bluetooth's Issues

Discovering valves with `bluez` rarely works

These watering valves have 2 distinct manufacturer data sections in their advertising packets. The one we want (and the one that nrf scanner, CoreBluetooth on Apple, etc have no problem parsing correctly) comes in via the ADV_IND message. bluez ignores this message and only returns the manufacturer data in SCAN_RESP. The behavior isn't deterministic and every so often the advertising packet we care about is returned by bluez so the current filtering can capture it. But it can take minutes sometimes and downstream user experiences suffer.

Here's a snippet from btmon while bluez was scanning

> HCI Event: LE Meta Event (0x3e) plen 43                                                                                                                                              #20 [hci0] 3.860728
      LE Advertising Report (0x02)
        Num reports: 1
        Event type: Connectable undirected - ADV_IND (0x00)
        Address type: Public (0x00)
        Address: 58:93:D8:AC:9F:2A (Texas Instruments)
        Data length: 31
        Flags: 0x06
          LE General Discoverable Mode
          BR/EDR Not Supported
        Company: Texas Instruments Inc. (13)
          Data: 59080285000000000000f00000f00000f00000f029f8095c
        RSSI: -59 dBm (0xc5)
> HCI Event: LE Meta Event (0x3e) plen 43                                                                                                                                              #21 [hci0] 3.863730
      LE Advertising Report (0x02)
        Num reports: 1
        Event type: Scan response - SCAN_RSP (0x04)
        Address type: Public (0x00)
        Address: 58:93:D8:AC:9F:2A (Texas Instruments)
        Data length: 31
        Slave Conn. Interval: 0x0050 - 0x0320
        Company: Texas Instruments Inc. (13)
          Data: 0800000000000000000000000000003638d8ac9f2a
        RSSI: -59 dBm (0xc5)
@ MGMT Event: Device Found (0x0012) plen 76                                                                                                                                       {0x0002} [hci0] 3.863779
        LE Address: 58:93:D8:AC:9F:2A (Texas Instruments)
        RSSI: -59 dBm (0xc5)
        Flags: 0x00000000
        Data length: 62
        Flags: 0x06
          LE General Discoverable Mode
          BR/EDR Not Supported
        Company: Texas Instruments Inc. (13)
          Data: 59080285000000000000f00000f00000f00000f029f8095c
        Slave Conn. Interval: 0x0050 - 0x0320
        Company: Texas Instruments Inc. (13)
          Data: 0800000000000000000000000000003638d8ac9f2a
@ MGMT Event: Device Found (0x0012) plen 76                                                                                                                                       {0x0001} [hci0] 3.863779
        LE Address: 58:93:D8:AC:9F:2A (Texas Instruments)
        RSSI: -59 dBm (0xc5)
        Flags: 0x00000000
        Data length: 62
        Flags: 0x06
          LE General Discoverable Mode
          BR/EDR Not Supported
        Company: Texas Instruments Inc. (13)
          Data: 59080285000000000000f00000f00000f00000f029f8095c
        Slave Conn. Interval: 0x0050 - 0x0320
        Company: Texas Instruments Inc. (13)
          Data: 0800000000000000000000000000003638d8ac9f2a

poetry run cli.py error on Linux

-Latest version of Poetry installed with "curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - " on Ubuntu 22.04

  • Cloned latest repo and ran "poetry install" - virtual environment is created and dependencies installed with 1 warning "The lock file is not up to date with the latest changes in pyproject.toml. You may be getting outdated dependencies. Run update to update them". This seems to be a Poetry issue and was fixed with command "poetry lock --no-update" to resolve dependencies.

  • Ran "poetry run cli.py" with the following:

` FileNotFoundError

[Errno 2] No such file or directory: b'/home/me/.local/bin/cli.py'

at ~/Documents/anaconda3/lib/python3.9/os.py:607 in _execvpe
603│ path_list = map(fsencode, path_list)
604│ for dir in path_list:
605│ fullname = path.join(dir, file)
606│ try:
→ 607│ exec_func(fullname, *argrest)
608│ except (FileNotFoundError, NotADirectoryError) as e:
609│ last_exc = e
610│ except OSError as e:
611│ last_exc = e
`
Tried copying cli.py to the /home/me/.local/bin/ directory and got "PermissionError" and same error code block. Changed permissions on cli.py and got "OSError" and same code block.

Not sure if that is a poetry issue.

BleakError object is not subscriptable

'poetry install python cli.py' runs and finds Melnor devices. Connection fails with:

~/Downloads/melnor-bluetooth-main$ poetry run python cli.py 58:93:D8:37:29:59 Found device: Device( battery=0 valves=( Valve(id=0|is_watering=False|manual_minutes=20|seconds_left=0) Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0) Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0) Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0) ) ) Connecting to: 58:93:D8:37:29:59 Failed to connect to: 58:93:D8:37:29:59 Traceback (most recent call last): File "/home/ian/Downloads/melnor-bluetooth-main/cli.py", line 88, in <module> asyncio.run(main()) File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/usr/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete return future.result() File "/home/ian/Downloads/melnor-bluetooth-main/cli.py", line 42, in main await device.fetch_state() File "/home/ian/Downloads/melnor-bluetooth-main/melnor_bluetooth/device.py", line 215, in fetch_state self._battery = parse_battery_value(some_bytes) File "/home/ian/Downloads/melnor-bluetooth-main/melnor_bluetooth/parser/battery.py", line 4, in parse_battery_value if (bytes[0] & 255 == 238) and (bytes[1] & 255 == 238): TypeError: 'BleakError' object is not subscriptable

Feature request : Retry on "Failed to Connect"

Due to the lag on device connect/disconnect it would be great to have a retry on a failure to connect to ensure the next command goes through.

Failed to connect to: 58:93:D8:3E:D2:DB Disconnected from: 58:93:D8:3E:D2:DB Traceback (most recent call last): File "/home/ian/Downloads/melnor-bluetooth/turnoff1.py", line 16, in <module> asyncio.run(main()) File "/home/ian/anaconda3/lib/python3.9/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/home/ian/anaconda3/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete return future.result() File "/home/ian/Downloads/melnor-bluetooth/turnoff1.py", line 13, in main await device.push_state(); File "/home/ian/Downloads/melnor-bluetooth/melnor_bluetooth/device.py", line 240, in push_state on_off.handle, AttributeError: 'NoneType' object has no attribute 'handle'

CLI example positional arguments error

Error when running a test script (turnon1.py) to turn on a valve:

import asyncio
from melnor_bluetooth.device import Device

address = 'xxxxx' # fill with your device mac address

async def main():
device = Device(address, 4)
await device.connect()
device.zone1.is_watering = True;
await device.push_state();
await device.disconnect();

asyncio.run(main())

gives:

Traceback (most recent call last):
File "/home/ian/Downloads/melnor-bluetooth/turnon1.py", line 16, in
asyncio.run(main())
File "/home/ian/anaconda3/lib/python3.9/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/home/ian/anaconda3/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
return future.result()
File "/home/ian/Downloads/melnor-bluetooth/turnon1.py", line 8, in main
device = Device(address, 4)
TypeError: init() takes 2 positional arguments but 3 were given

CLI examples missing "def"

I was unable to get the CLI examples to work without definition ie changing from "async main() " to "async def main()".

Manual on does not respect manual watering time set

Hi,

I am trying to turn on a zone manually to run it for the set manual watering time. I am able to push the manual watering time (10 minutes), but when I turn the zone on the second left value is 5+ hours.

This is the code I am using

#!/usr/bin/python

import asyncio
from time import sleep
from bleak import BleakScanner  # type: ignore - bleak has bad export types

from melnor_bluetooth.device import Device

ADDRESS = "XX:XX:XX:XX:XX:XX"  # fill with your device mac address

async def main():
    ble_device = await BleakScanner.find_device_by_address(ADDRESS)
    if ble_device is not None:
        device = Device(ble_device)
        await device.connect()
        await device.fetch_state()

        print(device)

        await device.zone1.set_manual_watering_minutes(10)
        await device.zone1.set_is_watering(True)
        await device.push_state()

        sleep(10)

        await device.fetch_state()

        print(device)

        await device.disconnect()

asyncio.run(main())

And here is the result:

Device(
    battery=96
    valves=(
      Valve(id=0|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0)
    )
)
Device(
    battery=89
    valves=(
      Valve(id=0|is_watering=True|manual_minutes=10|seconds_left=1684886359)
      Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0)
    )
)
Disconnected from XX:XX:XX:XX:XX:XX

Not sure what I am doing wrong.

I wrote another script to test how the seconds_left field value changes if I start the timer from the melnor app. It seems that the value is "random" and does not change during the time of the timer.

Example this is what I get when the timer is set to 1 minute (reading is done every 1 second with await device.fetch_state()):

Device(
    battery=80
    valves=(
      Valve(id=0|is_watering=True|manual_minutes=1|seconds_left=1684868920)
      Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0)
    )
)
Device(
    battery=80
    valves=(
      Valve(id=0|is_watering=True|manual_minutes=1|seconds_left=1684868920)
      Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0)
    )
)
Device(
    battery=80
    valves=(
      Valve(id=0|is_watering=True|manual_minutes=1|seconds_left=1684868920)
      Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0)
    )
)
## Timer expired here ...
Device(
    battery=89
    valves=(
      Valve(id=0|is_watering=False|manual_minutes=1|seconds_left=0)
      Valve(id=1|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=2|is_watering=False|manual_minutes=20|seconds_left=0)
      Valve(id=3|is_watering=False|manual_minutes=20|seconds_left=0)
    )
)

Only one device available

When trying to run to a second device using a different MAC address the turn on script fails. The scanner seems to see all the available devices, but only connects to one.

Connecting to: 58:93:D8:3E:D2:DB Disconnected from: 58:93:D8:3E:D2:DB Failed to connect to: 58:93:D8:3E:D2:DB Traceback (most recent call last): File "/home/ian/Downloads/melnor-bluetooth/turnon1.py", line 17, in <module> asyncio.run(main()) File "/home/ian/anaconda3/lib/python3.9/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/home/ian/anaconda3/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete return future.result() File "/home/ian/Downloads/melnor-bluetooth/turnon1.py", line 12, in main device.zone2.is_watering = True; File "/home/ian/Downloads/melnor-bluetooth/melnor_bluetooth/device.py", line 304, in zone2 if self._valve_count > 1: AttributeError: 'Device' object has no attribute '_valve_count'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.