From cbf01bea7bab4e06f887fcbe6bc45ed4692231a8 Mon Sep 17 00:00:00 2001 From: P-Storm Date: Wed, 21 Feb 2024 15:00:08 +0000 Subject: [PATCH 1/2] Added brightness and start pages --- custom_components/button_plus/__init__.py | 2 +- .../button_plus/button_plus_api/event_type.py | 4 + .../button_plus/button_plus_api/model.py | 7 +- .../button_plus/buttonplushub.py | 4 + custom_components/button_plus/config_flow.py | 40 ++++- custom_components/button_plus/coordinator.py | 50 ++++++- custom_components/button_plus/number.py | 141 ++++++++++++++++++ .../button_plus/resource/config.json.notes | 28 +++- 8 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 custom_components/button_plus/number.py diff --git a/custom_components/button_plus/__init__.py b/custom_components/button_plus/__init__.py index 1a36ca6..790f405 100644 --- a/custom_components/button_plus/__init__.py +++ b/custom_components/button_plus/__init__.py @@ -15,7 +15,7 @@ # List of platforms to support. There should be a matching .py file for each, # eg and -PLATFORMS: list[str] = ["button", "text"] +PLATFORMS: list[str] = ["button", "text", "number"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/button_plus/button_plus_api/event_type.py b/custom_components/button_plus/button_plus_api/event_type.py index 989e287..ee16941 100644 --- a/custom_components/button_plus/button_plus_api/event_type.py +++ b/custom_components/button_plus/button_plus_api/event_type.py @@ -4,6 +4,7 @@ class EventType(int, Enum): CLICK = 0 LONG_PRESS = 1 + PAGE_STATUS = 6 BLUE_LED = 8 RED_LED = 9 GREEN_LED = 10 @@ -14,3 +15,6 @@ class EventType(int, Enum): VALUE = 15 UNIT = 17 SENSOR_VALUE = 18 + SET_PAGE = 20 + BRIGHTNESS_LARGE_DISPLAY = 24 + BRIGHTNESS_MINI_DISPLAY = 25 diff --git a/custom_components/button_plus/button_plus_api/model.py b/custom_components/button_plus/button_plus_api/model.py index e7c2916..53bcd0e 100644 --- a/custom_components/button_plus/button_plus_api/model.py +++ b/custom_components/button_plus/button_plus_api/model.py @@ -54,7 +54,8 @@ def from_dict(data: Dict[str, Any]) -> 'Info': class Core: def __init__(self, name: str, location: str, auto_backup: bool, brightness_large_display: int, - brightness_mini_display: int, led_color_front: int, led_color_wall: int, color: int): + brightness_mini_display: int, led_color_front: int, led_color_wall: int, color: int, + topics: List[Dict[str, Any]]): self.name = name self.location = location self.auto_backup = auto_backup @@ -63,6 +64,7 @@ def __init__(self, name: str, location: str, auto_backup: bool, brightness_large self.led_color_front = led_color_front self.led_color_wall = led_color_wall self.color = color + self.topics = topics @staticmethod def from_dict(data: Dict[str, Any]) -> 'Core': @@ -74,7 +76,8 @@ def from_dict(data: Dict[str, Any]) -> 'Core': brightness_mini_display=data['brightnessminidisplay'], led_color_front=data['ledcolorfront'], led_color_wall=data['ledcolorwall'], - color=data['color'] + color=data['color'], + topics=data['topics'] ) diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 63f738e..fba222b 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -29,6 +29,7 @@ def __init__(self, hass: HomeAssistant, config: DeviceConfiguration, entry: Conf self.button_entities = {} self.label_entities = {} self.top_label_entities = {} + self.brightness_entities = {} device_registry = dr.async_get(hass) @@ -61,3 +62,6 @@ def add_label(self, button_id, entity): def add_top_label(self, button_id, entity): self.top_label_entities[str(button_id)] = entity + + def add_brightness(self, identifier, entity): + self.brightness_entities[identifier] = entity diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index 3ad66a4..7b03646 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -16,6 +16,7 @@ from .button_plus_api.model import DeviceConfiguration, MqttBroker from .button_plus_api.event_type import EventType from homeassistant.helpers.network import get_url +from packaging import version from .const import DOMAIN # pylint:disable=unused-import @@ -99,6 +100,7 @@ async def async_step_manual(self, user_input=None): device_config: DeviceConfiguration = DeviceConfiguration.from_json(json_config) self.add_broker_to_config(device_config) + self.add_topics_to_core(device_config) self.add_topics_to_buttons(device_config) await api_client.push_config(device_config) @@ -240,7 +242,43 @@ def add_broker_to_config(self, device_config: DeviceConfiguration) -> DeviceConf device_config.mqtt_brokers.append(broker) return device_config - def add_topics_to_buttons(self, device_config) -> DeviceConfiguration: + def add_topics_to_core(self, device_config : DeviceConfiguration) -> DeviceConfiguration: + device_id = device_config.info.device_id + min_version = "1.11" + + if version.parse(device_config.info.firmware) < version.parse(min_version): + _LOGGER.debug(f"Current version {device_config.info.firmware} doesn't support the brightness, it must be at least firmware version {min_version}") + return + + device_config.core.topics.append({ + "brokerid": "ha-button-plus", + "topic": f"buttonplus/{device_id}/brightness/large", + "payload": "", + "eventtype": EventType.BRIGHTNESS_LARGE_DISPLAY + }) + + device_config.core.topics.append({ + "brokerid": "ha-button-plus", + "topic": f"buttonplus/{device_id}/brightness/mini", + "payload": "", + "eventtype": EventType.BRIGHTNESS_MINI_DISPLAY + }) + + device_config.core.topics.append({ + "brokerid": "ha-button-plus", + "topic": f"buttonplus/{device_id}/page/status", + "payload": "", + "eventtype": EventType.PAGE_STATUS + }) + + device_config.core.topics.append({ + "brokerid": "ha-button-plus", + "topic": f"buttonplus/{device_id}/page/set", + "payload": "", + "eventtype": EventType.SET_PAGE + }) + + def add_topics_to_buttons(self, device_config : DeviceConfiguration) -> DeviceConfiguration: device_id = device_config.info.device_id active_connectors = [ diff --git a/custom_components/button_plus/coordinator.py b/custom_components/button_plus/coordinator.py index d7d69c9..10d95b7 100644 --- a/custom_components/button_plus/coordinator.py +++ b/custom_components/button_plus/coordinator.py @@ -1,6 +1,7 @@ from homeassistant.components.button import ButtonEntity from . import DOMAIN, ButtonPlusHub -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.components.number import NumberEntity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.components.mqtt import client as mqtt, ReceiveMessage @@ -26,6 +27,8 @@ def __init__(self, hass: HomeAssistant, hub: ButtonPlusHub): self._hass = hass self._mqtt_subscribed_buttons = False self._mqtt_topic_buttons = f"buttonplus/{hub.hub_id}/button/+/click" + self._mqtt_topic_brightness = f"buttonplus/{hub.hub_id}/brightness/+" + self._mqtt_topic_page = f"buttonplus/{hub.hub_id}/page/+" async def _async_update_data(self): """Create MQTT subscriptions for buttonplus """ @@ -39,6 +42,51 @@ async def _async_update_data(self): ) _LOGGER.debug(f"MQTT subscribed to {self._mqtt_topic_buttons}") + + if not self._mqtt_subscribed_buttons: + self.unsubscribe_mqtt = await mqtt.async_subscribe( + self._hass, + self._mqtt_topic_brightness, + self.mqtt_brightness_callback, + 0 + ) + _LOGGER.debug(f"MQTT subscribed to {self._mqtt_topic_brightness}") + + + if not self._mqtt_subscribed_buttons: + self.unsubscribe_mqtt = await mqtt.async_subscribe( + self._hass, + self._mqtt_topic_page, + self.mqtt_page_callback, + 0 + ) + _LOGGER.debug(f"MQTT subscribed to {self._mqtt_topic_page}") + + @callback + async def mqtt_page_callback(self, message: ReceiveMessage): + # Handle the message here + _LOGGER.debug(f"Received message on topic {message.topic}: {message.payload}") + match = re.search(r'/page/(\w+)', message.topic) + # is 'status' or 'set' + page_type = match.group(1) + + # TODO: implement page control + + + @callback + async def mqtt_brightness_callback(self, message: ReceiveMessage): + # Handle the message here + _LOGGER.debug(f"Received message on topic {message.topic}: {message.payload}") + match = re.search(r'/brightness/(\w+)', message.topic) + brightness_type = match.group(1) + + entity: NumberEntity = self.hub.brightness_entities[brightness_type] + + value = float(message.payload) + entity._attr_native_value = value + entity.schedule_update_ha_state() + + @callback async def mqtt_button_callback(self, message: ReceiveMessage): # Handle the message here _LOGGER.debug(f"Received message on topic {message.topic}: {message.payload}") diff --git a/custom_components/button_plus/number.py b/custom_components/button_plus/number.py new file mode 100644 index 0000000..1e42c8f --- /dev/null +++ b/custom_components/button_plus/number.py @@ -0,0 +1,141 @@ +""" Platform for light integration. """ +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.components.mqtt import client as mqtt +from .button_plus_api.event_type import EventType +from . import ButtonPlusHub +from packaging import version + +from .const import DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + +brightness = [] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add switches for passed config_entry in HA.""" + + hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] + + if version.parse(hub.config.info.firmware) < version.parse('1.11'): + _LOGGER.info(f"Current version {hub.config.info.firmware} doesn't support the brightness, it must be at least firmware version {min_version}") + return + + _LOGGER.debug(f"Creating number with parameters: {hub.hub_id}") + # _LOGGER.debug(f"Creating Lights with parameters: {button.button_id} {button.label} {hub.hub_id}") + mini = ButtonPlusMiniBrightness(hub) + brightness.append(mini) + hub.add_brightness("mini", mini) + + large = ButtonPlusLargeBrightness(hub) + brightness.append(large) + hub.add_brightness("large", mini) + + async_add_entities(brightness) + + + +class ButtonPlusBrightness(NumberEntity): + def __init__(self, hub: ButtonPlusHub, brightness_type: str, event_type: EventType): + self._hub = hub + self._hub_id = hub.hub_id + self._brightness_type = brightness_type + self._attr_unique_id = f'brightness-{brightness_type}-{self._hub_id}' + self.entity_id = f"brightness.{brightness_type}_{self._hub_id}" + self._attr_name = f'brightness-{brightness_type}' + self.event_type = event_type + self._topics = hub.config.core.topics + self._attr_icon = "mdi:television-ambient-light" + + @property + def native_max_value(self) -> float: + return 100 + + @property + def native_min_value(self) -> float: + return 0 + + @property + def native_unit_of_measurement(self) -> str: + return "%" + + def update(self) -> None: + """Fetch new state data for this light.""" + # get latest stats from mqtt for this light + # then update self._state + _LOGGER.debug(f"Update {self.name} (attr_name: {self._attr_name}) (unique: {self._attr_unique_id})") + + @property + def device_info(self)-> DeviceInfo: + """Return information to link this entity with the correct device.""" + via_device = (DOMAIN, self._hub.hub_id) + + match self.event_type: + case EventType.BRIGHTNESS_LARGE_DISPLAY: + name = f"Display Module" + connections: set[tuple[str, str]] = {("display_module", 1)} + model = "Display Module" + identifiers = {(DOMAIN, f'{self._hub.hub_id}_large_display_module')} + device_info = DeviceInfo(name=name, connections=connections, model=model, identifiers=identifiers, manufacturer=MANUFACTURER, via_device=via_device) + case EventType.BRIGHTNESS_MINI_DISPLAY: + name = f"BAR Module 1" + #name = f"Brightness Mini Display Module" + connections: set[tuple[str, str]] = { + #(DOMAIN, f'{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.connector_id}') + ("bar_module", 1), + ("bar_module", 2), + ("bar_module", 3) + } + model = "BAR Module" + identifiers = {(DOMAIN, f'{self._hub.hub_id}_display_module_mini_brightness')} + device_info = DeviceInfo(name=name, connections=connections, model=model, identifiers=identifiers, manufacturer=MANUFACTURER, via_device=via_device) + + return device_info + + + async def async_set_value(self, value: float) -> None: + """Set the text value and publish to mqtt.""" + # TODO: Add support for mini + label_topic = f"buttonplus/{self._hub_id}/brightness/{self._brightness_type}" + _LOGGER.debug(f"ButtonPlus brightness update for {self.entity_id}") + _LOGGER.debug(f"ButtonPlus brightness update to {label_topic} with new value: {value}") + await mqtt.async_publish(hass=self.hass, topic=label_topic, payload=value, qos=0, retain=True) + self._attr_native_value = value + self.async_write_ha_state() + + +class ButtonPlusMiniBrightness(ButtonPlusBrightness): + """ Numeric entity representation """ + + def __init__(self, hub: ButtonPlusHub): + super().__init__(hub, "mini", EventType.BRIGHTNESS_MINI_DISPLAY) + + @property + def name(self) -> str: + """Return the display name of this light.""" + return f'Brightness mini display' + + +class ButtonPlusLargeBrightness(ButtonPlusBrightness): + """ Numeric entity representation """ + + def __init__(self, hub: ButtonPlusHub): + super().__init__(hub, "large", EventType.BRIGHTNESS_LARGE_DISPLAY) + + @property + def name(self) -> str: + """Return the display name of this light.""" + return f'Brightness large display' diff --git a/custom_components/button_plus/resource/config.json.notes b/custom_components/button_plus/resource/config.json.notes index 9523563..fa01abe 100644 --- a/custom_components/button_plus/resource/config.json.notes +++ b/custom_components/button_plus/resource/config.json.notes @@ -1,5 +1,5 @@ { - "info": { + "info": { "largedisplay": 3, // on which connector the large display is hooked up? "i2cs": [], // serial communication data probably "connectors": [ // Which button+ modules are attached after the screen and the 2 buttons @@ -16,13 +16,27 @@ }, "core": { "name": "demo3", // Button+ set name - "location": "Living Room", // location where its found + "location": "Living Room", // location where its found "color": 15140872, // 24 bit integer representation of the main color - "invert": true // wether to invert colors + "invert": true, // wether to invert colors + "topics": [ //active topics + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/demo3/brightness/large", + "payload": "", + "eventtype": 24 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/demo3/brightness/mini", + "payload": "", + "eventtype": 25 + } + ] }, "mqttbuttons": [ // all possible available button configurations with what mqtt to publish or subscribe { - "id": 0, + "id": 0, "label": "Btn 0", "topics": [] }, @@ -52,14 +66,14 @@ "topics": [] }, { - "id": 6, // not following this id scheme yet, the left display button has id 6 for some reason. probably going over each connector and its type + "id": 6, // not following this id scheme yet, the left display button has id 6 for some reason. probably going over each connector and its type "label": "Btn 8", // label of this button "topics": [ // array of mqtt topics to publish or subscribe { "brokerid": "buttonplus", "topic": "buttonplus/abc", "payload": "dummy", - "eventtype": 0 + "eventtype": 0 // different types of event we can have topics for in mqtt: // 0 = 'Click' // 1 = 'Long press' @@ -125,7 +139,7 @@ // 5 = 'Center Right' // 6 = 'Bottom Left' // 7 = 'Bottom Center' - // 8 = 'Bottom Right' + // 8 = 'Bottom Right' "width": 60, // in % of the display width "label": "Central European Time", "round": 0, // Round the incoming payload to decimal places, 0 is round to whole numbers From 6665b91ba36ab8b2406c1cd712019b3793046817 Mon Sep 17 00:00:00 2001 From: P-Storm Date: Sun, 10 Mar 2024 12:34:58 +0000 Subject: [PATCH 2/2] Fixes multiple devices problems --- custom_components/button_plus/number.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/custom_components/button_plus/number.py b/custom_components/button_plus/number.py index 1e42c8f..644d4f6 100644 --- a/custom_components/button_plus/number.py +++ b/custom_components/button_plus/number.py @@ -18,7 +18,6 @@ _LOGGER = logging.getLogger(__name__) -brightness = [] async def async_setup_entry( @@ -26,6 +25,9 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: + + brightness = [] + """Add switches for passed config_entry in HA.""" hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] @@ -53,12 +55,12 @@ def __init__(self, hub: ButtonPlusHub, brightness_type: str, event_type: EventTy self._hub = hub self._hub_id = hub.hub_id self._brightness_type = brightness_type - self._attr_unique_id = f'brightness-{brightness_type}-{self._hub_id}' self.entity_id = f"brightness.{brightness_type}_{self._hub_id}" self._attr_name = f'brightness-{brightness_type}' self.event_type = event_type self._topics = hub.config.core.topics self._attr_icon = "mdi:television-ambient-light" + self._attr_unique_id = f'brightness_{brightness_type}-{self._hub_id}' @property def native_max_value(self) -> float: @@ -85,13 +87,13 @@ def device_info(self)-> DeviceInfo: match self.event_type: case EventType.BRIGHTNESS_LARGE_DISPLAY: - name = f"Display Module" + name = f"{self._hub_id} Display Module" connections: set[tuple[str, str]] = {("display_module", 1)} model = "Display Module" identifiers = {(DOMAIN, f'{self._hub.hub_id}_large_display_module')} device_info = DeviceInfo(name=name, connections=connections, model=model, identifiers=identifiers, manufacturer=MANUFACTURER, via_device=via_device) case EventType.BRIGHTNESS_MINI_DISPLAY: - name = f"BAR Module 1" + name = f"{self._hub_id} BAR Module 1" #name = f"Brightness Mini Display Module" connections: set[tuple[str, str]] = { #(DOMAIN, f'{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.connector_id}')