dnschneid / pysmartblinds

Python interface to BLE-based MySmartBlinds
Apache License 2.0
13 stars 9 forks source link

Option to Control via MySmartBlinds Bridge? #4

Open creedda opened 5 years ago

creedda commented 5 years ago

Looks like it's possible?

https://github.com/ianlevesque/smartblinds-client

Might want to see if you can get them to fork in some of their code. Always good to have options? Or maybe try one and fall back to the other?

deftdawg commented 5 years ago

@creedda The bridge is a dead-end, the code you referenced impersonates the app using your user credentials to push commands to the bridge via the cloud. Even the author of that codebase has said he will not move forward merging that into HA if this approach is feasible.

This codebase directly accesses the blinds via BLE, it completely replaces the bridge hardware but requires no cloud connectivity and will work even if the internet goes down or mysmartblinds co goes out of business.

creedda commented 5 years ago

Ok thanks.

He did also make the suggestion that there might be a way to grab the keys off of the api vs trying to scan for them. I know that is what makes his approach more appealing to me, because I have not been able to get my keys using the search script or via an android phone.

My thought was that it would just be great to have the option. Setting the library to try and connect locally first and only if the blind cannot be reached or is out of range try the API option. I know that as someone who wants to use this for homebridge and HomeAssistant local options are better but reliability is a close second.

Just wanted to bring the option to your attention. I'll close this now.

Thanks again for this library it is awesome!

deftdawg commented 5 years ago

There is a way to scan for they key... Just brute force the first byte lol... Seriously, that's what the code in https://github.com/dnschneid/pysmartblinds/blob/master/examples/search.py does and it works :)

creedda commented 5 years ago

So I have been trying that, I even increased the number of keyscan attempts up to 256 and just targeting one specific blind and it is still failing. I have my pi3 right next to the blind (since distance seemed to be a factor) and everything :)

Not an expert with gatt or bluetooth packets so I've been going through the search.py and pysmartblinds.py code trying to understand how it works. Not sure if there are other levers I can pull outside of increasing the number of tries.

deftdawg commented 5 years ago

Basically it does a hcitool lescan for everything called SmartBlind_DFU, then it hits those addresses going with a byte from 0 to 255 until the blind acks the correct key.

If it takes more than a second for hcitool to see your blind, reception may be poor because of devices/wires in between. I would also check you can control via the app via Bluetooth (power off the bridge, app should be able to communicate directly via BT)... That'll make sure the blind isn't out of charge or something

dnschneid commented 5 years ago

@creedda could you please file a separate issue, since pysmartblinds's search doesn't seem to be working for you?

There is benefit to integrating the two for sure; people with large installs probably would be best served by one or more bridges to hit all their blinds. @ianlevesque, your thoughts?

creedda commented 5 years ago

Looking through what the api provides this is what I can see so far:

[Blind(name='Living Room',encoded_mac='gxxxxx7+',room_id='6xxxxxxx-0xxx-4xxx-axxx-2xxxxxxxxxx'), Blind(name='Big Window',encoded_mac='gxxxxx7+',room_id='6xxxxxxx-0xxx-4xxx-axxx-2xxxxxxxxxx'), Blind(name='Balcony Door',encoded_mac='rxxxxxxr',room_id='6xxxxxxx-0xxx-4xxx-axxx-2xxxxxxxxxx)] {'rxxxxxxr': BlindState(position=-1,rssi=0,battery_level=-1), 'gxxxxx7+': BlindState(position=-1,rssi=0,battery_level=-1)}

(Note: The position, rssi, and battery level are not 0 or -1 when the bridge is on and connected to the blinds)

At the very least its a way to get the battery level which I don't think is offered as part of your library today, but again I think it should be in addition to, but not in place of the ble connection. Maybe for each blind in the config, the library attempts to connect and send the command via ble if the the blinds are out of range or cannot connect due to interference, the library could send the same command via the api to see if the bridge could reach it. Creates a sort of backup if the ble connection is not good.

Your library also does stepping which the other library does not. (super useful) I just think a lot could be combined and learned from here.

Thanks for putting this together. Been trying to get my hands on an android phone for a while now to do the sniffing and here you are providing me a way to do it with my pi :)

Thanks!

ianlevesque commented 5 years ago

I think the cloud integration is useful mainly for grabbing the existing names, keys, and groups from the mobile app setup (though brute forcing the key is hilarious, I love IoT “security”). It seemed fairly robust, a vanilla (if proprietary) auth0 integration and then a pretty future proof GraphQL API for the config and control. From my personal usage the control was also very reliable - the primary reason I am unhappy with it is that cloud polling the current blind state is both slow (on the order of 15-25 seconds for a single poll) and not very reliable (blinds will report -1 presumably for bad reception decently often). I also am concerned that if polling was frequent they’d ban it, the usage pattern would be super different than their app or their Alexa integration, which only fall back to cloud as a last resort.

I am completely fine with combining the code or approaches, ideally for my setup I’d use a hybrid of this project and mine. If it helps my stab at hass integration is on a gist https://gist.github.com/ianlevesque/f97e3a9bfafc72cffcb4cec5059444cc but I am not planning to try to merge that due to the polling limitations.

dnschneid commented 5 years ago

At the very least its a way to get the battery level which I don't think is offered as part of your library today

The battery level is somewhere in those BLE packets; I just haven't looked very hard for it since I use the solar panel to keep them charged.

the library attempts to connect and send the command via ble if the the blinds are out of range or cannot connect due to interference, the library could send the same command via the api to see if the bridge could reach it. Creates a sort of backup if the ble connection is not good.

Yeah, and then default to the API approach to avoid lagginess. This seems reasonable.

I think the cloud integration is useful mainly for grabbing the existing names, keys, and groups from the mobile app setup (though brute forcing the key is hilarious, I love IoT “security”).

Agreed, although unless I'm reading it wrong, I don't think your current code is requesting the key in its current form. Have you looked into decoding the MAC and key from the API output?

the primary reason I am unhappy with it is that cloud polling the current blind state is both slow (on the order of 15-25 seconds for a single poll) and not very reliable (blinds will report -1 presumably for bad reception decently often). I also am concerned that if polling was frequent they’d ban it, the usage pattern would be super different than their app or their Alexa integration, which only fall back to cloud as a last resort.

Yeah, I think I'd stick with the current method this library uses -- write-only and track the state locally (with the caveats as described in the README). I took this approach because it didn't seem like the phone app was able to read the current state either; I'm surprised the bridge does it -- unless it, too, is keeping local state.

I am completely fine with combining the code or approaches, ideally for my setup I’d use a hybrid of this project and mine. If it helps my stab at hass integration is on a gist https://gist.github.com/ianlevesque/f97e3a9bfafc72cffcb4cec5059444cc but I am not planning to try to merge that due to the polling limitations.

Thanks! This one's here.

ianlevesque commented 5 years ago

The battery level is somewhere in those BLE packets; I just haven't looked very hard for it since I use the solar panel to keep them charged.

I don't have the panel so this is useful. I suspect comparing the exact bridge values from the API to the BLE packets would make finding it easier.

Agreed, although unless I'm reading it wrong, I don't think your current code is requesting the key in its current form. Have you looked into decoding the MAC and key from the API output?

Yes this is trivial. GraphQL just has you whitelist the fields you want so I left it out. The full response to the GetUserInfo query if you include all fields is:

"__typename": "Blind",
"userId": XXXX,
"id": XXXXX,
"roomId": XXXX,
"name": "Dining Room",
"encodedPasskey": "hj7DfmAf",
"encodedMacAddress": "h0pdvVrv",
"batteryPercent": -1,
"hasSyncedPositions": true,
"hasSyncedSchedule": true,
"lastUpdateName": 556939648,
"lastUpdateStatus": 556920896,
"passkeyNeedsChanging": false,
"reverseRotation": false,
"deleted": false,
"clientUpdatedAt": 1537142334.7803612

I left the MAC & passkey in the clear so you can see if they make sense to you. If someone moves my blinds while they're in the neighborhood I'll wave.

Yeah, I think I'd stick with the current method this library uses -- write-only and track the state locally (with the caveats as described in the README). I took this approach because it didn't seem like the phone app was able to read the current state either; I'm surprised the bridge does it -- unless it, too, is keeping local state.

That's a shame, I was hoping local BLE would be pollable (and more frequently).

ianlevesque commented 5 years ago

For completeness here's a query of blindsState (which is what exposes a valid battery reading):

{
    "data": {
        "blindsState": [{
            "__typename": "BlindState",
            "encodedMacAddress": "rFT2VMTD",
            "position": 199,
            "rssi": -82,
            "batteryLevel": 51
        }, {
            "__typename": "BlindState",
            "encodedMacAddress": "h0pdvVrv",
            "position": 199,
            "rssi": -88,
            "batteryLevel": 52
        }, {
            "__typename": "BlindState",
            "encodedMacAddress": "Megw7y3x",
            "position": 199,
            "rssi": -83,
            "batteryLevel": 79
        }]
    }
}
dnschneid commented 5 years ago

Thanks! I'm guessing the encoded fields are just base64. I'll confirm against my own account.

dnschneid commented 5 years ago

That's a shame, I was hoping local BLE would be pollable (and more frequently).

Well, maybe the position info is in the same place the battery info is. I'll start a new issue for figuring that stuff out.

dnschneid commented 5 years ago

I'm guessing the encoded fields are just base64. I'll confirm against my own account.

Yup, it's simply base-64 encoded. Filed ianlevesque/smartblinds-client#3

dnschneid commented 5 years ago

I've dumped a bunch of completely untested code into the sbc branch.

deftdawg commented 5 years ago

I've dumped a bunch of completely untested code into the sbc branch.

Code doesn't load on >= 0.77 without my hatch patch in #7... after removing SERVICE_TO_METHOD, it dies with the following (tested on 0.85.1):

2019-01-22 07:02:39 ERROR (MainThread) [homeassistant.components.cover] Error while setting up platform mysmartblinds
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/homeassistant/helpers/entity_platform.py", line 128, in _async_setup_platform
    SLOW_SETUP_MAX_WAIT, loop=hass.loop)
  File "/usr/local/lib/python3.6/asyncio/tasks.py", line 358, in wait_for
    return fut.result()
  File "/usr/local/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/cover/mysmartblinds.py", line 122, in setup_platform
    ':'.join(('%02X' for x in blind.mac_address)),
AttributeError: 'Blind' object has no attribute 'mac_address'
deftdawg commented 5 years ago

I replaced mac_address with encoded_mac and passkey with room_id since those don't seem to exist in the sbc definition of blind ... but then it failed with this, looks like it's trying to do bluetooth even though I've only provided the username / password to login.

2019-01-22 07:26:02 ERROR (MainThread) [homeassistant.components.cover] Error while setting up platform mysmartblinds
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/pygatt/backends/gatttool/gatttool.py", line 382, in connect
    self.sendline(cmd)
  File "/usr/local/lib/python3.6/contextlib.py", line 88, in __exit__
    next(self.gen)
  File "/usr/local/lib/python3.6/site-packages/pygatt/backends/gatttool/gatttool.py", line 180, in event
    self.wait(event, timeout)
  File "/usr/local/lib/python3.6/site-packages/pygatt/backends/gatttool/gatttool.py", line 154, in wait
    raise NotificationTimeout()
pygatt.exceptions.NotificationTimeout: None
2019-01-22T07:26:02.120632532Z 
During handling of the above exception, another exception occurred:
2019-01-22T07:26:02.120638506Z 
Traceback (most recent call last):
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 127, in _connect
    address_type=pygatt.backends.BLEAddressType.random)
  File "/usr/local/lib/python3.6/site-packages/pygatt/backends/gatttool/gatttool.py", line 388, in connect
    raise NotConnectedError(message)
pygatt.exceptions.NotConnectedError: Timed out connecting to %02X:%02X:%02X:%0 after 5.0 seconds.
2019-01-22T07:26:02.120704338Z 
During handling of the above exception, another exception occurred:
2019-01-22T07:26:02.120726411Z 
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/homeassistant/helpers/entity_platform.py", line 128, in _async_setup_platform
    SLOW_SETUP_MAX_WAIT, loop=hass.loop)
  File "/usr/local/lib/python3.6/asyncio/tasks.py", line 358, in wait_for
    return fut.result()
  File "/usr/local/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/cover/mysmartblinds.py", line 123, in setup_platform
    tuple(x for x in blind.room_id)
  File "/config/custom_components/cover/mysmartblinds.py", line 146, in __init__
    self._blind.pos(200)
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 243, in pos
    return self._update()
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 193, in _update
    if not self._set(pos):
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 152, in _set
    if not self._connect():
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 133, in _connect
    self._sbcBlind = self._getSbcBlind()
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 96, in _getSbcBlind
    macBytes = bytes(int(x, 16) for x in self._mac.split(':'))
  File "/config/deps/lib/python3.6/site-packages/pysmartblinds.py", line 96, in <genexpr>
    macBytes = bytes(int(x, 16) for x in self._mac.split(':'))
ValueError: invalid literal for int() with base 16: '%02X'
deftdawg commented 5 years ago

I bumped smarblinds-client to v0.3 and retrying replacing passkey with encoded_passkey this time... made no difference still chokes loading with the same error.