diff --git a/README.md b/README.md index ecc1124..24f8b26 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,14 @@ mesh: : uuid: name: - type: light # thats it for now - [brightness_min: 0] # might be always 0. - [brightness_max: 100] # max BLE brightness value - [mireds_min: 0] # min BLE mireds value. - [mireds_max: 100] # max BLE mireds value - [ack: ] # use ack or unack mode - [relay: ] # whether this node should act as relay + type: light # thats it for now + [brightness_min: 0] # might be always 0. + [brightness_max: 100] # max BLE brightness value + [mireds_min: 0] # min BLE mireds value. + [mireds_max: 100] # max BLE mireds value + [hsl_ligthness_max: 65535] # the maximum brightness allowed for HSL (RGB) mode. Lower this if colors become washed out at high brightness. + [ack: ] # use ack or unack mode + [relay: ] # whether this node should act as relay ... ``` diff --git a/docker/Dockerfile b/docker/Dockerfile index 6ab49c4..239ee31 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,8 +28,8 @@ RUN sh ./install-bluez.sh # install bridge WORKDIR /opt/hass-ble-mesh -RUN git clone https://github.com/minims/homeassistant-bluetooth-mesh.git . -RUN git checkout master +RUN git clone https://github.com/louisjennings/homeassistant-bluetooth-mesh.git . +RUN git checkout light-hsl RUN pip3 install -r requirements.txt # mount config diff --git a/docker/config/config.yaml.sample b/docker/config/config.yaml.sample index 7c67b29..e1bb16a 100644 --- a/docker/config/config.yaml.sample +++ b/docker/config/config.yaml.sample @@ -8,20 +8,22 @@ mesh: : uuid: name: - type: light # Only type supported for now. - brightness_min: 0 # Might be always 0. - brightness_max: 65535 # Max BLE brightness value - mireds_min: 5 # Min BLE mireds value. - mireds_max: 1250 # Max BLE mireds value - ack: false # use ack or unack mode - relay: false # Whether this node should act as a Bluetooth Relay + type: light # Only type supported for now. + brightness_min: 0 # Might be always 0. + brightness_max: 65535 # Max BLE brightness value + mireds_min: 5 # Min BLE mireds value. + mireds_max: 1250 # Max BLE mireds value + hsl_ligthness_max: 65535 # the maximum brightness allowed for HSL (RGB) mode. Lower this if colors become washed out at high brightness. + ack: false # use ack or unack mode + relay: false # Whether this node should act as a Bluetooth Relay : uuid: name: - type: light # Only type supported for now. - brightness_min: 0 # Might be always 0. - brightness_max: 65535 # Max BLE brightness value - mireds_min: 5 # Min BLE mireds value. - mireds_max: 1250 # Max BLE mireds value - ack: true # use ack or unack mode - relay: false # Whether this node should act as a Bluetooth Relay + type: light # Only type supported for now. + brightness_min: 0 # Might be always 0. + brightness_max: 65535 # Max BLE brightness value + mireds_min: 5 # Min BLE mireds value. + mireds_max: 1250 # Max BLE mireds value + hsl_ligthness_max: 65535 # the maximum brightness allowed for HSL (RGB) mode. Lower this if colors become washed out at high brightness. + ack: false # use ack or unack mode + relay: false # Whether this node should act as a Bluetooth Relay diff --git a/gateway/gateway.py b/gateway/gateway.py index ba9c99d..581b478 100644 --- a/gateway/gateway.py +++ b/gateway/gateway.py @@ -47,6 +47,7 @@ class MainElement(Element): models.GenericOnOffClient, models.LightLightnessClient, models.LightCTLClient, + models.LightHSLClient, ] @@ -160,6 +161,8 @@ async def _import_keys(self): await client.bind(self.app_keys[0][0]) client = self.elements[0][models.LightCTLClient] await client.bind(self.app_keys[0][0]) + client = self.elements[0][models.LightHSLClient] + await client.bind(self.app_keys[0][0]) async def _try_bind_node(self, node): try: diff --git a/gateway/mesh/nodes/light.py b/gateway/mesh/nodes/light.py index 7a112f7..09c3fa5 100644 --- a/gateway/mesh/nodes/light.py +++ b/gateway/mesh/nodes/light.py @@ -12,6 +12,7 @@ BLE_MESH_MAX_TEMPERATURE = 20000 # Kelvin BLE_MESH_MIN_MIRED = 50 BLE_MESH_MAX_MIRED = 1250 +BLE_MESH_MAX_HSL_LIGHTNESS = 65535 class Light(Generic): @@ -32,6 +33,9 @@ class Light(Generic): OnOffProperty = "onoff" BrightnessProperty = "brightness" TemperatureProperty = "temperature" + HueProperty = "hue" + SaturationProperty = "saturation" + ModeProperty = "mode" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -78,6 +82,14 @@ async def mireds_to_kelvin(self, temperature, ack=False): else: await self.set_ctl(temperature=kelvin) + async def hsl(self, h, s, l, ack=False): + if self._is_model_bound(models.LightHSLServer): + if not ack: + await self.set_hsl_unack(h=h,s=s,l=l) + else: + await self.set_hsl(h=h,s=s,l=l) + + async def bind(self, app): await super().bind(app) @@ -97,6 +109,10 @@ async def bind(self, app): await self.get_ctl() await self.get_light_temperature_range() + if await self.bind_model(models.LightHSLServer): + self._features.add(Light.HueProperty) + self._features.add(Light.SaturationProperty) + async def set_onoff_unack(self, onoff, **kwargs): self.notify(Light.OnOffProperty, onoff) client = self._app.elements[0][models.GenericOnOffClient] @@ -136,6 +152,24 @@ async def set_lightness(self, lightness, **kwargs): client = self._app.elements[0][models.LightLightnessClient] await client.set_lightness([self.unicast], app_index=self._app.app_keys[0][0], lightness=lightness, **kwargs) + async def set_hsl(self, h, s, l, **kwargs): + self.notify(Light.ModeProperty, 'hsl') + self.notify(Light.HueProperty, h) + self.notify(Light.SaturationProperty, s) + self.notify(Light.BrightnessProperty, l) + + client = self._app.elements[0][models.LightHSLClient] + await client.set_hsl(self.unicast, app_index=self._app.app_keys[0][0], lightness=l, hue=h, saturation=s, transition_time=0, **kwargs) + + async def set_hsl_unack(self, h, s, l, **kwargs): + self.notify(Light.ModeProperty, 'hsl') + self.notify(Light.HueProperty, h) + self.notify(Light.SaturationProperty, s) + self.notify(Light.BrightnessProperty, l) + + client = self._app.elements[0][models.LightHSLClient] + await client.set_hsl_unack(self.unicast, app_index=self._app.app_keys[0][0], lightness=l, hue=h, saturation=s, transition_time=0, **kwargs) + async def get_lightness(self): client = self._app.elements[0][models.LightLightnessClient] state = await client.get_lightness([self.unicast], self._app.app_keys[0][0]) @@ -165,6 +199,8 @@ async def set_ctl_unack(self, temperature=None, brightness=None, **kwargs): if brightness and brightness > BLE_MESH_MAX_LIGHTNESS: brightness = BLE_MESH_MAX_LIGHTNESS + self.notify(Light.ModeProperty, 'ctl') + if temperature: self.notify(Light.TemperatureProperty, temperature) else: @@ -190,6 +226,8 @@ async def set_ctl(self, temperature=None, **kwargs): elif temperature and temperature > BLE_MESH_MAX_TEMPERATURE: temperature = BLE_MESH_MAX_TEMPERATURE + self.notify(Light.ModeProperty, 'ctl') + if temperature: self.notify(Light.TemperatureProperty, temperature) else: diff --git a/gateway/mqtt/bridges/light.py b/gateway/mqtt/bridges/light.py index 6f133f3..a834d50 100644 --- a/gateway/mqtt/bridges/light.py +++ b/gateway/mqtt/bridges/light.py @@ -4,6 +4,7 @@ BLE_MESH_MAX_TEMPERATURE, BLE_MESH_MAX_MIRED, BLE_MESH_MIN_MIRED, + BLE_MESH_MAX_HSL_LIGHTNESS, Light, ) from mqtt.bridge import HassMqttBridge @@ -62,6 +63,9 @@ async def config(self, node): message["min_mireds"] = node.config.optional("min_mireds", BLE_MESH_MIN_MIRED) message["max_mireds"] = node.config.optional("max_mireds", BLE_MESH_MAX_MIRED) + if node.supports(Light.HueProperty) and node.supports(Light.SaturationProperty): + color_modes.add("hs") + if color_modes: message["color_mode"] = True message["supported_color_modes"] = list(color_modes) @@ -82,9 +86,12 @@ async def _state(self, node, onoff): int(node.retained(Light.BrightnessProperty, BLE_MESH_MAX_LIGHTNESS)) / self.brightness_max * 100 ) - if onoff and node.supports(Light.TemperatureProperty): + if onoff and node.supports(Light.TemperatureProperty) and (node.retained(Light.ModeProperty, None) == 'ctl'): message["color_temp"] = node.retained(Light.TemperatureProperty, BLE_MESH_MAX_TEMPERATURE) + if onoff and node.supports(Light.HueProperty) and node.supports(Light.SaturationProperty) and (node.retained(Light.ModeProperty, None) == 'hsl'): + message["color"] = {'h': node.retained(Light.HueProperty, None)/0xFFFF * 360, 's': node.retained(Light.SaturationProperty, None) / 0xFFFF * 100} + await self._messenger.publish(self.component, node, "state", message, retain=True) async def _mqtt_set(self, node, payload): @@ -93,10 +100,33 @@ async def _mqtt_set(self, node, payload): if "brightness" in payload: brightness = int(payload["brightness"]) - desired_brightness = int(brightness * self.brightness_max / 100) - if desired_brightness > BLE_MESH_MAX_LIGHTNESS: - desired_brightness = BLE_MESH_MAX_LIGHTNESS - await node.set_brightness(brightness=desired_brightness, ack=node.config.optional("ack")) + + if node.retained(Light.ModeProperty, 'ctl') == 'ctl': + desired_brightness = int(brightness * self.brightness_max / 100) + if desired_brightness > BLE_MESH_MAX_LIGHTNESS: + desired_brightness = BLE_MESH_MAX_LIGHTNESS + await node.set_brightness(brightness=desired_brightness, ack=node.config.optional("ack")) + elif node.retained(Light.ModeProperty, None) == 'hsl': + + max_hsl_lightness = node.config.optional("hsl_ligthness_max", BLE_MESH_MAX_HSL_LIGHTNESS) + + h = node.retained(Light.HueProperty, 0) + s = node.retained(Light.SaturationProperty, 0xFFFF) + l = brightness * max_hsl_lightness // 100 + await node.hsl(h=h,s=s,l=l) + else: + raise NotImplemented(f"Cannot set brightness for mode: {node.retained(Light.ModeProperty, None)}") + + + if "color" in payload: + h = int(payload["color"]["h"]*0xffff/360) + s = int(payload["color"]["s"]*0xffff/100) + l = node.retained(Light.BrightnessProperty, 0xFFFF) + + max_hsl_lightness = node.config.optional("hsl_ligthness_max", BLE_MESH_MAX_HSL_LIGHTNESS) + l_scaled = l * max_hsl_lightness // 0xFFFF + + await node.hsl(h=h,s=s,l=l_scaled) if payload.get("state") == "ON": await node.turn_on(ack=node.config.optional("ack")) diff --git a/requirements.txt b/requirements.txt index d28ac94..ab3352d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ asyncio-mqtt==0.12.1 bitstring==3.1.9 black==22.10.0 -git+https://github.com/minims/python-bluetooth-mesh@43f3b368add0e0a40782478369a30454b95a9034 +git+https://github.com/louisjennings/python-bluetooth-mesh@dc4132274b0883c9296eab9dd0998a345280e051 cffi==1.15.1 construct==2.9.45 crc==0.3.0