Skip to content

Commit

Permalink
Add support bluetooth mesh
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Oct 17, 2020
1 parent c9b4ebf commit 66a5401
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 12 deletions.
2 changes: 1 addition & 1 deletion custom_components/xiaomi_gateway3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def device_info(self):
'sw_version': self.device['zb_ver'],
'via_device': (DOMAIN, self.gw.device['mac'])
}
elif type_ == 'ble':
elif type_ == 'bluetooth':
return {
'connections': {(type_, self.device['mac'])},
'identifiers': {(DOMAIN, self.device['mac'])},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import datetime
from typing import Optional
from typing import Optional, Union

DEVICES = {
# BLE
152: ["Xiaomi", "Flower Care", "HHCCJCY01"],
426: ["Xiaomi", "TH Sensor", "LYWSDCGQ/01ZM"],
1034: ["Xiaomi", "Mosquito Repellent", "WX08ZM"],
Expand All @@ -13,6 +14,9 @@
1747: ["Xaiomi", "ZenMeasure Clock", "MHO-C303"],
1983: ["Yeelight", "Button S1", "YLAI003"],
2443: ["Xiaomi", "Door Sensor 2", "MCCGQ02HL"],
# Mesh
1771: ["Xiaomi", "Mesh Bulb", "MJDP09YL"],
2342: ["Yeelight", "Mesh Bulb M2", "YLDP25YL/YLDP26YL"],
}

BLE_FINGERPRINT_ACTION = [
Expand Down Expand Up @@ -236,7 +240,41 @@ def parse_xiaomi_ble(event: dict) -> Optional[dict]:
return None


def get_device(pdid: int) -> Optional[dict]:
MESH_PROPS = [None, 'light', 'brightness', 'color_temp']


def parse_xiaomi_mesh(data: list):
"""Can receive multiple properties from multiple devices."""
result = {}

for payload in data:
if payload['siid'] != 2 or payload.get('code', 0) != 0:
continue

did = payload['did']
key = MESH_PROPS[payload['piid']]
result.setdefault(did, {})[key] = payload['value']

return result


def pack_xiaomi_mesh(did: str, data: Union[dict, list]):
if isinstance(data, dict):
return [{
'did': did,
'siid': 2,
'piid': MESH_PROPS.index(k),
'value': v
} for k, v in data.items()]
else:
return [{
'did': did,
'siid': 2,
'piid': MESH_PROPS.index(k),
} for k in data]


def get_device(pdid: int, default_name: str) -> Optional[dict]:
if pdid in DEVICES:
desc = DEVICES[pdid]
return {
Expand All @@ -246,6 +284,6 @@ def get_device(pdid: int) -> Optional[dict]:
}
else:
return {
'device_name': "BLE",
'device_name': default_name,
'device_model': pdid
}
97 changes: 90 additions & 7 deletions custom_components/xiaomi_gateway3/gateway3.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from typing import Optional, Union

from paho.mqtt.client import Client, MQTTMessage
from . import ble, utils
from . import bluetooth, utils
from .miio_fix import Device
from .unqlite import Unqlite
from .unqlite import Unqlite, SQLite
from .utils import GLOBAL_PROP

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -565,9 +565,9 @@ def process_ble_event(self, raw: Union[bytes, str]):
if 'mac' in data['dev'] else \
'ble_' + did.replace('blt.3.', '')
self.devices[did] = device = {
'did': did, 'mac': mac, 'init': {}, 'type': 'ble'}
'did': did, 'mac': mac, 'init': {}, 'type': 'bluetooth'}
pdid = data['dev'].get('pdid')
desc = ble.get_device(pdid)
desc = bluetooth.get_device(pdid, 'BLE')
device.update(desc)

# update params from config
Expand All @@ -581,9 +581,9 @@ def process_ble_event(self, raw: Union[bytes, str]):
if isinstance(data['evt'], list):
# check if only one
assert len(data['evt']) == 1, data
payload = ble.parse_xiaomi_ble(data['evt'][0])
payload = bluetooth.parse_xiaomi_ble(data['evt'][0])
elif isinstance(data['evt'], dict):
payload = ble.parse_xiaomi_ble(data['evt'])
payload = bluetooth.parse_xiaomi_ble(data['evt'])
else:
payload = None

Expand All @@ -598,7 +598,7 @@ def process_ble_event(self, raw: Union[bytes, str]):

device['init'][k] = payload[k]

domain = ble.get_ble_domain(k)
domain = bluetooth.get_ble_domain(k)
if not domain:
continue

Expand All @@ -612,6 +612,32 @@ def process_ble_event(self, raw: Union[bytes, str]):
for handler in self.updates[did]:
handler(payload)

def process_mesh_data(self, raw: Union[bytes, list]):
data = json.loads(raw[10:])['params'] \
if isinstance(raw, bytes) else raw

_LOGGER.debug(f"{self.host} | Process Mesh {data}")

data = bluetooth.parse_xiaomi_mesh(data)
for did, payload in data.items():
device = self.devices.get(did)
if not device:
_LOGGER.warning("Unknown mesh device, reboot Hass may helps")
return

if 'init' not in device:
device['init'] = payload

# wait domain init
while 'light' not in self.setups:
time.sleep(1)

self.setups['light'](self, device, 'light')

if did in self.updates:
for handler in self.updates[did]:
handler(payload)

def send(self, device: dict, data: dict):
# convert hass prop to lumi prop
params = [{
Expand Down Expand Up @@ -652,6 +678,11 @@ def send_mqtt(self, cmd: str):
mac = self.device['mac'][2:].upper()
self.mqtt.publish(f"gw/{mac}/publishstate")

def send_mesh(self, device: dict, data: dict):
did = device['did']
payload = bluetooth.pack_xiaomi_mesh(did, data)
self.miio.send('set_properties', payload)

def get_device(self, mac: str) -> Optional[dict]:
for device in self.devices.values():
if device.get('mac') == mac:
Expand All @@ -660,6 +691,8 @@ def get_device(self, mac: str) -> Optional[dict]:


class GatewayBLE(Thread):
devices_loaded = False

def __init__(self, gw: Gateway3):
super().__init__(daemon=True)
self.gw = gw
Expand All @@ -673,6 +706,10 @@ def run(self):
telnet.write(b"admin\r\n")
telnet.read_until(b"\r\n# ") # skip greeting

if not self.devices_loaded:
self.get_devices(telnet)
self.devices_loaded = True

telnet.write(b"killall silabs_ncp_bt; "
b"silabs_ncp_bt /dev/ttyS1 1\r\n")
telnet.read_until(b"\r\n") # skip command
Expand All @@ -685,6 +722,8 @@ def run(self):

if b'_async.ble_event' in raw:
self.gw.process_ble_event(raw)
elif b'properties_changed' in raw:
self.gw.process_mesh_data(raw)

except (ConnectionRefusedError, ConnectionResetError, EOFError,
socket.timeout):
Expand All @@ -694,6 +733,50 @@ def run(self):

time.sleep(30)

def get_devices(self, telnet: Telnet):
payload = []

# read bluetooth db
telnet.write(b"cat /data/miio/mible_local.db | base64\r\n")
telnet.read_until(b'\r\n') # skip command
raw = telnet.read_until(b'# ')
raw = base64.b64decode(raw)

db = SQLite(raw)
tables = db.read_page(0)
device_page = next(table[3] - 1 for table in tables
if table[1] == 'mesh_device')
rows = db.read_page(device_page)
for row in rows:
did = row[0]
mac = row[1].replace(':', '')
device = {'did': did, 'mac': mac, 'type': 'bluetooth'}
# get device model from pdid
desc = bluetooth.get_device(row[2], 'Mesh')
device.update(desc)

# update params from config
default_config = self.gw.default_devices.get(did)
if default_config:
device.update(default_config)

self.gw.devices[did] = device

payload += [{'did': did, 'siid': 2, 'piid': p}
for p in range(1, 4)]

if not payload:
return

# 3 attempts to get actual data
for _ in range(3):
resp = self.gw.miio.send('get_properties', payload)
if all(p['code'] == 0 for p in resp):
break
time.sleep(1)

self.gw.process_mesh_data(resp)


def is_gw3(host: str, token: str) -> Optional[str]:
try:
Expand Down
71 changes: 70 additions & 1 deletion custom_components/xiaomi_gateway3/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from homeassistant.components.light import LightEntity, SUPPORT_BRIGHTNESS, \
ATTR_BRIGHTNESS, SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP
from homeassistant.util import color

from . import DOMAIN, Gateway3Device
from .gateway3 import Gateway3
Expand All @@ -11,7 +12,10 @@

async def async_setup_entry(hass, config_entry, async_add_entities):
def setup(gateway: Gateway3, device: dict, attr: str):
async_add_entities([Gateway3Light(gateway, device, attr)])
if device['type'] == 'zigbee':
async_add_entities([Gateway3Light(gateway, device, attr)])
else:
async_add_entities([Gateway3MeshLight(gateway, device, attr)])

gw: Gateway3 = hass.data[DOMAIN][config_entry.entry_id]
gw.add_setup('light', setup)
Expand Down Expand Up @@ -71,3 +75,68 @@ def turn_on(self, **kwargs):

def turn_off(self):
self.gw.send(self.device, {self._attr: 0})


class Gateway3MeshLight(Gateway3Device, LightEntity):
_brightness = None
_color_temp = None

@property
def is_on(self) -> bool:
return self._state

@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness

@property
def color_temp(self):
return self._color_temp

@property
def min_mireds(self):
return 153

@property
def max_mireds(self):
return 370

@property
def supported_features(self):
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP

def update(self, data: dict = None):
if self._attr in data:
self._state = data[self._attr]
if 'brightness' in data:
# 0...65535
self._brightness = data['brightness'] / 65535.0 * 255.0
self._state = True
if 'color_temp' in data:
# 2700..6500 => 370..153
self._color_temp = \
color.color_temperature_kelvin_to_mired(data['color_temp'])
self._state = True

self.schedule_update_ha_state()

def turn_on(self, **kwargs):
payload = {}

if ATTR_BRIGHTNESS in kwargs:
payload['brightness'] = \
int(kwargs[ATTR_BRIGHTNESS] / 255.0 * 65535)

if ATTR_COLOR_TEMP in kwargs:
payload['color_temp'] = color.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])

if not payload:
payload[self._attr] = True

self.gw.send_mesh(self.device, payload)
pass

def turn_off(self):
self.gw.send_mesh(self.device, {self._attr: False})
Loading

0 comments on commit 66a5401

Please sign in to comment.