From 43145641de12937889685983868a446e572cf01d Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Tue, 23 Jul 2024 22:52:22 +0200 Subject: [PATCH 01/29] Add versioning to button_plus_api --- .gitignore | 56 +-- custom_components/button_plus/__init__.py | 5 +- .../button_plus_api/model_detection.py | 16 + .../button_plus_api/model_interface.py | 10 + .../{model.py => model_v1_07.py} | 1 - .../button_plus_api/model_v1_12.py | 284 ++++++++++++ .../button_plus/buttonplushub.py | 3 +- custom_components/button_plus/manifest.json | 2 +- custom_components/button_plus/sensor.py | 1 + pyproject.toml | 5 + .../resource => resource}/config.json.notes | 0 .../resource => resource}/physicalconfig.json | 0 .../physicalconfig1.07.json | 0 resource/physicalconfig1.12.1.json | 421 ++++++++++++++++++ .../resource => resource}/virtualconfig.json | 0 .../button_plus_api/test_model_v1_07.py | 68 +++ .../button_plus_api/test_model_v1_12.py | 91 ++++ 17 files changed, 903 insertions(+), 60 deletions(-) create mode 100644 custom_components/button_plus/button_plus_api/model_detection.py create mode 100644 custom_components/button_plus/button_plus_api/model_interface.py rename custom_components/button_plus/button_plus_api/{model.py => model_v1_07.py} (99%) create mode 100644 custom_components/button_plus/button_plus_api/model_v1_12.py rename {custom_components/button_plus/resource => resource}/config.json.notes (100%) rename {custom_components/button_plus/resource => resource}/physicalconfig.json (100%) rename {custom_components/button_plus/resource => resource}/physicalconfig1.07.json (100%) create mode 100644 resource/physicalconfig1.12.1.json rename {custom_components/button_plus/resource => resource}/virtualconfig.json (100%) create mode 100644 tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py create mode 100644 tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py diff --git a/.gitignore b/.gitignore index f620e99..077427f 100644 --- a/.gitignore +++ b/.gitignore @@ -155,77 +155,23 @@ cython_debug/ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr # CMake cmake-build-*/ -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - # File-based project format *.iws # IntelliJ out/ -# mpeltonen/sbt-idea plugin -.idea_modules/ - # JIRA plugin atlassian-ide-plugin.xml -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser +.idea \ No newline at end of file diff --git a/custom_components/button_plus/__init__.py b/custom_components/button_plus/__init__.py index 3f32854..2117c8b 100644 --- a/custom_components/button_plus/__init__.py +++ b/custom_components/button_plus/__init__.py @@ -7,7 +7,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from custom_components.button_plus.button_plus_api.model import DeviceConfiguration +from custom_components.button_plus.button_plus_api.model_interface import DeviceConfiguration +from custom_components.button_plus.button_plus_api.model_detection import ModelDetection from custom_components.button_plus.buttonplushub import ButtonPlusHub from custom_components.button_plus.const import DOMAIN from custom_components.button_plus.coordinator import ButtonPlusCoordinator @@ -23,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Button+ from a config entry.""" _LOGGER.debug(f"Button+ init got new device entry! {entry.entry_id.title}") - device_configuration: DeviceConfiguration = DeviceConfiguration.from_json( + device_configuration: DeviceConfiguration = ModelDetection.model_for_json( entry.data.get("config") ) diff --git a/custom_components/button_plus/button_plus_api/model_detection.py b/custom_components/button_plus/button_plus_api/model_detection.py new file mode 100644 index 0000000..a21b834 --- /dev/null +++ b/custom_components/button_plus/button_plus_api/model_detection.py @@ -0,0 +1,16 @@ +from typing import Dict, Any +import json +from packaging.version import parse as parseSemver, Version as SemverVersion +from .model_interface import DeviceConfiguration + +class ModelDetection: + @staticmethod + def model_for_json(json_data: str) -> "DeviceConfiguration": + data = json.loads(json_data) + deviceVersion = parseSemver(data["info"]["firmware"]); + if deviceVersion >= parseSemver("1.12.0"): + from .model_v1_12 import DeviceConfiguration + return DeviceConfiguration() + else: + from .model_v1_07 import DeviceConfiguration + return DeviceConfiguration() diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py new file mode 100644 index 0000000..0833d2e --- /dev/null +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -0,0 +1,10 @@ + +class DeviceConfiguration: + @staticmethod + def from_json(json_data: str) -> "DeviceConfiguration": + """ Deserialize the DeviceConfiguration from a JSON string. """ + pass + + def to_json(self) -> str: + """ Serialize the DeviceConfiguration to a JSON string. """ + pass diff --git a/custom_components/button_plus/button_plus_api/model.py b/custom_components/button_plus/button_plus_api/model_v1_07.py similarity index 99% rename from custom_components/button_plus/button_plus_api/model.py rename to custom_components/button_plus/button_plus_api/model_v1_07.py index ebaa496..ccffdc8 100644 --- a/custom_components/button_plus/button_plus_api/model.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -4,7 +4,6 @@ from .connector_type import ConnectorEnum from .event_type import EventType - class Connector: def __init__(self, connector_id: int, connector_type: int): self.connector_id = connector_id diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py new file mode 100644 index 0000000..d733246 --- /dev/null +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -0,0 +1,284 @@ +import json +from typing import List, Dict, Any + +from .connector_type import ConnectorEnum +from .event_type import EventType + + +import json +from typing import List, Dict, Any + +class Connector: + def __init__(self, connector_id: int, connector_type: int): + self.connector_id = connector_id + self.connector_type = connector_type + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Connector": + return Connector(connector_id=data["id"], connector_type=data["type"]) + +class Sensor: + def __init__(self, sensor_id: int, description: str): + self.sensor_id = sensor_id + self.description = description + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Sensor": + return Sensor(sensor_id=data["sensorid"], description=data["description"]) + +class Info: + def __init__(self, device_id: str, mac: str, ip_address: str, firmware: str, large_display: int, connectors: List[Connector], sensors: List[Sensor]): + self.device_id = device_id + self.mac = mac + self.ip_address = ip_address + self.firmware = firmware + self.large_display = large_display + self.connectors = connectors + self.sensors = sensors + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Info": + return Info( + device_id=data["id"], + mac=data["mac"], + ip_address=data["ipaddress"], + firmware=data["firmware"], + large_display=data["largedisplay"], + connectors=[Connector.from_dict(connector) for connector in data["connectors"]], + sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]] + ) + +class Topic: + def __init__(self, broker_id: str, topic: str, payload: str, event_type: int): + self.broker_id = broker_id + self.topic = topic + self.payload = payload + self.event_type = event_type + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Topic": + return Topic( + broker_id=data["brokerid"], + topic=data["topic"], + payload=data["payload"], + event_type=data["eventtype"] + ) + +class Core: + def __init__(self, name: str, location: str, auto_backup: bool, brightness: int, color: int, statusbar: int, topics: List[Topic]): + self.name = name + self.location = location + self.auto_backup = auto_backup + self.brightness = brightness + self.color = color + self.statusbar = statusbar + self.topics = topics + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Core": + return Core( + name=data["name"], + location=data["location"], + auto_backup=data["autobackup"], + brightness=data["brightness"], + color=data["color"], + statusbar=data["statusbar"], + topics=[Topic.from_dict(topic) for topic in data["topics"]] + ) + +class MqttButton: + def __init__(self, button_id: int, label: str, top_label: str, led_color_front: int, led_color_wall: int, long_delay: int, long_repeat: int, topics: List[Topic]): + self.button_id = button_id + self.label = label + self.top_label = top_label + self.led_color_front = led_color_front + self.led_color_wall = led_color_wall + self.long_delay = long_delay + self.long_repeat = long_repeat + self.topics = topics + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "MqttButton": + return MqttButton( + button_id=data["id"], + label=data["label"], + top_label=data["toplabel"], + led_color_front=data["ledcolorfront"], + led_color_wall=data["ledcolorwall"], + long_delay=data["longdelay"], + long_repeat=data["longrepeat"], + topics=[Topic.from_dict(topic) for topic in data["topics"]] + ) + +class MqttDisplay: + def __init__(self, align: int, x: int, y: int, box_type: int, font_size: int, page: int, label: str, width: int, unit: str, round: int, topics: List[Topic]): + self.x = x + self.y = y + self.box_type = box_type + self.font_size = font_size + self.align = align + self.width = width + self.label = label + self.unit = unit + self.round = round + self.page = page + self.topics = topics + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "MqttDisplay": + return MqttDisplay( + x=data["x"], + y=data["y"], + box_type=data["boxtype"], + font_size=data["fontsize"], + align=data["align"], + width=data["width"], + label=data["label"], + unit=data["unit"], + round=data["round"], + page=data["page"], + topics=[Topic.from_dict(topic) for topic in data["topics"]] + ) + +class MqttBroker: + def __init__(self, broker_id: str, url: str, port: int, ws_port: int, username: str, password: str): + self.broker_id = broker_id + self.url = url + self.port = port + self.ws_port = ws_port + self.username = username + self.password = password + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "MqttBroker": + return MqttBroker( + broker_id=data["brokerid"], + url=data["url"], + port=data["port"], + ws_port=data["wsport"], + username=data["username"], + password=data["password"] + ) + +class MqttSensor: + def __init__(self, sensor_id: int, interval: int, topic: Topic): + self.sensor_id = sensor_id + self.interval = interval + self.topic = topic + + @staticmethod + def from_dict(data: Dict[str, Any]) -> "MqttSensor": + return MqttSensor( + sensor_id=data["sensorid"], + interval=data["interval"], + topic=Topic.from_dict(data["topic"]) + ) + +class DeviceConfiguration: + def __init__(self, info: Info, core: Core, mqtt_buttons: List[MqttButton], mqtt_displays: List[MqttDisplay], mqtt_brokers: List[MqttBroker], mqtt_sensors: List[MqttSensor]): + self.info = info + self.core = core + self.mqtt_buttons = mqtt_buttons + self.mqtt_displays = mqtt_displays + self.mqtt_brokers = mqtt_brokers + self.mqtt_sensors = mqtt_sensors + + @staticmethod + def from_json(json_data: str) -> "DeviceConfiguration": + data = json.loads(json_data) + return DeviceConfiguration( + info=Info.from_dict(data["info"]), + core=Core.from_dict(data["core"]), + mqtt_buttons=[MqttButton.from_dict(button) for button in data["mqttbuttons"]], + mqtt_displays=[MqttDisplay.from_dict(display) for display in data["mqttdisplays"]], + mqtt_brokers=[MqttBroker.from_dict(broker) for broker in data["mqttbrokers"]], + mqtt_sensors=[MqttSensor.from_dict(sensor) for sensor in data["mqttsensors"]] + ) + + def to_json(self) -> str: + def serialize(obj): + if hasattr(obj, "__dict__"): + d = obj.__dict__.copy() + + if isinstance(obj, DeviceConfiguration): + d["info"] = serialize(d.pop("info")) + d["core"] = serialize(d.pop("core")) + d["mqttbuttons"] = [serialize(button) for button in d.pop("mqtt_buttons")] + d["mqttdisplays"] = [serialize(display) for display in d.pop("mqtt_displays")] + d["mqttbrokers"] = [serialize(broker) for broker in d.pop("mqtt_brokers")] + d["mqttsensors"] = [serialize(sensor) for sensor in d.pop("mqtt_sensors")] + + if isinstance(obj, Info): + d["id"] = d.pop("device_id") + d["mac"] = d.pop("mac") + d["ipaddress"] = d.pop("ip_address") + d["firmware"] = d.pop("firmware") + d["largedisplay"] = d.pop("large_display") + d["connectors"] = [serialize(connector) for connector in d.pop("connectors")] + d["sensors"] = [serialize(sensor) for sensor in d.pop("sensors")] + + elif isinstance(obj, Connector): + d["id"] = d.pop("connector_id") + d["type"] = d.pop("connector_type") + + elif isinstance(obj, Sensor): + d["sensorid"] = d.pop("sensor_id") + d["description"] = d.pop("description") + + elif isinstance(obj, Core): + d["name"] = d.pop("name") + d["location"] = d.pop("location") + d["autobackup"] = d.pop("auto_backup") + d["brightness"] = d.pop("brightness") + d["color"] = d.pop("color") + d["statusbar"] = d.pop("statusbar") + d["topics"] = [serialize(topic) for topic in d.pop("topics")] + + elif isinstance(obj, MqttButton): + d["id"] = d.pop("button_id") + d["label"] = d.pop("label") + d["toplabel"] = d.pop("top_label") + d["ledcolorfront"] = d.pop("led_color_front") + d["ledcolorwall"] = d.pop("led_color_wall") + d["longdelay"] = d.pop("long_delay") + d["longrepeat"] = d.pop("long_repeat") + d["topics"] = [serialize(topic) for topic in d.pop("topics")] + + elif isinstance(obj, Topic): + d["brokerid"] = d.pop("broker_id") + d["topic"] = d.pop("topic") + d["payload"] = d.pop("payload") + d["eventtype"] = d.pop("event_type") + + elif isinstance(obj, MqttDisplay): + d["x"] = d.pop("x") + d["y"] = d.pop("y") + d["boxtype"] = d.pop("box_type") + d["fontsize"] = d.pop("font_size") + d["align"] = d.pop("align") + d["width"] = d.pop("width") + d["label"] = d.pop("label") + d["unit"] = d.pop("unit") + d["round"] = d.pop("round") + d["page"] = d.pop("page") + d["topics"] = [serialize(topic) for topic in d.pop("topics")] + + elif isinstance(obj, MqttBroker): + d["brokerid"] = d.pop("broker_id") + d["url"] = d.pop("url") + d["port"] = d.pop("port") + d["wsport"] = d.pop("ws_port") + d["username"] = d.pop("username") + d["password"] = d.pop("password") + + elif isinstance(obj, MqttSensor): + d["sensorid"] = d.pop("sensor_id") + d["interval"] = d.pop("interval") + d["topic"] = serialize(d["topic"]) + + return {k: v for k, v in d.items() if v is not None} + else: + return str(obj) + + return json.dumps(self, default=serialize) + diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 4d7c9b5..41041a9 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -10,7 +10,8 @@ from homeassistant.helpers import aiohttp_client from .button_plus_api.local_api_client import LocalApiClient -from .button_plus_api.model import ConnectorEnum, DeviceConfiguration +from .button_plus_api.connector_type import ConnectorEnum +from .button_plus_api.model_interface import DeviceConfiguration from .const import DOMAIN, MANUFACTURER diff --git a/custom_components/button_plus/manifest.json b/custom_components/button_plus/manifest.json index 14c72b5..707afe5 100644 --- a/custom_components/button_plus/manifest.json +++ b/custom_components/button_plus/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "local_push", "issue_tracker": "https://github.com/koenhendriks/ha-button-plus/issues", - "requirements": [], + "requirements": ["packaging"], "version": "0.0.1", "zeroconf": [] } diff --git a/custom_components/button_plus/sensor.py b/custom_components/button_plus/sensor.py index f823489..12eeedf 100644 --- a/custom_components/button_plus/sensor.py +++ b/custom_components/button_plus/sensor.py @@ -27,6 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): new_devices = [] for device in hub.devices: new_devices.append(IlluminanceSensor(device)) + if new_devices: async_add_entities(new_devices) diff --git a/pyproject.toml b/pyproject.toml index d7d2868..302d527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,3 +6,8 @@ dependencies = [ "homeassistant", "pre-commit", ] + +[tool.pytest.ini_options] +pythonpath = [ + "." +] \ No newline at end of file diff --git a/custom_components/button_plus/resource/config.json.notes b/resource/config.json.notes similarity index 100% rename from custom_components/button_plus/resource/config.json.notes rename to resource/config.json.notes diff --git a/custom_components/button_plus/resource/physicalconfig.json b/resource/physicalconfig.json similarity index 100% rename from custom_components/button_plus/resource/physicalconfig.json rename to resource/physicalconfig.json diff --git a/custom_components/button_plus/resource/physicalconfig1.07.json b/resource/physicalconfig1.07.json similarity index 100% rename from custom_components/button_plus/resource/physicalconfig1.07.json rename to resource/physicalconfig1.07.json diff --git a/resource/physicalconfig1.12.1.json b/resource/physicalconfig1.12.1.json new file mode 100644 index 0000000..e22bcce --- /dev/null +++ b/resource/physicalconfig1.12.1.json @@ -0,0 +1,421 @@ +{ + "info": { + "id": "btn_4584b8", + "mac": "F4:12:FA:45:84:B8", + "ipaddress": "192.168.102.10", + "firmware": "1.12.2", + "largedisplay": 0, + "connectors": [ + { + "id": 0, + "type": 2 + }, + { + "id": 1, + "type": 1 + }, + { + "id": 2, + "type": 1 + }, + { + "id": 3, + "type": 1 + } + ], + "sensors": [ + { + "sensorid": 1, + "description": "Sensirion STS35 Temperature Sensor" + } + ] + }, + "core": { + "name": "btn_4584b8", + "location": "Room 1", + "autobackup": true, + "brightness": 80, + "color": 16765791, + "statusbar": 2, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/page/status", + "payload": "", + "eventtype": 6 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/page/set", + "payload": "", + "eventtype": 20 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/brightness/large", + "payload": "", + "eventtype": 24 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/brightness/mini", + "payload": "", + "eventtype": 25 + } + ] + }, + "mqttbuttons": [ + { + "id": 0, + "label": "Btn 0", + "toplabel": "Label", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/0/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/0/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/0/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/0/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 1, + "label": "Btn 1", + "toplabel": "Label", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/1/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/1/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/1/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/1/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 2, + "label": "SNEL", + "toplabel": "Autoladen", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/2/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/2/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/2/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/2/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 3, + "label": "Thuis", + "toplabel": "Aanwezigheid", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/3/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/3/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/3/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/3/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 4, + "label": "Uit tot 22:25", + "toplabel": "Airco", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/4/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/4/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/4/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/4/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 5, + "label": "Btn", + "toplabel": "Label", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/5/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/5/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/5/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/5/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 6, + "label": "Btn 6", + "toplabel": "Label", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/6/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/6/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/6/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/6/top_label", + "payload": "", + "eventtype": 12 + } + ] + }, + { + "id": 7, + "label": "Btn 7", + "toplabel": "Label", + "ledcolorfront": 0, + "ledcolorwall": 0, + "longdelay": 40, + "longrepeat": 15, + "topics": [ + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/7/click", + "payload": "press", + "eventtype": 0 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/7/long_press", + "payload": "press", + "eventtype": 1 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/7/label", + "payload": "", + "eventtype": 11 + }, + { + "brokerid": "ha-button-plus", + "topic": "buttonplus/btn_4584b8/button/7/top_label", + "payload": "", + "eventtype": 12 + } + ] + } + ], + "mqttdisplays": [ + { + "x": 0, + "y": 0, + "boxtype": 0, + "fontsize": 4, + "align": 1, + "width": 50, + "round": 0, + "label": "Amsterdam", + "unit": "", + "page": 0, + "topics": [ + { + "brokerid": "buttonplus", + "topic": "system/datetime/amsterdam", + "payload": "", + "eventtype": 15 + } + ] + }, + { + "x": 0, + "y": 40, + "boxtype": 0, + "fontsize": 2, + "align": 1, + "width": 30, + "round": 1, + "label": "", + "unit": "°C", + "page": 0, + "topics": [ + { + "brokerid": "buttonplus", + "topic": "button/btn_4584b8/temperature", + "payload": "", + "eventtype": 15 + } + ] + } + ], + "mqttbrokers": [ + { + "brokerid": "ha", + "url": "ha.localdomain", + "port": 0, + "wsport": 0, + "username": "mqtt_user", + "password": "mqtt_password" + }, + { + "brokerid": "ha-button-plus", + "url": "mqtt://ha.localdomain/", + "port": 1883, + "wsport": 9001, + "username": "mqtt_user", + "password": "mqtt_password" + } + ], + "mqttsensors": [ + { + "sensorid": 1, + "interval": 10, + "topic": { + "brokerid": "buttonplus", + "topic": "button/btn_4584b8/temperature", + "payload": "", + "eventtype": 18 + } + } + ] +} \ No newline at end of file diff --git a/custom_components/button_plus/resource/virtualconfig.json b/resource/virtualconfig.json similarity index 100% rename from custom_components/button_plus/resource/virtualconfig.json rename to resource/virtualconfig.json diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py new file mode 100644 index 0000000..3a04318 --- /dev/null +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py @@ -0,0 +1,68 @@ +import pytest + +from custom_components.button_plus.button_plus_api.model_v1_07 import DeviceConfiguration + +@pytest.fixture +def device_config(): + # Load and parse the JSON file + with open('resource/physicalconfig1.07.json') as file: + json_data = file.read() + # Parse the JSON data into a DeviceConfiguration object + return DeviceConfiguration.from_json(json_data) + +def test_buttons(device_config): + buttons = device_config.mqtt_buttons + assert len(buttons) == 8 + assert buttons[0].label == 'Btn 0' + assert buttons[0].top_label == 'Label' + assert buttons[0].led_color_front == 0 + assert buttons[0].led_color_wall == 0 + assert buttons[0].long_delay == 75 + assert buttons[0].long_repeat == 15 + assert buttons[2].topics[0].broker_id == 'hassdev' + assert buttons[2].topics[0].topic == 'buttonplus/btn_4967c8/bars/2/click' + assert buttons[2].topics[0].payload == 'true' + assert buttons[2].topics[0].event_type == 0 + +def test_mqttdisplays(device_config): + mqttdisplays = device_config.mqtt_displays + assert len(mqttdisplays) == 2 + assert mqttdisplays[0].x == 0 + assert mqttdisplays[0].y == 0 + assert mqttdisplays[0].font_size == 4 + assert mqttdisplays[0].align == 0 + assert mqttdisplays[0].width == 50 + assert mqttdisplays[0].round == 0 + assert mqttdisplays[0].label == 'Amsterdam' + assert mqttdisplays[0].unit == '' + assert mqttdisplays[0].topics[0].broker_id == 'buttonplus' + assert mqttdisplays[0].topics[0].topic == 'system/datetime/amsterdam' + assert mqttdisplays[0].topics[0].payload == '' + assert mqttdisplays[0].topics[0].event_type == 15 + assert mqttdisplays[1].unit == '°C' + +def test_mqttbrokers(device_config): + mqttbrokers = device_config.mqtt_brokers + assert len(mqttbrokers) == 2 + assert mqttbrokers[0].broker_id == 'buttonplus' + assert mqttbrokers[0].url == 'mqtt://mqtt.button.plus' + assert mqttbrokers[0].port == 0 + assert mqttbrokers[0].ws_port == 0 + assert mqttbrokers[0].username == '' + assert mqttbrokers[0].password == '' + assert mqttbrokers[1].broker_id == 'hassdev' + assert mqttbrokers[1].url == 'mqtt://192.168.2.16/' + assert mqttbrokers[1].port == 1883 + assert mqttbrokers[1].ws_port == 9001 + assert mqttbrokers[1].username == 'koen' + assert mqttbrokers[1].password == 'koen' + +def test_mqttsensors(device_config): + mqttsensors = device_config.mqtt_sensors + assert len(mqttsensors) == 1 + assert mqttsensors[0].sensor_id == 1 + assert mqttsensors[0].interval == 10 + assert mqttsensors[0].topic.broker_id == 'buttonplus' + assert mqttsensors[0].topic.topic == 'button/btn_4967c8/temperature' + assert mqttsensors[0].topic.payload == '' + assert mqttsensors[0].topic.event_type == 18 \ No newline at end of file diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py new file mode 100644 index 0000000..59a1340 --- /dev/null +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py @@ -0,0 +1,91 @@ +from custom_components.button_plus.button_plus_api.model_v1_12 import DeviceConfiguration +import json + + +def test_model_v1_12_from_to_json_should_be_same(): + # Load the JSON file + with open('resource/physicalconfig1.12.1.json') as file: + json_data = file.read() + + # Parse the JSON data into a DeviceConfiguration object + device_config = DeviceConfiguration.from_json(json_data) + + # Serialize the DeviceConfiguration object back into a JSON string + jsonString = device_config.to_json() + + originalJsonData = json.loads(json_data) + newJSONData = json.loads(jsonString) + + # Assert that the JSON strings are the same + assert originalJsonData == newJSONData + + +def test_model_v1_12(): + # Load the JSON file + with open('resource/physicalconfig1.12.1.json') as file: + json_data = file.read() + + # Parse the JSON data into a DeviceConfiguration object + device_config = DeviceConfiguration.from_json(json_data) + + # Assert the values from the parsed DeviceConfiguration object + assert device_config.info.device_id == "btn_4584b8" + assert device_config.info.mac == "F4:12:FA:45:84:B8" + assert device_config.info.ip_address == "192.168.102.10" + assert device_config.info.firmware == "1.12.2" + assert device_config.info.large_display == 0 + + # Assert the values from the parsed Core object + assert device_config.core.name == "btn_4584b8" + assert device_config.core.location == "Room 1" + assert device_config.core.auto_backup == True + assert device_config.core.brightness == 80 + assert device_config.core.color == 16765791 + assert device_config.core.statusbar == 2 + + # Assert the values from the parsed MqttButton objects + assert len(device_config.mqtt_buttons) == 8 + assert device_config.mqtt_buttons[0].button_id == 0 + assert device_config.mqtt_buttons[0].label == "Btn 0" + assert device_config.mqtt_buttons[0].top_label == "Label" + assert device_config.mqtt_buttons[0].led_color_front == 0 + assert device_config.mqtt_buttons[0].led_color_wall == 0 + assert device_config.mqtt_buttons[0].long_delay == 40 + assert device_config.mqtt_buttons[0].long_repeat == 15 + + assert device_config.mqtt_buttons[1].button_id == 1 + assert device_config.mqtt_buttons[1].label == "Btn 1" + assert device_config.mqtt_buttons[1].top_label == "Label" + assert device_config.mqtt_buttons[1].led_color_front == 0 + assert device_config.mqtt_buttons[1].led_color_wall == 0 + assert device_config.mqtt_buttons[1].long_delay == 40 + assert device_config.mqtt_buttons[1].long_repeat == 15 + + # Assert the values from the parsed MqttDisplay objects + assert len(device_config.mqtt_displays) == 2 + assert device_config.mqtt_displays[0].x == 0 + assert device_config.mqtt_displays[0].y == 0 + assert device_config.mqtt_displays[0].font_size == 4 + assert device_config.mqtt_displays[0].align == 1 + assert device_config.mqtt_displays[0].width == 50 + assert device_config.mqtt_displays[0].label == "Amsterdam" + assert device_config.mqtt_displays[0].unit == "" + assert device_config.mqtt_displays[0].round == 0 + + # Assert the values from the parsed MqttBroker objects + assert len(device_config.mqtt_brokers) == 2 + assert device_config.mqtt_brokers[0].broker_id == "ha" + assert device_config.mqtt_brokers[0].url == "ha.localdomain" + assert device_config.mqtt_brokers[0].port == 0 + assert device_config.mqtt_brokers[0].ws_port == 0 + assert device_config.mqtt_brokers[0].username == "mqtt_user" + assert device_config.mqtt_brokers[0].password == "mqtt_password" + + # Assert the values from the parsed MqttSensor objects + assert len(device_config.mqtt_sensors) == 1 + assert device_config.mqtt_sensors[0].sensor_id == 1 + assert device_config.mqtt_sensors[0].interval == 10 + assert device_config.mqtt_sensors[0].topic.broker_id == "buttonplus" + assert device_config.mqtt_sensors[0].topic.topic == "button/btn_4584b8/temperature" + assert device_config.mqtt_sensors[0].topic.payload == "" + assert device_config.mqtt_sensors[0].topic.event_type == 18 From 7e0c76667c879ad1ec1e603d0221021cfa36ea3c Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:13:06 +0200 Subject: [PATCH 02/29] Add model version detection --- .../button_plus_api/model_detection.py | 10 ++++--- .../button_plus_api/model_v1_07.py | 15 +++++------ .../button_plus_api/model_v1_12.py | 15 +++++------ .../button_plus_api/test_model_detection.py | 27 +++++++++++++++++++ .../button_plus_api/test_model_v1_07.py | 10 +++++-- .../button_plus_api/test_model_v1_12.py | 12 ++++----- 6 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 tests/custom_components/button_plus/button_plus_api/test_model_detection.py diff --git a/custom_components/button_plus/button_plus_api/model_detection.py b/custom_components/button_plus/button_plus_api/model_detection.py index a21b834..282ce48 100644 --- a/custom_components/button_plus/button_plus_api/model_detection.py +++ b/custom_components/button_plus/button_plus_api/model_detection.py @@ -3,14 +3,16 @@ from packaging.version import parse as parseSemver, Version as SemverVersion from .model_interface import DeviceConfiguration + class ModelDetection: @staticmethod def model_for_json(json_data: str) -> "DeviceConfiguration": data = json.loads(json_data) - deviceVersion = parseSemver(data["info"]["firmware"]); - if deviceVersion >= parseSemver("1.12.0"): + device_version = parseSemver(data["info"]["firmware"]) + + if device_version >= parseSemver("1.12.0"): from .model_v1_12 import DeviceConfiguration - return DeviceConfiguration() + return DeviceConfiguration.from_json(data) else: from .model_v1_07 import DeviceConfiguration - return DeviceConfiguration() + return DeviceConfiguration.from_json(data) diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index ccffdc8..6de844c 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -254,22 +254,21 @@ def __init__( self.mqtt_sensors = mqtt_sensors @staticmethod - def from_json(json_data: str) -> "DeviceConfiguration": - data = json.loads(json_data) + def from_json(json_data: any) -> "DeviceConfiguration": return DeviceConfiguration( - info=Info.from_dict(data["info"]), - core=Core.from_dict(data["core"]), + info=Info.from_dict(json_data["info"]), + core=Core.from_dict(json_data["core"]), mqtt_buttons=[ - MqttButton.from_dict(button) for button in data["mqttbuttons"] + MqttButton.from_dict(button) for button in json_data["mqttbuttons"] ], mqtt_displays=[ - MqttDisplay.from_dict(display) for display in data["mqttdisplays"] + MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"] ], mqtt_brokers=[ - MqttBroker.from_dict(broker) for broker in data["mqttbrokers"] + MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"] ], mqtt_sensors=[ - MqttSensor.from_dict(sensor) for sensor in data["mqttsensors"] + MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] ], ) diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index d733246..c2682a7 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -184,15 +184,14 @@ def __init__(self, info: Info, core: Core, mqtt_buttons: List[MqttButton], mqtt_ self.mqtt_sensors = mqtt_sensors @staticmethod - def from_json(json_data: str) -> "DeviceConfiguration": - data = json.loads(json_data) + def from_json(json_data: any) -> "DeviceConfiguration": return DeviceConfiguration( - info=Info.from_dict(data["info"]), - core=Core.from_dict(data["core"]), - mqtt_buttons=[MqttButton.from_dict(button) for button in data["mqttbuttons"]], - mqtt_displays=[MqttDisplay.from_dict(display) for display in data["mqttdisplays"]], - mqtt_brokers=[MqttBroker.from_dict(broker) for broker in data["mqttbrokers"]], - mqtt_sensors=[MqttSensor.from_dict(sensor) for sensor in data["mqttsensors"]] + info=Info.from_dict(json_data["info"]), + core=Core.from_dict(json_data["core"]), + mqtt_buttons=[MqttButton.from_dict(button) for button in json_data["mqttbuttons"]], + mqtt_displays=[MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"]], + mqtt_brokers=[MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"]], + mqtt_sensors=[MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"]] ) def to_json(self) -> str: diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_detection.py b/tests/custom_components/button_plus/button_plus_api/test_model_detection.py new file mode 100644 index 0000000..a14e579 --- /dev/null +++ b/tests/custom_components/button_plus/button_plus_api/test_model_detection.py @@ -0,0 +1,27 @@ +import pytest +import json +from custom_components.button_plus.button_plus_api.model_detection import ModelDetection +from custom_components.button_plus.button_plus_api.model_v1_07 import DeviceConfiguration as DeviceConfiguration_v1_07 +from custom_components.button_plus.button_plus_api.model_v1_12 import DeviceConfiguration as DeviceConfiguration_v1_12 + + +@pytest.fixture +def json_data_v1_07(): + with open('resource/physicalconfig1.07.json') as file: + return file.read() + + +@pytest.fixture +def json_data_v1_12(): + with open('resource/physicalconfig1.12.1.json') as file: + return file.read() + + +def test_model_for_json_v1_07(json_data_v1_07): + device_config = ModelDetection.model_for_json(json_data_v1_07) + assert isinstance(device_config, DeviceConfiguration_v1_07) + + +def test_model_for_json_v1_12(json_data_v1_12): + device_config = ModelDetection.model_for_json(json_data_v1_12) + assert isinstance(device_config, DeviceConfiguration_v1_12) diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py index 3a04318..aa3cdf2 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py @@ -1,15 +1,18 @@ import pytest +import json from custom_components.button_plus.button_plus_api.model_v1_07 import DeviceConfiguration + @pytest.fixture def device_config(): # Load and parse the JSON file with open('resource/physicalconfig1.07.json') as file: - json_data = file.read() + json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object return DeviceConfiguration.from_json(json_data) + def test_buttons(device_config): buttons = device_config.mqtt_buttons assert len(buttons) == 8 @@ -24,6 +27,7 @@ def test_buttons(device_config): assert buttons[2].topics[0].payload == 'true' assert buttons[2].topics[0].event_type == 0 + def test_mqttdisplays(device_config): mqttdisplays = device_config.mqtt_displays assert len(mqttdisplays) == 2 @@ -41,6 +45,7 @@ def test_mqttdisplays(device_config): assert mqttdisplays[0].topics[0].event_type == 15 assert mqttdisplays[1].unit == '°C' + def test_mqttbrokers(device_config): mqttbrokers = device_config.mqtt_brokers assert len(mqttbrokers) == 2 @@ -57,6 +62,7 @@ def test_mqttbrokers(device_config): assert mqttbrokers[1].username == 'koen' assert mqttbrokers[1].password == 'koen' + def test_mqttsensors(device_config): mqttsensors = device_config.mqtt_sensors assert len(mqttsensors) == 1 @@ -65,4 +71,4 @@ def test_mqttsensors(device_config): assert mqttsensors[0].topic.broker_id == 'buttonplus' assert mqttsensors[0].topic.topic == 'button/btn_4967c8/temperature' assert mqttsensors[0].topic.payload == '' - assert mqttsensors[0].topic.event_type == 18 \ No newline at end of file + assert mqttsensors[0].topic.event_type == 18 diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py index 59a1340..718a7df 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py @@ -5,25 +5,25 @@ def test_model_v1_12_from_to_json_should_be_same(): # Load the JSON file with open('resource/physicalconfig1.12.1.json') as file: - json_data = file.read() + json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object device_config = DeviceConfiguration.from_json(json_data) # Serialize the DeviceConfiguration object back into a JSON string - jsonString = device_config.to_json() + json_string = device_config.to_json() - originalJsonData = json.loads(json_data) - newJSONData = json.loads(jsonString) + original_json_data = json_data + new_json_data = json.loads(json_string) # Assert that the JSON strings are the same - assert originalJsonData == newJSONData + assert original_json_data == new_json_data def test_model_v1_12(): # Load the JSON file with open('resource/physicalconfig1.12.1.json') as file: - json_data = file.read() + json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object device_config = DeviceConfiguration.from_json(json_data) From 504c27d4e60efb302a7140f6af26be76d7502e34 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:15:38 +0200 Subject: [PATCH 03/29] Add dependency to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 302d527..5d9c20f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ dynamic = ["version"] dependencies = [ "homeassistant", "pre-commit", + "packaging" ] [tool.pytest.ini_options] From f9f5bd2ac8915d5ff690653fe824d29d4d12ce9a Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Wed, 24 Jul 2024 00:04:52 +0200 Subject: [PATCH 04/29] Added Docker Compose file for easy local testing --- .gitignore | 2 ++ compose.yml | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 compose.yml diff --git a/.gitignore b/.gitignore index 077427f..66681f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +ha_config + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f82b0da --- /dev/null +++ b/compose.yml @@ -0,0 +1,14 @@ +services: + homeassistant: + container_name: homeassistant + image: "ghcr.io/home-assistant/home-assistant:stable" + volumes: + - ./ha_config:/config + - ./custom_components:/config/custom_components + - /etc/localtime:/etc/localtime:ro + - /run/dbus:/run/dbus:ro + restart: unless-stopped + privileged: true + ports: + - "8123:8123" + From 9c02c827e0d727098fedfa923c0f5802d3020ead Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:11:55 +0200 Subject: [PATCH 05/29] More compatibility interface done --- custom_components/button_plus/button.py | 33 ++-- .../button_plus/button_plus_api/api_client.py | 2 +- .../button_plus_api/connector_type.py | 2 +- .../button_plus_api/model_detection.py | 2 +- .../button_plus_api/model_interface.py | 56 +++++++ .../button_plus_api/model_v1_07.py | 154 +++++++++++------- .../button_plus_api/model_v1_12.py | 139 +++++----------- .../button_plus/buttonplushub.py | 62 ++++--- custom_components/button_plus/coordinator.py | 5 +- custom_components/button_plus/device.py | 29 ++-- 10 files changed, 254 insertions(+), 230 deletions(-) diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index dbdf3f7..765791f 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -16,7 +16,7 @@ async_get_current_platform, ) -from .button_plus_api.model import Connector, ConnectorEnum +from .button_plus_api.model_interface import Connector, ConnectorType from .const import DOMAIN from . import ButtonPlusHub @@ -37,14 +37,13 @@ async def async_setup_entry( button_entities: list[ButtonPlusButton] = [] hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] - active_connectors = active_connectors = [ - connector.connector_id - for connector in hub.config.info.connectors - if connector.connector_type_enum() in [ConnectorEnum.DISPLAY, ConnectorEnum.BAR] + active_connectors = [ + connector.identifier() + for connector in hub.config.connectors_for(ConnectorType.BAR, ConnectorType.DISPLAY) ] buttons = filter( - lambda b: b.button_id // 2 in active_connectors, hub.config.mqtt_buttons + lambda b: b.button_id // 2 in active_connectors, hub.config.buttons() ) for button in buttons: @@ -83,18 +82,18 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub): self._attr_name = f"button-{btn_id}" self._name = f"Button {btn_id}" self._device_class = ButtonDeviceClass.IDENTIFY - self._connector: Connector = hub.config.info.connectors[btn_id // 2] - self.unique_id = self.unique_id_gen() + self._connector: Connector = hub.config.connector_for(btn_id // 2) + self.our_id = self.unique_id_gen() def unique_id_gen(self): - match self._connector.connector_type_enum(): - case ConnectorEnum.BAR: + match self._connector.connector_type(): + case ConnectorType.BAR: return self.unique_id_gen_bar() - case ConnectorEnum.DISPLAY: + case ConnectorType.DISPLAY: return self.unique_id_gen_display() def unique_id_gen_bar(self): - return f"button_{self._hub_id}_{self._btn_id}_bar_module_{self._connector.connector_id}" + return f"button_{self._hub_id}_{self._btn_id}_bar_module_{self._connector.identifier()}" def unique_id_gen_display(self): return f"button_{self._hub_id}_{self._btn_id}_display_module" @@ -112,17 +111,17 @@ def should_poll(self) -> bool: def device_info(self) -> DeviceInfo: """Return information to link this entity with the correct device.""" - identifiers: set[tuple[str, str]] = {} + identifiers: set[tuple[str, str]] = set() - match self._connector.connector_type_enum(): - case ConnectorEnum.BAR: + match self._connector.connector_type(): + case ConnectorType.BAR: identifiers = { ( DOMAIN, - f"{self._hub.hub_id} BAR Module {self._connector.connector_id}", + f"{self._hub.hub_id} BAR Module {self._connector.identifier()}", ) } - case ConnectorEnum.DISPLAY: + case ConnectorType.DISPLAY: identifiers = {(DOMAIN, f"{self._hub.hub_id} Display Module")} return DeviceInfo( diff --git a/custom_components/button_plus/button_plus_api/api_client.py b/custom_components/button_plus/button_plus_api/api_client.py index a3116c1..b0771ff 100644 --- a/custom_components/button_plus/button_plus_api/api_client.py +++ b/custom_components/button_plus/button_plus_api/api_client.py @@ -67,7 +67,7 @@ async def get_cookie_from_login(self, email=str, password=str): if not response.cookies: raise Exception( - f"Login error with username and password, response: {response_body}" + f'Login error with username and password, response: {response_body}' ) cookie_string = str(response.cookies) diff --git a/custom_components/button_plus/button_plus_api/connector_type.py b/custom_components/button_plus/button_plus_api/connector_type.py index a6a26f7..e8306ee 100644 --- a/custom_components/button_plus/button_plus_api/connector_type.py +++ b/custom_components/button_plus/button_plus_api/connector_type.py @@ -1,7 +1,7 @@ from enum import Enum -class ConnectorEnum(Enum): +class ConnectorType(Enum): NOT_CONNECTED = 0 BAR = 1 DISPLAY = 2 diff --git a/custom_components/button_plus/button_plus_api/model_detection.py b/custom_components/button_plus/button_plus_api/model_detection.py index 282ce48..4b43dc3 100644 --- a/custom_components/button_plus/button_plus_api/model_detection.py +++ b/custom_components/button_plus/button_plus_api/model_detection.py @@ -1,6 +1,6 @@ from typing import Dict, Any import json -from packaging.version import parse as parseSemver, Version as SemverVersion +from packaging.version import parse as parseSemver from .model_interface import DeviceConfiguration diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 0833d2e..b7c8b5e 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -1,3 +1,23 @@ +from typing import List + +from button_plus.button_plus_api.connector_type import ConnectorType + + +class Connector: + def identifier(self) -> int: + """ Return the identifier of the connector. """ + pass + + def connector_type(self) -> ConnectorType: + """ Return the connector type. """ + pass + + +class Button: + def button_id(self) -> int: + """ Return the identifier of the connector. """ + pass + class DeviceConfiguration: @staticmethod @@ -8,3 +28,39 @@ def from_json(json_data: str) -> "DeviceConfiguration": def to_json(self) -> str: """ Serialize the DeviceConfiguration to a JSON string. """ pass + + def firmware_version(self) -> str: + """ Return the firmware version of the device. """ + pass + + def name(self) -> str: + """ Return the name of the device. """ + pass + + def identifier(self) -> str: + """ Return the identifier of the device. """ + pass + + def ip_address(self) -> str: + """ Return the IP address of the device. """ + pass + + def mac_address(self) -> str: + """ Return the MAC address of the device. """ + pass + + def location(self) -> str: + """ Return the location description of the device. """ + pass + + def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: + """ Return the connectors of the given type. """ + pass + + def connector_for(self, *identifier: int) -> Connector: + """ Return the connectors of the given type. """ + pass + + def buttons(self) -> List[Button]: + """ Return the available buttons. """ + pass diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index 6de844c..482a850 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -1,20 +1,25 @@ import json from typing import List, Dict, Any -from .connector_type import ConnectorEnum +from .connector_type import ConnectorType from .event_type import EventType +from .model_interface import Button + class Connector: - def __init__(self, connector_id: int, connector_type: int): - self.connector_id = connector_id + def __init__(self, identifier: int, connector_type: int): + self.identifier = identifier self.connector_type = connector_type - def connector_type_enum(self) -> ConnectorEnum: - return ConnectorEnum(self.connector_type) - @staticmethod def from_dict(data: Dict[str, Any]) -> "Connector": - return Connector(connector_id=data["id"], connector_type=data["type"]) + return Connector(identifier=data["id"], connector_type=data["type"]) + + def identifier(self) -> int: + return self.identifier + + def connector_type(self) -> ConnectorType: + return ConnectorType(self.connector_type) class Sensor: @@ -29,14 +34,14 @@ def from_dict(data: Dict[str, Any]) -> "Sensor": class Info: def __init__( - self, - device_id: str, - mac: str, - ip_address: str, - firmware: str, - large_display: int, - connectors: List[Connector], - sensors: List[Sensor], + self, + device_id: str, + mac: str, + ip_address: str, + firmware: str, + large_display: int, + connectors: List[Connector], + sensors: List[Sensor], ): self.device_id = device_id self.mac = mac @@ -83,16 +88,16 @@ def from_dict(data: Dict[str, Any]) -> "Topic": 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, - topics: List[Topic], + 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, + topics: List[Topic], ): self.name = name self.location = location @@ -119,17 +124,17 @@ def from_dict(data: Dict[str, Any]) -> "Core": ) -class MqttButton: +class MqttButton(Button): def __init__( - self, - button_id: int, - label: str, - top_label: str, - led_color_front: int, - led_color_wall: int, - long_delay: int, - long_repeat: int, - topics: List[Topic], + self, + button_id: int, + label: str, + top_label: str, + led_color_front: int, + led_color_wall: int, + long_delay: int, + long_repeat: int, + topics: List[Topic], ): self.button_id = button_id self.label = label @@ -156,16 +161,16 @@ def from_dict(data: Dict[str, Any]) -> "MqttButton": class MqttDisplay: def __init__( - self, - x: int, - y: int, - font_size: int, - align: int, - width: int, - label: str, - unit: str, - round: int, - topics: List[Topic], + self, + x: int, + y: int, + font_size: int, + align: int, + width: int, + label: str, + unit: str, + round: int, + topics: List[Topic], ): self.x = x self.y = y @@ -194,13 +199,13 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": class MqttBroker: def __init__( - self, - broker_id: str, - url: str, - port: int, - ws_port: int, - username: str, - password: str, + self, + broker_id: str, + url: str, + port: int, + ws_port: int, + username: str, + password: str, ): self.broker_id = broker_id self.url = url @@ -238,13 +243,13 @@ def from_dict(data: Dict[str, Any]) -> "MqttSensor": class DeviceConfiguration: def __init__( - self, - info: Info, - core: Core, - mqtt_buttons: List[MqttButton], - mqtt_displays: List[MqttDisplay], - mqtt_brokers: List[MqttBroker], - mqtt_sensors: List[MqttSensor], + self, + info: Info, + core: Core, + mqtt_buttons: List[MqttButton], + mqtt_displays: List[MqttDisplay], + mqtt_brokers: List[MqttBroker], + mqtt_sensors: List[MqttSensor], ): self.info = info self.core = core @@ -298,7 +303,7 @@ def serialize(obj): d["largedisplay"] = d.pop("large_display") elif isinstance(obj, Connector): - d["id"] = d.pop("connector_id") + d["id"] = d.pop("identifier") d["type"] = d.pop("connector_type") elif isinstance(obj, Sensor): @@ -342,3 +347,32 @@ def serialize(obj): return str(obj) return json.dumps(self, default=serialize, indent=4) + + def firmware_version(self) -> str: + return self.info.firmware + + def name(self) -> str: + return self.core.name or self.info.device_id + + def identifier(self) -> str: + return self.info.device_id + + def ip_address(self) -> str: + return self.info.ip_address + + def mac_address(self) -> str: + return self.info.mac + + def location(self) -> str: + return self.core.location + + def connector_for(self, *identifier: int) -> Connector: + return next( + (connector for connector in self.info.connectors if connector.identifier == identifier), None + ) + + def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: + return [connector for connector in self.info.connectors if connector.connector_type in [connector_type]] + + def buttons(self) -> List[Button]: + return [button for button in self.mqtt_buttons] diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index c2682a7..a410052 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -1,33 +1,14 @@ import json from typing import List, Dict, Any -from .connector_type import ConnectorEnum -from .event_type import EventType +from .connector_type import ConnectorType +from .model_interface import Button +from .model_v1_07 import Connector, Sensor, Topic, MqttButton, MqttBroker, MqttSensor -import json -from typing import List, Dict, Any - -class Connector: - def __init__(self, connector_id: int, connector_type: int): - self.connector_id = connector_id - self.connector_type = connector_type - - @staticmethod - def from_dict(data: Dict[str, Any]) -> "Connector": - return Connector(connector_id=data["id"], connector_type=data["type"]) - -class Sensor: - def __init__(self, sensor_id: int, description: str): - self.sensor_id = sensor_id - self.description = description - - @staticmethod - def from_dict(data: Dict[str, Any]) -> "Sensor": - return Sensor(sensor_id=data["sensorid"], description=data["description"]) - class Info: - def __init__(self, device_id: str, mac: str, ip_address: str, firmware: str, large_display: int, connectors: List[Connector], sensors: List[Sensor]): + def __init__(self, device_id: str, mac: str, ip_address: str, firmware: str, large_display: int, + connectors: List[Connector], sensors: List[Sensor]): self.device_id = device_id self.mac = mac self.ip_address = ip_address @@ -48,24 +29,10 @@ def from_dict(data: Dict[str, Any]) -> "Info": sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]] ) -class Topic: - def __init__(self, broker_id: str, topic: str, payload: str, event_type: int): - self.broker_id = broker_id - self.topic = topic - self.payload = payload - self.event_type = event_type - - @staticmethod - def from_dict(data: Dict[str, Any]) -> "Topic": - return Topic( - broker_id=data["brokerid"], - topic=data["topic"], - payload=data["payload"], - event_type=data["eventtype"] - ) class Core: - def __init__(self, name: str, location: str, auto_backup: bool, brightness: int, color: int, statusbar: int, topics: List[Topic]): + def __init__(self, name: str, location: str, auto_backup: bool, brightness: int, color: int, statusbar: int, + topics: List[Topic]): self.name = name self.location = location self.auto_backup = auto_backup @@ -86,32 +53,10 @@ def from_dict(data: Dict[str, Any]) -> "Core": topics=[Topic.from_dict(topic) for topic in data["topics"]] ) -class MqttButton: - def __init__(self, button_id: int, label: str, top_label: str, led_color_front: int, led_color_wall: int, long_delay: int, long_repeat: int, topics: List[Topic]): - self.button_id = button_id - self.label = label - self.top_label = top_label - self.led_color_front = led_color_front - self.led_color_wall = led_color_wall - self.long_delay = long_delay - self.long_repeat = long_repeat - self.topics = topics - - @staticmethod - def from_dict(data: Dict[str, Any]) -> "MqttButton": - return MqttButton( - button_id=data["id"], - label=data["label"], - top_label=data["toplabel"], - led_color_front=data["ledcolorfront"], - led_color_wall=data["ledcolorwall"], - long_delay=data["longdelay"], - long_repeat=data["longrepeat"], - topics=[Topic.from_dict(topic) for topic in data["topics"]] - ) class MqttDisplay: - def __init__(self, align: int, x: int, y: int, box_type: int, font_size: int, page: int, label: str, width: int, unit: str, round: int, topics: List[Topic]): + def __init__(self, align: int, x: int, y: int, box_type: int, font_size: int, page: int, label: str, width: int, + unit: str, round: int, topics: List[Topic]): self.x = x self.y = y self.box_type = box_type @@ -140,42 +85,10 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": topics=[Topic.from_dict(topic) for topic in data["topics"]] ) -class MqttBroker: - def __init__(self, broker_id: str, url: str, port: int, ws_port: int, username: str, password: str): - self.broker_id = broker_id - self.url = url - self.port = port - self.ws_port = ws_port - self.username = username - self.password = password - - @staticmethod - def from_dict(data: Dict[str, Any]) -> "MqttBroker": - return MqttBroker( - broker_id=data["brokerid"], - url=data["url"], - port=data["port"], - ws_port=data["wsport"], - username=data["username"], - password=data["password"] - ) - -class MqttSensor: - def __init__(self, sensor_id: int, interval: int, topic: Topic): - self.sensor_id = sensor_id - self.interval = interval - self.topic = topic - - @staticmethod - def from_dict(data: Dict[str, Any]) -> "MqttSensor": - return MqttSensor( - sensor_id=data["sensorid"], - interval=data["interval"], - topic=Topic.from_dict(data["topic"]) - ) class DeviceConfiguration: - def __init__(self, info: Info, core: Core, mqtt_buttons: List[MqttButton], mqtt_displays: List[MqttDisplay], mqtt_brokers: List[MqttBroker], mqtt_sensors: List[MqttSensor]): + def __init__(self, info: Info, core: Core, mqtt_buttons: List[MqttButton], mqtt_displays: List[MqttDisplay], + mqtt_brokers: List[MqttBroker], mqtt_sensors: List[MqttSensor]): self.info = info self.core = core self.mqtt_buttons = mqtt_buttons @@ -217,7 +130,7 @@ def serialize(obj): d["sensors"] = [serialize(sensor) for sensor in d.pop("sensors")] elif isinstance(obj, Connector): - d["id"] = d.pop("connector_id") + d["id"] = d.pop("identifier") d["type"] = d.pop("connector_type") elif isinstance(obj, Sensor): @@ -281,3 +194,31 @@ def serialize(obj): return json.dumps(self, default=serialize) + def firmware_version(self) -> str: + return self.info.firmware + + def name(self) -> str: + return self.core.name or self.info.device_id + + def identifier(self) -> str: + return self.info.device_id + + def ip_address(self) -> str: + return self.info.ip_address + + def mac_address(self) -> str: + return self.info.mac + + def location(self) -> str: + return self.core.location + + def connector_for(self, *identifier: int) -> Connector: + return next( + (connector for connector in self.info.connectors if connector.identifier == identifier), None + ) + + def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: + return [connector for connector in self.info.connectors if connector.connector_type in [connector_type]] + + def buttons(self) -> List[Button]: + return [button for button in self.mqtt_buttons] diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 41041a9..4e972b4 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -3,18 +3,18 @@ from __future__ import annotations import logging +from typing import List from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers import device_registry as dr from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import device_registry as dr +from .button_plus_api.connector_type import ConnectorType from .button_plus_api.local_api_client import LocalApiClient -from .button_plus_api.connector_type import ConnectorEnum from .button_plus_api.model_interface import DeviceConfiguration from .const import DOMAIN, MANUFACTURER - _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -22,15 +22,15 @@ class ButtonPlusHub: """hub for Button+.""" def __init__( - self, hass: HomeAssistant, config: DeviceConfiguration, entry: ConfigEntry + self, hass: HomeAssistant, config: DeviceConfiguration, entry: ConfigEntry ) -> None: - _LOGGER.debug(f"New hub with config {config.core}") + _LOGGER.debug(f"New hub with config {config}") self._hass = hass self.config = config - self._name = config.core.name or config.info.device_id - self._id = config.info.device_id + self._name = config.name() + self.identifier = config.identifier() self._client = LocalApiClient( - config.info.ip_address, aiohttp_client.async_get_clientsession(hass) + config.ip_address(), aiohttp_client.async_get_clientsession(hass) ) self.online = True self.button_entities = {} @@ -41,55 +41,53 @@ def __init__( device_registry = dr.async_get(hass) self.device = device_registry.async_get_or_create( - configuration_url=f"http://{self.config.info.ip_address}/", + configuration_url=f"http://{self.config.ip_address()}/", config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, self.config.info.mac)}, - identifiers={(DOMAIN, self.config.info.device_id)}, + connections={(dr.CONNECTION_NETWORK_MAC, self.config.mac_address())}, + identifiers={(DOMAIN, self.config.identifier())}, manufacturer=MANUFACTURER, - suggested_area=self.config.core.location, + suggested_area=self.config.location(), name=self._name, model="Base Module", - sw_version=config.info.firmware, + sw_version=config.firmware_version(), ) # 1 or none display module self.display_module = next( ( self.create_display_module(hass, entry, self) - for _ in self.connector(ConnectorEnum.DISPLAY) + for _ in self.connector_identifiers_for(ConnectorType.DISPLAY) ), None, ) self.display_bar = [ (connector_id, self.create_bar_module(hass, entry, self, connector_id)) - for connector_id in self.connector(ConnectorEnum.BAR) + for connector_id in self.connector_identifiers_for(ConnectorType.BAR) ] - def create_display_module( - self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub - ) -> None: + @staticmethod + def create_display_module(hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub) -> None: _LOGGER.debug(f"Add display module from '{hub.hub_id}'") device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - # connections={(DOMAIN, hub.config.info.device_id)}, name=f"{hub.name} Display Module", model="Display Module", manufacturer=MANUFACTURER, - suggested_area=hub.config.core.location, + suggested_area=hub.config.location(), identifiers={(DOMAIN, f"{hub.hub_id} Display Module")}, via_device=(DOMAIN, hub.hub_id), ) return device + @staticmethod def create_bar_module( - self, - hass: HomeAssistant, - entry: ConfigEntry, - hub: ButtonPlusHub, - connector_id: int, + hass: HomeAssistant, + entry: ConfigEntry, + hub: ButtonPlusHub, + connector_id: int, ) -> None: _LOGGER.debug( f"Add bar module from '{hub.hub_id}' with connector '{connector_id}'" @@ -98,22 +96,20 @@ def create_bar_module( device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - # connections={(DOMAIN, hub.config.info.device_id)}, name=f"{hub._name} BAR Module {connector_id}", model="Bar module", manufacturer=MANUFACTURER, - suggested_area=hub.config.core.location, - identifiers={(DOMAIN, f"{hub.hub_id} BAR Module {connector_id}")}, + suggested_area=hub.config.location(), + identifiers={(DOMAIN, f"{hub.identifier} BAR Module {connector_id}")}, via_device=(DOMAIN, hub.hub_id), ) return device - def connector(self, connector_type: ConnectorEnum): + def connector_identifiers_for(self, connector_type: ConnectorType) -> List[int]: return [ - connector.connector_id - for connector in self.config.info.connectors - if connector.connector_type_enum() in [connector_type] + connector.identifier() + for connector in self.config.connectors_for(connector_type) ] @property @@ -123,7 +119,7 @@ def client(self) -> LocalApiClient: @property def hub_id(self) -> str: - return self._id + return self.identifier @property def name(self) -> str: diff --git a/custom_components/button_plus/coordinator.py b/custom_components/button_plus/coordinator.py index 9b438a3..d0a8a67 100644 --- a/custom_components/button_plus/coordinator.py +++ b/custom_components/button_plus/coordinator.py @@ -2,15 +2,14 @@ import re from homeassistant.components.button import ButtonEntity -from homeassistant.core import HomeAssistant, callback +from homeassistant.components.mqtt import client as mqtt, ReceiveMessage from homeassistant.components.number import NumberEntity +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.components.mqtt import client as mqtt, ReceiveMessage from .buttonplushub import ButtonPlusHub from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/button_plus/device.py b/custom_components/button_plus/device.py index a8e3b93..3e0609d 100644 --- a/custom_components/button_plus/device.py +++ b/custom_components/button_plus/device.py @@ -5,45 +5,44 @@ from homeassistant.helpers import device_registry as dr from .buttonplushub import ButtonPlusHub - from .const import DOMAIN, MANUFACTURER class BarModuleDevice: def __init__( - self, - hass: HomeAssistant, - entry: ConfigEntry, - hub: ButtonPlusHub, - connector_id: int, + self, + hass: HomeAssistant, + entry: ConfigEntry, + hub: ButtonPlusHub, + connector_id: int, ) -> None: self.device_registry = dr.async_get(hass) self.device = self.device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(DOMAIN, hub.config.info.device_id)}, - name=f"{hub._name} BAR Module {connector_id}", + connections={(DOMAIN, hub.config.identifier())}, + name=f"{hub.name} BAR Module {connector_id}", model="Bar module", manufacturer=MANUFACTURER, - suggested_area=hub.config.core.location, - identifiers={(DOMAIN, f"{hub._hub_id} BAR Module {connector_id}")}, + suggested_area=hub.config.location(), + identifiers={(DOMAIN, f"{hub.hub_id} BAR Module {connector_id}")}, via_device=(DOMAIN, hub.hub_id), ) class DisplayModuleDevice: def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub + self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub ) -> None: self.device_registry = dr.async_get(hass) self.device = self.device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(DOMAIN, hub.config.info.device_id)}, - name=f"{hub._name} Display Module", + connections={(DOMAIN, hub.config.identifier())}, + name=f"{hub.name} Display Module", model="Display Module", manufacturer=MANUFACTURER, - suggested_area=hub.config.core.location, - identifiers={(DOMAIN, f"{hub._hub_id} Display Module")}, + suggested_area=hub.config.location(), + identifiers={(DOMAIN, f"{hub.hub_id} Display Module")}, via_device=(DOMAIN, hub.hub_id), ) From 41e7097197492fd7346553aebfdd95b051dc2dba Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:27:03 +0200 Subject: [PATCH 06/29] Ruff fix and format --- custom_components/button_plus/__init__.py | 4 +- custom_components/button_plus/button.py | 4 +- .../button_plus/button_plus_api/api_client.py | 2 +- .../button_plus_api/model_detection.py | 3 +- .../button_plus_api/model_interface.py | 28 ++--- .../button_plus_api/model_v1_07.py | 115 ++++++++++-------- .../button_plus_api/model_v1_12.py | 110 +++++++++++++---- .../button_plus/buttonplushub.py | 14 ++- custom_components/button_plus/config_flow.py | 14 ++- custom_components/button_plus/coordinator.py | 16 ++- custom_components/button_plus/device.py | 12 +- .../button_plus_api/test_model_detection.py | 13 +- .../button_plus_api/test_model_v1_07.py | 50 ++++---- .../button_plus_api/test_model_v1_12.py | 10 +- 14 files changed, 241 insertions(+), 154 deletions(-) diff --git a/custom_components/button_plus/__init__.py b/custom_components/button_plus/__init__.py index 2117c8b..23734c2 100644 --- a/custom_components/button_plus/__init__.py +++ b/custom_components/button_plus/__init__.py @@ -7,7 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from custom_components.button_plus.button_plus_api.model_interface import DeviceConfiguration +from custom_components.button_plus.button_plus_api.model_interface import ( + DeviceConfiguration, +) from custom_components.button_plus.button_plus_api.model_detection import ModelDetection from custom_components.button_plus.buttonplushub import ButtonPlusHub from custom_components.button_plus.const import DOMAIN diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index 765791f..1c5ea75 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -39,7 +39,9 @@ async def async_setup_entry( active_connectors = [ connector.identifier() - for connector in hub.config.connectors_for(ConnectorType.BAR, ConnectorType.DISPLAY) + for connector in hub.config.connectors_for( + ConnectorType.BAR, ConnectorType.DISPLAY + ) ] buttons = filter( diff --git a/custom_components/button_plus/button_plus_api/api_client.py b/custom_components/button_plus/button_plus_api/api_client.py index b0771ff..a3116c1 100644 --- a/custom_components/button_plus/button_plus_api/api_client.py +++ b/custom_components/button_plus/button_plus_api/api_client.py @@ -67,7 +67,7 @@ async def get_cookie_from_login(self, email=str, password=str): if not response.cookies: raise Exception( - f'Login error with username and password, response: {response_body}' + f"Login error with username and password, response: {response_body}" ) cookie_string = str(response.cookies) diff --git a/custom_components/button_plus/button_plus_api/model_detection.py b/custom_components/button_plus/button_plus_api/model_detection.py index 4b43dc3..445f51b 100644 --- a/custom_components/button_plus/button_plus_api/model_detection.py +++ b/custom_components/button_plus/button_plus_api/model_detection.py @@ -1,4 +1,3 @@ -from typing import Dict, Any import json from packaging.version import parse as parseSemver from .model_interface import DeviceConfiguration @@ -12,7 +11,9 @@ def model_for_json(json_data: str) -> "DeviceConfiguration": if device_version >= parseSemver("1.12.0"): from .model_v1_12 import DeviceConfiguration + return DeviceConfiguration.from_json(data) else: from .model_v1_07 import DeviceConfiguration + return DeviceConfiguration.from_json(data) diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index b7c8b5e..019ceba 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -5,62 +5,62 @@ class Connector: def identifier(self) -> int: - """ Return the identifier of the connector. """ + """Return the identifier of the connector.""" pass def connector_type(self) -> ConnectorType: - """ Return the connector type. """ + """Return the connector type.""" pass class Button: def button_id(self) -> int: - """ Return the identifier of the connector. """ + """Return the identifier of the connector.""" pass class DeviceConfiguration: @staticmethod def from_json(json_data: str) -> "DeviceConfiguration": - """ Deserialize the DeviceConfiguration from a JSON string. """ + """Deserialize the DeviceConfiguration from a JSON string.""" pass def to_json(self) -> str: - """ Serialize the DeviceConfiguration to a JSON string. """ + """Serialize the DeviceConfiguration to a JSON string.""" pass def firmware_version(self) -> str: - """ Return the firmware version of the device. """ + """Return the firmware version of the device.""" pass def name(self) -> str: - """ Return the name of the device. """ + """Return the name of the device.""" pass def identifier(self) -> str: - """ Return the identifier of the device. """ + """Return the identifier of the device.""" pass def ip_address(self) -> str: - """ Return the IP address of the device. """ + """Return the IP address of the device.""" pass def mac_address(self) -> str: - """ Return the MAC address of the device. """ + """Return the MAC address of the device.""" pass def location(self) -> str: - """ Return the location description of the device. """ + """Return the location description of the device.""" pass def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: - """ Return the connectors of the given type. """ + """Return the connectors of the given type.""" pass def connector_for(self, *identifier: int) -> Connector: - """ Return the connectors of the given type. """ + """Return the connectors of the given type.""" pass def buttons(self) -> List[Button]: - """ Return the available buttons. """ + """Return the available buttons.""" pass diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index 482a850..3e4ceb8 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -34,14 +34,14 @@ def from_dict(data: Dict[str, Any]) -> "Sensor": class Info: def __init__( - self, - device_id: str, - mac: str, - ip_address: str, - firmware: str, - large_display: int, - connectors: List[Connector], - sensors: List[Sensor], + self, + device_id: str, + mac: str, + ip_address: str, + firmware: str, + large_display: int, + connectors: List[Connector], + sensors: List[Sensor], ): self.device_id = device_id self.mac = mac @@ -88,16 +88,16 @@ def from_dict(data: Dict[str, Any]) -> "Topic": 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, - topics: List[Topic], + 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, + topics: List[Topic], ): self.name = name self.location = location @@ -126,15 +126,15 @@ def from_dict(data: Dict[str, Any]) -> "Core": class MqttButton(Button): def __init__( - self, - button_id: int, - label: str, - top_label: str, - led_color_front: int, - led_color_wall: int, - long_delay: int, - long_repeat: int, - topics: List[Topic], + self, + button_id: int, + label: str, + top_label: str, + led_color_front: int, + led_color_wall: int, + long_delay: int, + long_repeat: int, + topics: List[Topic], ): self.button_id = button_id self.label = label @@ -161,16 +161,16 @@ def from_dict(data: Dict[str, Any]) -> "MqttButton": class MqttDisplay: def __init__( - self, - x: int, - y: int, - font_size: int, - align: int, - width: int, - label: str, - unit: str, - round: int, - topics: List[Topic], + self, + x: int, + y: int, + font_size: int, + align: int, + width: int, + label: str, + unit: str, + round: int, + topics: List[Topic], ): self.x = x self.y = y @@ -199,13 +199,13 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": class MqttBroker: def __init__( - self, - broker_id: str, - url: str, - port: int, - ws_port: int, - username: str, - password: str, + self, + broker_id: str, + url: str, + port: int, + ws_port: int, + username: str, + password: str, ): self.broker_id = broker_id self.url = url @@ -243,13 +243,13 @@ def from_dict(data: Dict[str, Any]) -> "MqttSensor": class DeviceConfiguration: def __init__( - self, - info: Info, - core: Core, - mqtt_buttons: List[MqttButton], - mqtt_displays: List[MqttDisplay], - mqtt_brokers: List[MqttBroker], - mqtt_sensors: List[MqttSensor], + self, + info: Info, + core: Core, + mqtt_buttons: List[MqttButton], + mqtt_displays: List[MqttDisplay], + mqtt_brokers: List[MqttBroker], + mqtt_sensors: List[MqttSensor], ): self.info = info self.core = core @@ -368,11 +368,20 @@ def location(self) -> str: def connector_for(self, *identifier: int) -> Connector: return next( - (connector for connector in self.info.connectors if connector.identifier == identifier), None + ( + connector + for connector in self.info.connectors + if connector.identifier == identifier + ), + None, ) def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: - return [connector for connector in self.info.connectors if connector.connector_type in [connector_type]] + return [ + connector + for connector in self.info.connectors + if connector.connector_type in [connector_type] + ] def buttons(self) -> List[Button]: return [button for button in self.mqtt_buttons] diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index a410052..f660644 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -7,8 +7,16 @@ class Info: - def __init__(self, device_id: str, mac: str, ip_address: str, firmware: str, large_display: int, - connectors: List[Connector], sensors: List[Sensor]): + def __init__( + self, + device_id: str, + mac: str, + ip_address: str, + firmware: str, + large_display: int, + connectors: List[Connector], + sensors: List[Sensor], + ): self.device_id = device_id self.mac = mac self.ip_address = ip_address @@ -25,14 +33,24 @@ def from_dict(data: Dict[str, Any]) -> "Info": ip_address=data["ipaddress"], firmware=data["firmware"], large_display=data["largedisplay"], - connectors=[Connector.from_dict(connector) for connector in data["connectors"]], - sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]] + connectors=[ + Connector.from_dict(connector) for connector in data["connectors"] + ], + sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]], ) class Core: - def __init__(self, name: str, location: str, auto_backup: bool, brightness: int, color: int, statusbar: int, - topics: List[Topic]): + def __init__( + self, + name: str, + location: str, + auto_backup: bool, + brightness: int, + color: int, + statusbar: int, + topics: List[Topic], + ): self.name = name self.location = location self.auto_backup = auto_backup @@ -50,13 +68,25 @@ def from_dict(data: Dict[str, Any]) -> "Core": brightness=data["brightness"], color=data["color"], statusbar=data["statusbar"], - topics=[Topic.from_dict(topic) for topic in data["topics"]] + topics=[Topic.from_dict(topic) for topic in data["topics"]], ) class MqttDisplay: - def __init__(self, align: int, x: int, y: int, box_type: int, font_size: int, page: int, label: str, width: int, - unit: str, round: int, topics: List[Topic]): + def __init__( + self, + align: int, + x: int, + y: int, + box_type: int, + font_size: int, + page: int, + label: str, + width: int, + unit: str, + round: int, + topics: List[Topic], + ): self.x = x self.y = y self.box_type = box_type @@ -82,13 +112,20 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": unit=data["unit"], round=data["round"], page=data["page"], - topics=[Topic.from_dict(topic) for topic in data["topics"]] + topics=[Topic.from_dict(topic) for topic in data["topics"]], ) class DeviceConfiguration: - def __init__(self, info: Info, core: Core, mqtt_buttons: List[MqttButton], mqtt_displays: List[MqttDisplay], - mqtt_brokers: List[MqttBroker], mqtt_sensors: List[MqttSensor]): + def __init__( + self, + info: Info, + core: Core, + mqtt_buttons: List[MqttButton], + mqtt_displays: List[MqttDisplay], + mqtt_brokers: List[MqttBroker], + mqtt_sensors: List[MqttSensor], + ): self.info = info self.core = core self.mqtt_buttons = mqtt_buttons @@ -101,10 +138,18 @@ def from_json(json_data: any) -> "DeviceConfiguration": return DeviceConfiguration( info=Info.from_dict(json_data["info"]), core=Core.from_dict(json_data["core"]), - mqtt_buttons=[MqttButton.from_dict(button) for button in json_data["mqttbuttons"]], - mqtt_displays=[MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"]], - mqtt_brokers=[MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"]], - mqtt_sensors=[MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"]] + mqtt_buttons=[ + MqttButton.from_dict(button) for button in json_data["mqttbuttons"] + ], + mqtt_displays=[ + MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"] + ], + mqtt_brokers=[ + MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"] + ], + mqtt_sensors=[ + MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] + ], ) def to_json(self) -> str: @@ -115,10 +160,18 @@ def serialize(obj): if isinstance(obj, DeviceConfiguration): d["info"] = serialize(d.pop("info")) d["core"] = serialize(d.pop("core")) - d["mqttbuttons"] = [serialize(button) for button in d.pop("mqtt_buttons")] - d["mqttdisplays"] = [serialize(display) for display in d.pop("mqtt_displays")] - d["mqttbrokers"] = [serialize(broker) for broker in d.pop("mqtt_brokers")] - d["mqttsensors"] = [serialize(sensor) for sensor in d.pop("mqtt_sensors")] + d["mqttbuttons"] = [ + serialize(button) for button in d.pop("mqtt_buttons") + ] + d["mqttdisplays"] = [ + serialize(display) for display in d.pop("mqtt_displays") + ] + d["mqttbrokers"] = [ + serialize(broker) for broker in d.pop("mqtt_brokers") + ] + d["mqttsensors"] = [ + serialize(sensor) for sensor in d.pop("mqtt_sensors") + ] if isinstance(obj, Info): d["id"] = d.pop("device_id") @@ -126,7 +179,9 @@ def serialize(obj): d["ipaddress"] = d.pop("ip_address") d["firmware"] = d.pop("firmware") d["largedisplay"] = d.pop("large_display") - d["connectors"] = [serialize(connector) for connector in d.pop("connectors")] + d["connectors"] = [ + serialize(connector) for connector in d.pop("connectors") + ] d["sensors"] = [serialize(sensor) for sensor in d.pop("sensors")] elif isinstance(obj, Connector): @@ -214,11 +269,20 @@ def location(self) -> str: def connector_for(self, *identifier: int) -> Connector: return next( - (connector for connector in self.info.connectors if connector.identifier == identifier), None + ( + connector + for connector in self.info.connectors + if connector.identifier == identifier + ), + None, ) def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: - return [connector for connector in self.info.connectors if connector.connector_type in [connector_type]] + return [ + connector + for connector in self.info.connectors + if connector.connector_type in [connector_type] + ] def buttons(self) -> List[Button]: return [button for button in self.mqtt_buttons] diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 4e972b4..fafd2ad 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -22,7 +22,7 @@ class ButtonPlusHub: """hub for Button+.""" def __init__( - self, hass: HomeAssistant, config: DeviceConfiguration, entry: ConfigEntry + self, hass: HomeAssistant, config: DeviceConfiguration, entry: ConfigEntry ) -> None: _LOGGER.debug(f"New hub with config {config}") self._hass = hass @@ -66,7 +66,9 @@ def __init__( ] @staticmethod - def create_display_module(hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub) -> None: + def create_display_module( + hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub + ) -> None: _LOGGER.debug(f"Add display module from '{hub.hub_id}'") device_registry = dr.async_get(hass) @@ -84,10 +86,10 @@ def create_display_module(hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPl @staticmethod def create_bar_module( - hass: HomeAssistant, - entry: ConfigEntry, - hub: ButtonPlusHub, - connector_id: int, + hass: HomeAssistant, + entry: ConfigEntry, + hub: ButtonPlusHub, + connector_id: int, ) -> None: _LOGGER.debug( f"Add bar module from '{hub.hub_id}' with connector '{connector_id}'" diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index ccdc464..44e9635 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -343,12 +343,14 @@ def add_topics_to_buttons( ) # Create topics for button click - button.topics.append({ - "brokerid": "ha-button-plus", - "topic": f"buttonplus/{device_id}/button/{button.button_id}/long_press", - "payload": "press", - "eventtype": EventType.LONG_PRESS - }) + button.topics.append( + { + "brokerid": "ha-button-plus", + "topic": f"buttonplus/{device_id}/button/{button.button_id}/long_press", + "payload": "press", + "eventtype": EventType.LONG_PRESS, + } + ) return device_config diff --git a/custom_components/button_plus/coordinator.py b/custom_components/button_plus/coordinator.py index d0a8a67..7a62dbc 100644 --- a/custom_components/button_plus/coordinator.py +++ b/custom_components/button_plus/coordinator.py @@ -30,7 +30,10 @@ def __init__(self, hass: HomeAssistant, hub: ButtonPlusHub): self._mqtt_subscribed_buttons = False self._mqtt_topics = [ (f"buttonplus/{hub.hub_id}/button/+/click", self.mqtt_button_callback), - (f"buttonplus/{hub.hub_id}/button/+/long_press", self.mqtt_button_long_press_callback), + ( + f"buttonplus/{hub.hub_id}/button/+/long_press", + self.mqtt_button_long_press_callback, + ), (f"buttonplus/{hub.hub_id}/brightness/+", self.mqtt_brightness_callback), (f"buttonplus/{hub.hub_id}/page/+", self.mqtt_page_callback), ] @@ -41,10 +44,7 @@ async def _async_update_data(self): if not self._mqtt_subscribed_buttons: for topic, cb in self._mqtt_topics: self.unsubscribe_mqtt = await mqtt.async_subscribe( - self._hass, - topic, - cb, - 0 + self._hass, topic, cb, 0 ) _LOGGER.debug(f"MQTT subscribed to {topic}") @@ -88,13 +88,11 @@ async def mqtt_button_callback(self, message: ReceiveMessage): async def mqtt_button_long_press_callback(self, message: ReceiveMessage): # Handle the message here _LOGGER.debug(f"Received message on topic {message.topic}: {message.payload}") - match = re.search(r'/(\d+)/long_press', message.topic) + match = re.search(r"/(\d+)/long_press", message.topic) btn_id = int(match.group(1)) if match else None entity: ButtonEntity = self.hub.button_entities[str(btn_id)] await self.hass.services.async_call( - DOMAIN, - 'long_press', - target={"entity_id": entity.entity_id} + DOMAIN, "long_press", target={"entity_id": entity.entity_id} ) diff --git a/custom_components/button_plus/device.py b/custom_components/button_plus/device.py index 3e0609d..8b27e7f 100644 --- a/custom_components/button_plus/device.py +++ b/custom_components/button_plus/device.py @@ -10,11 +10,11 @@ class BarModuleDevice: def __init__( - self, - hass: HomeAssistant, - entry: ConfigEntry, - hub: ButtonPlusHub, - connector_id: int, + self, + hass: HomeAssistant, + entry: ConfigEntry, + hub: ButtonPlusHub, + connector_id: int, ) -> None: self.device_registry = dr.async_get(hass) @@ -32,7 +32,7 @@ def __init__( class DisplayModuleDevice: def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub + self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub ) -> None: self.device_registry = dr.async_get(hass) diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_detection.py b/tests/custom_components/button_plus/button_plus_api/test_model_detection.py index a14e579..0ba8178 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_detection.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_detection.py @@ -1,19 +1,22 @@ import pytest -import json from custom_components.button_plus.button_plus_api.model_detection import ModelDetection -from custom_components.button_plus.button_plus_api.model_v1_07 import DeviceConfiguration as DeviceConfiguration_v1_07 -from custom_components.button_plus.button_plus_api.model_v1_12 import DeviceConfiguration as DeviceConfiguration_v1_12 +from custom_components.button_plus.button_plus_api.model_v1_07 import ( + DeviceConfiguration as DeviceConfiguration_v1_07, +) +from custom_components.button_plus.button_plus_api.model_v1_12 import ( + DeviceConfiguration as DeviceConfiguration_v1_12, +) @pytest.fixture def json_data_v1_07(): - with open('resource/physicalconfig1.07.json') as file: + with open("resource/physicalconfig1.07.json") as file: return file.read() @pytest.fixture def json_data_v1_12(): - with open('resource/physicalconfig1.12.1.json') as file: + with open("resource/physicalconfig1.12.1.json") as file: return file.read() diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py index aa3cdf2..d8dcd08 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py @@ -1,13 +1,15 @@ import pytest import json -from custom_components.button_plus.button_plus_api.model_v1_07 import DeviceConfiguration +from custom_components.button_plus.button_plus_api.model_v1_07 import ( + DeviceConfiguration, +) @pytest.fixture def device_config(): # Load and parse the JSON file - with open('resource/physicalconfig1.07.json') as file: + with open("resource/physicalconfig1.07.json") as file: json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object return DeviceConfiguration.from_json(json_data) @@ -16,15 +18,15 @@ def device_config(): def test_buttons(device_config): buttons = device_config.mqtt_buttons assert len(buttons) == 8 - assert buttons[0].label == 'Btn 0' - assert buttons[0].top_label == 'Label' + assert buttons[0].label == "Btn 0" + assert buttons[0].top_label == "Label" assert buttons[0].led_color_front == 0 assert buttons[0].led_color_wall == 0 assert buttons[0].long_delay == 75 assert buttons[0].long_repeat == 15 - assert buttons[2].topics[0].broker_id == 'hassdev' - assert buttons[2].topics[0].topic == 'buttonplus/btn_4967c8/bars/2/click' - assert buttons[2].topics[0].payload == 'true' + assert buttons[2].topics[0].broker_id == "hassdev" + assert buttons[2].topics[0].topic == "buttonplus/btn_4967c8/bars/2/click" + assert buttons[2].topics[0].payload == "true" assert buttons[2].topics[0].event_type == 0 @@ -37,30 +39,30 @@ def test_mqttdisplays(device_config): assert mqttdisplays[0].align == 0 assert mqttdisplays[0].width == 50 assert mqttdisplays[0].round == 0 - assert mqttdisplays[0].label == 'Amsterdam' - assert mqttdisplays[0].unit == '' - assert mqttdisplays[0].topics[0].broker_id == 'buttonplus' - assert mqttdisplays[0].topics[0].topic == 'system/datetime/amsterdam' - assert mqttdisplays[0].topics[0].payload == '' + assert mqttdisplays[0].label == "Amsterdam" + assert mqttdisplays[0].unit == "" + assert mqttdisplays[0].topics[0].broker_id == "buttonplus" + assert mqttdisplays[0].topics[0].topic == "system/datetime/amsterdam" + assert mqttdisplays[0].topics[0].payload == "" assert mqttdisplays[0].topics[0].event_type == 15 - assert mqttdisplays[1].unit == '°C' + assert mqttdisplays[1].unit == "°C" def test_mqttbrokers(device_config): mqttbrokers = device_config.mqtt_brokers assert len(mqttbrokers) == 2 - assert mqttbrokers[0].broker_id == 'buttonplus' - assert mqttbrokers[0].url == 'mqtt://mqtt.button.plus' + assert mqttbrokers[0].broker_id == "buttonplus" + assert mqttbrokers[0].url == "mqtt://mqtt.button.plus" assert mqttbrokers[0].port == 0 assert mqttbrokers[0].ws_port == 0 - assert mqttbrokers[0].username == '' - assert mqttbrokers[0].password == '' - assert mqttbrokers[1].broker_id == 'hassdev' - assert mqttbrokers[1].url == 'mqtt://192.168.2.16/' + assert mqttbrokers[0].username == "" + assert mqttbrokers[0].password == "" + assert mqttbrokers[1].broker_id == "hassdev" + assert mqttbrokers[1].url == "mqtt://192.168.2.16/" assert mqttbrokers[1].port == 1883 assert mqttbrokers[1].ws_port == 9001 - assert mqttbrokers[1].username == 'koen' - assert mqttbrokers[1].password == 'koen' + assert mqttbrokers[1].username == "koen" + assert mqttbrokers[1].password == "koen" def test_mqttsensors(device_config): @@ -68,7 +70,7 @@ def test_mqttsensors(device_config): assert len(mqttsensors) == 1 assert mqttsensors[0].sensor_id == 1 assert mqttsensors[0].interval == 10 - assert mqttsensors[0].topic.broker_id == 'buttonplus' - assert mqttsensors[0].topic.topic == 'button/btn_4967c8/temperature' - assert mqttsensors[0].topic.payload == '' + assert mqttsensors[0].topic.broker_id == "buttonplus" + assert mqttsensors[0].topic.topic == "button/btn_4967c8/temperature" + assert mqttsensors[0].topic.payload == "" assert mqttsensors[0].topic.event_type == 18 diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py index 718a7df..a5ab8a3 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py @@ -1,10 +1,12 @@ -from custom_components.button_plus.button_plus_api.model_v1_12 import DeviceConfiguration +from custom_components.button_plus.button_plus_api.model_v1_12 import ( + DeviceConfiguration, +) import json def test_model_v1_12_from_to_json_should_be_same(): # Load the JSON file - with open('resource/physicalconfig1.12.1.json') as file: + with open("resource/physicalconfig1.12.1.json") as file: json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object @@ -22,7 +24,7 @@ def test_model_v1_12_from_to_json_should_be_same(): def test_model_v1_12(): # Load the JSON file - with open('resource/physicalconfig1.12.1.json') as file: + with open("resource/physicalconfig1.12.1.json") as file: json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object @@ -38,7 +40,7 @@ def test_model_v1_12(): # Assert the values from the parsed Core object assert device_config.core.name == "btn_4584b8" assert device_config.core.location == "Room 1" - assert device_config.core.auto_backup == True + assert device_config.core.auto_backup == "true" assert device_config.core.brightness == 80 assert device_config.core.color == 16765791 assert device_config.core.statusbar == 2 From c711fea8aa9939ec445585b3ab2fb42e5fb41c3c Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:37:51 +0200 Subject: [PATCH 07/29] Some fixes and cleanup before stopping for today --- custom_components/button_plus/button.py | 2 +- .../button_plus/button_plus_api/model_interface.py | 2 +- .../button_plus/button_plus_api/test_model_v1_12.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index 1c5ea75..22c03e2 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -85,7 +85,7 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub): self._name = f"Button {btn_id}" self._device_class = ButtonDeviceClass.IDENTIFY self._connector: Connector = hub.config.connector_for(btn_id // 2) - self.our_id = self.unique_id_gen() + self._attr_unique_id = self.unique_id_gen() def unique_id_gen(self): match self._connector.connector_type(): diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 019ceba..ea4678a 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -1,6 +1,6 @@ from typing import List -from button_plus.button_plus_api.connector_type import ConnectorType +from custom_components.button_plus.button_plus_api.connector_type import ConnectorType class Connector: diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py index a5ab8a3..149cc13 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py @@ -40,7 +40,7 @@ def test_model_v1_12(): # Assert the values from the parsed Core object assert device_config.core.name == "btn_4584b8" assert device_config.core.location == "Room 1" - assert device_config.core.auto_backup == "true" + assert device_config.core.auto_backup assert device_config.core.brightness == 80 assert device_config.core.color == 16765791 assert device_config.core.statusbar == 2 From 04308c4f5ec27aaf33fa78673f99e1fc9b60cade Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:43:37 +0200 Subject: [PATCH 08/29] More steps in the right direction, simplified model_V1_12 --- .../button_plus_api/model_interface.py | 51 +++++++++++- .../button_plus_api/model_v1_07.py | 45 +++++++---- .../button_plus_api/model_v1_12.py | 46 ++--------- custom_components/button_plus/config_flow.py | 81 ++++++++----------- 4 files changed, 121 insertions(+), 102 deletions(-) diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index ea4678a..5090fcf 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -1,5 +1,8 @@ from typing import List +from packaging.version import Version + +from button_plus.button_plus_api.event_type import EventType from custom_components.button_plus.button_plus_api.connector_type import ConnectorType @@ -19,6 +22,36 @@ def button_id(self) -> int: pass +class MqttBroker: + url: str + port: int + username: str + password: str + + def __init__( + self, + url: str, + port: int, + username: str, + password: str, + ): + """Initialize the MQTT broker.""" + pass + + +class Topic: + topic: str + event_type: EventType + + def __init__( + self, + topic: str, + event_type: EventType, + ): + """Initialize the MQTT topic.""" + pass + + class DeviceConfiguration: @staticmethod def from_json(json_data: str) -> "DeviceConfiguration": @@ -29,7 +62,7 @@ def to_json(self) -> str: """Serialize the DeviceConfiguration to a JSON string.""" pass - def firmware_version(self) -> str: + def firmware_version(self) -> Version: """Return the firmware version of the device.""" pass @@ -64,3 +97,19 @@ def connector_for(self, *identifier: int) -> Connector: def buttons(self) -> List[Button]: """Return the available buttons.""" pass + + def get_broker(self) -> MqttBroker: + """Return the MQTT broker.""" + pass + + def set_broker(self, broker: MqttBroker) -> None: + """Set the MQTT broker.""" + pass + + def add_topic(self, topic: Topic) -> None: + """Set the MQTT topic.""" + pass + + def remove_topic_for(self, event_type: EventType) -> None: + """Remove the MQTT topic.""" + pass diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index 3e4ceb8..91f6ce3 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -1,9 +1,12 @@ import json -from typing import List, Dict, Any +from typing import List, Dict, Any, MutableSet + +from packaging import version +from packaging.version import Version from .connector_type import ConnectorType from .event_type import EventType -from .model_interface import Button +from .model_interface import Button, Topic as TopicInterface class Connector: @@ -97,7 +100,7 @@ def __init__( led_color_front: int, led_color_wall: int, color: int, - topics: List[Topic], + topics: MutableSet[Topic], ): self.name = name self.location = location @@ -120,7 +123,7 @@ def from_dict(data: Dict[str, Any]) -> "Core": led_color_front=data["ledcolorfront"], led_color_wall=data["ledcolorwall"], color=data["color"], - topics=[Topic.from_dict(topic) for topic in data.get("topics", [])], + topics=MutableSet[Topic]([Topic.from_dict(topic) for topic in data.get("topics", [])]), ) @@ -134,7 +137,7 @@ def __init__( led_color_wall: int, long_delay: int, long_repeat: int, - topics: List[Topic], + topics: MutableSet[Topic], ): self.button_id = button_id self.label = label @@ -155,7 +158,7 @@ def from_dict(data: Dict[str, Any]) -> "MqttButton": led_color_wall=data["ledcolorwall"], long_delay=data["longdelay"], long_repeat=data["longrepeat"], - topics=[Topic.from_dict(topic) for topic in data.get("topics", [])], + topics=MutableSet[Topic]([Topic.from_dict(topic) for topic in data.get("topics", [])]), ) @@ -170,7 +173,7 @@ def __init__( label: str, unit: str, round: int, - topics: List[Topic], + topics: MutableSet[Topic], ): self.x = x self.y = y @@ -193,26 +196,26 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": label=data["label"], unit=data["unit"], round=data["round"], - topics=[Topic.from_dict(topic) for topic in data.get("topics", [])], + topics=MutableSet[Topic]([Topic.from_dict(topic) for topic in data.get("topics", [])]), ) class MqttBroker: def __init__( self, - broker_id: str, url: str, port: int, - ws_port: int, username: str, password: str, + broker_id="ha-button-plus", + ws_port=9001, ): - self.broker_id = broker_id self.url = url self.port = port - self.ws_port = ws_port self.username = username self.password = password + self.broker_id = broker_id + self.ws_port = ws_port @staticmethod def from_dict(data: Dict[str, Any]) -> "MqttBroker": @@ -348,8 +351,8 @@ def serialize(obj): return json.dumps(self, default=serialize, indent=4) - def firmware_version(self) -> str: - return self.info.firmware + def firmware_version(self) -> Version: + return version.parse(self.info.firmware) def name(self) -> str: return self.core.name or self.info.device_id @@ -385,3 +388,17 @@ def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: def buttons(self) -> List[Button]: return [button for button in self.mqtt_buttons] + + def add_topic(self, topic: TopicInterface) -> None: + self.core.topics.add(Topic( + broker_id="ha-button-plus", + topic=topic.topic, + payload="", + event_type=topic.event_type + )) + + def remove_topic_for(self, event_type: EventType) -> None: + # Remove the topic with EventType event_type + self.core.topics = [ + topic for topic in self.core.topics if topic.event_type != event_type + ] diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index f660644..6f3d3e1 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -1,9 +1,12 @@ import json from typing import List, Dict, Any +from packaging import version +from packaging.version import Version + from .connector_type import ConnectorType from .model_interface import Button -from .model_v1_07 import Connector, Sensor, Topic, MqttButton, MqttBroker, MqttSensor +from .model_v1_07 import Connector, Sensor, Topic, MqttButton, MqttBroker, MqttSensor, DeviceConfiguration as DeviceConfiguration_v1_07 class Info: @@ -116,7 +119,7 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": ) -class DeviceConfiguration: +class DeviceConfiguration(DeviceConfiguration_v1_07): def __init__( self, info: Info, @@ -249,40 +252,5 @@ def serialize(obj): return json.dumps(self, default=serialize) - def firmware_version(self) -> str: - return self.info.firmware - - def name(self) -> str: - return self.core.name or self.info.device_id - - def identifier(self) -> str: - return self.info.device_id - - def ip_address(self) -> str: - return self.info.ip_address - - def mac_address(self) -> str: - return self.info.mac - - def location(self) -> str: - return self.core.location - - def connector_for(self, *identifier: int) -> Connector: - return next( - ( - connector - for connector in self.info.connectors - if connector.identifier == identifier - ), - None, - ) - - def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: - return [ - connector - for connector in self.info.connectors - if connector.connector_type in [connector_type] - ] - - def buttons(self) -> List[Button]: - return [button for button in self.mqtt_buttons] + def firmware_version(self) -> Version: + return version.parse(self.info.firmware) diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index 44e9635..a6b4b8f 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -17,7 +17,7 @@ from .button_plus_api.api_client import ApiClient from .button_plus_api.local_api_client import LocalApiClient -from .button_plus_api.model import ConnectorEnum, DeviceConfiguration, MqttBroker +from .button_plus_api.model_interface import ConnectorType, DeviceConfiguration, MqttBroker, Topic from .button_plus_api.event_type import EventType from .const import DOMAIN @@ -103,8 +103,8 @@ async def async_step_manual(self, user_input=None): await api_client.push_config(device_config) return self.async_create_entry( - title=f"{device_config.core.name}", - description=f"Base module on {ip} with id {device_config.info.device_id}", + title=f"{device_config.name()}", + description=f"Base module on {ip} with id {device_config.identifier()}", data={"config": json_config}, ) @@ -238,79 +238,64 @@ def add_broker_to_config( broker_password = mqtt_entry.data.get("password", "") broker = MqttBroker( - broker_id="ha-button-plus", url=f"mqtt://{self.broker_endpoint}/", port=broker_port, - ws_port=9001, username=broker_username, password=broker_password, ) - device_config.mqtt_brokers.append(broker) + device_config.set_broker(broker) return device_config def add_topics_to_core( self, device_config: DeviceConfiguration ) -> DeviceConfiguration: - device_id = device_config.info.device_id + device_id = device_config.identifier() min_version = "1.11" - if version.parse(device_config.info.firmware) < version.parse(min_version): + if device_config.firmware_version() < 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}" + f"Current version {device_config.firmware_version()} 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.add_topic(Topic( + topic=f"buttonplus/{device_id}/brightness/large", + event_type=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.add_topic(Topic( + topic=f"buttonplus/{device_id}/brightness/mini", + event_type=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.add_topic(Topic( + topic=f"buttonplus/{device_id}/page/status", + event_type=EventType.PAGE_STATUS, + )) - device_config.core.topics.append( - { - "brokerid": "ha-button-plus", - "topic": f"buttonplus/{device_id}/page/set", - "payload": "", - "eventtype": EventType.SET_PAGE, - } - ) + device_config.add_topic(Topic( + topic=f"buttonplus/{device_id}/page/set", + event_type=EventType.SET_PAGE, + )) def add_topics_to_buttons( self, device_config: DeviceConfiguration ) -> DeviceConfiguration: device_id = device_config.info.device_id - active_connectors = [ - connector.connector_id - for connector in device_config.info.connectors - if connector.connector_type_enum() - in [ConnectorEnum.DISPLAY, ConnectorEnum.BAR] - ] + active_connectors = device_config.connectors_for( + ConnectorType.BAR, ConnectorType.DISPLAY + ) + # Each button should have a connector, so check if the buttons connector is present, else skip it + # Each connector has two buttons, and the *implicit API contract* is that connectors create buttons in + # ascending (and sorted) order. So connector 0 has buttons 0 and 1, connector 1 has buttons 2 and 3, etc. + # + # This means the connector ID is equal to floor(button_id / 2). Button ID's start at 0! So: + # button 0 and 1 are on connector 0, button 2 and 3 are on connector 1 for button in filter( - lambda b: b.button_id // 2 in active_connectors, device_config.mqtt_buttons + lambda button: button.button_id // 2 in active_connectors, device_config.mqtt_buttons ): # Create topics for button main label button.topics.append( From 70029bc47c86162ee1f5ffa17b819ca0226991b2 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:42:08 +0200 Subject: [PATCH 09/29] Removed a LOT of code by reusing JSON serialisation from 1.07 --- .gitignore | 3 +- .../button_plus_api/JSONCustomEncoder.py | 9 + .../button_plus_api/model_detection.py | 10 +- .../button_plus_api/model_interface.py | 6 +- .../button_plus_api/model_v1_07.py | 233 ++++++++++-------- .../button_plus_api/model_v1_12.py | 156 ++++-------- .../button_plus_api/test_model_v1_07.py | 20 +- .../button_plus_api/test_model_v1_12.py | 18 +- 8 files changed, 226 insertions(+), 229 deletions(-) create mode 100644 custom_components/button_plus/button_plus_api/JSONCustomEncoder.py diff --git a/.gitignore b/.gitignore index 66681f7..77426fb 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,5 @@ crashlytics.properties crashlytics-build.properties fabric.properties -.idea \ No newline at end of file +.idea +.run diff --git a/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py b/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py new file mode 100644 index 0000000..7f081fe --- /dev/null +++ b/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py @@ -0,0 +1,9 @@ +import json + + +# Python MAGIC to be able to use NORMAL serialisation (-: +class CustomEncoder(json.JSONEncoder): + def default(self, obj): + if hasattr(obj, 'to_dict'): + return obj.to_dict() + return super().default(obj) diff --git a/custom_components/button_plus/button_plus_api/model_detection.py b/custom_components/button_plus/button_plus_api/model_detection.py index 445f51b..6536c75 100644 --- a/custom_components/button_plus/button_plus_api/model_detection.py +++ b/custom_components/button_plus/button_plus_api/model_detection.py @@ -1,19 +1,17 @@ import json from packaging.version import parse as parseSemver -from .model_interface import DeviceConfiguration +from .model_interface import DeviceConfiguration as DeviceConfigurationInterface class ModelDetection: @staticmethod - def model_for_json(json_data: str) -> "DeviceConfiguration": + def model_for_json(json_data: str) -> "DeviceConfigurationInterface": data = json.loads(json_data) device_version = parseSemver(data["info"]["firmware"]) if device_version >= parseSemver("1.12.0"): from .model_v1_12 import DeviceConfiguration - - return DeviceConfiguration.from_json(data) + return DeviceConfiguration.from_dict(data) else: from .model_v1_07 import DeviceConfiguration - - return DeviceConfiguration.from_json(data) + return DeviceConfiguration.from_dict(data) diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 5090fcf..1c29e76 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -2,7 +2,7 @@ from packaging.version import Version -from button_plus.button_plus_api.event_type import EventType +from custom_components.button_plus.button_plus_api.event_type import EventType from custom_components.button_plus.button_plus_api.connector_type import ConnectorType @@ -54,8 +54,8 @@ def __init__( class DeviceConfiguration: @staticmethod - def from_json(json_data: str) -> "DeviceConfiguration": - """Deserialize the DeviceConfiguration from a JSON string.""" + def from_dict(json_data: any) -> "DeviceConfiguration": + """Deserialize the DeviceConfiguration from a dictionary.""" pass def to_json(self) -> str: diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index 91f6ce3..c81452d 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -1,9 +1,10 @@ import json -from typing import List, Dict, Any, MutableSet +from typing import List, Dict, Any from packaging import version from packaging.version import Version +from .JSONCustomEncoder import CustomEncoder from .connector_type import ConnectorType from .event_type import EventType from .model_interface import Button, Topic as TopicInterface @@ -14,16 +15,19 @@ def __init__(self, identifier: int, connector_type: int): self.identifier = identifier self.connector_type = connector_type - @staticmethod - def from_dict(data: Dict[str, Any]) -> "Connector": - return Connector(identifier=data["id"], connector_type=data["type"]) - def identifier(self) -> int: return self.identifier def connector_type(self) -> ConnectorType: return ConnectorType(self.connector_type) + @staticmethod + def from_dict(data: Dict[str, Any]) -> "Connector": + return Connector(identifier=data["id"], connector_type=data["type"]) + + def to_dict(self) -> Dict[str, Any]: + return {"id": self.identifier, "type": self.connector_type} + class Sensor: def __init__(self, sensor_id: int, description: str): @@ -34,6 +38,9 @@ def __init__(self, sensor_id: int, description: str): def from_dict(data: Dict[str, Any]) -> "Sensor": return Sensor(sensor_id=data["sensorid"], description=data["description"]) + def to_dict(self) -> Dict[str, Any]: + return {"sensorid": self.sensor_id, "description": self.description} + class Info: def __init__( @@ -63,11 +70,22 @@ def from_dict(data: Dict[str, Any]) -> "Info": firmware=data["firmware"], large_display=data["largedisplay"], connectors=[ - Connector.from_dict(connector) for connector in data["connectors"] + Connector.from_dict(data=connector) for connector in data["connectors"] ], sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]], ) + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.device_id, + "mac": self.mac, + "ipaddress": self.ip_address, + "firmware": self.firmware, + "largedisplay": self.large_display, + "connectors": [connector.to_dict() for connector in self.connectors], + "sensors": [sensor.to_dict() for sensor in self.sensors], + } + class Topic: def __init__(self, broker_id: str, topic: str, payload: str, event_type: int): @@ -88,6 +106,14 @@ def from_dict(data: Dict[str, Any]) -> "Topic": event_type=data["eventtype"], ) + def to_dict(self) -> Dict[str, Any]: + return { + "brokerid": self.broker_id, + "topic": self.topic, + "payload": self.payload, + "eventtype": self.event_type, + } + class Core: def __init__( @@ -100,7 +126,7 @@ def __init__( led_color_front: int, led_color_wall: int, color: int, - topics: MutableSet[Topic], + topics: List[Topic], ): self.name = name self.location = location @@ -123,9 +149,23 @@ def from_dict(data: Dict[str, Any]) -> "Core": led_color_front=data["ledcolorfront"], led_color_wall=data["ledcolorwall"], color=data["color"], - topics=MutableSet[Topic]([Topic.from_dict(topic) for topic in data.get("topics", [])]), + topics=[Topic.from_dict(topic) for topic in data.get("topics", [])], ) + def to_dict(self) -> Dict[str, Any]: + return { + "name": self.name, + "location": self.location, + "autobackup": self.auto_backup, + "brightnesslargedisplay": self.brightness_large_display, + "brightnessminidisplay": self.brightness_mini_display, + "ledcolorfront": self.led_color_front, + "ledcolorwall": self.led_color_wall, + "color": self.color, + # Only the Core object does not include the key when this list is empty (-: + **({"topics": [topic.to_dict() for topic in self.topics]} if len(self.topics) > 0 else {}) + } + class MqttButton(Button): def __init__( @@ -137,7 +177,7 @@ def __init__( led_color_wall: int, long_delay: int, long_repeat: int, - topics: MutableSet[Topic], + topics: List[Topic], ): self.button_id = button_id self.label = label @@ -158,9 +198,21 @@ def from_dict(data: Dict[str, Any]) -> "MqttButton": led_color_wall=data["ledcolorwall"], long_delay=data["longdelay"], long_repeat=data["longrepeat"], - topics=MutableSet[Topic]([Topic.from_dict(topic) for topic in data.get("topics", [])]), + topics=[Topic.from_dict(topic) for topic in data.get("topics", [])], ) + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.button_id, + "label": self.label, + "toplabel": self.top_label, + "ledcolorfront": self.led_color_front, + "ledcolorwall": self.led_color_wall, + "longdelay": self.long_delay, + "longrepeat": self.long_repeat, + "topics": [topic.to_dict() for topic in self.topics], + } + class MqttDisplay: def __init__( @@ -173,7 +225,7 @@ def __init__( label: str, unit: str, round: int, - topics: MutableSet[Topic], + topics: List[Topic], ): self.x = x self.y = y @@ -196,9 +248,22 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": label=data["label"], unit=data["unit"], round=data["round"], - topics=MutableSet[Topic]([Topic.from_dict(topic) for topic in data.get("topics", [])]), + topics=[Topic.from_dict(topic) for topic in data.get("topics", [])], ) + def to_dict(self) -> Dict[str, Any]: + return { + "x": self.x, + "y": self.y, + "fontsize": self.font_size, + "align": self.align, + "width": self.width, + "label": self.label, + "unit": self.unit, + "round": self.round, + "topics": [topic.to_dict() for topic in self.topics], + } + class MqttBroker: def __init__( @@ -228,6 +293,16 @@ def from_dict(data: Dict[str, Any]) -> "MqttBroker": password=data["password"], ) + def to_dict(self) -> Dict[str, Any]: + return { + "brokerid": self.broker_id, + "url": self.url, + "port": self.port, + "wsport": self.ws_port, + "username": self.username, + "password": self.password + } + class MqttSensor: def __init__(self, sensor_id: int, topic: Topic, interval: int): @@ -243,6 +318,13 @@ def from_dict(data: Dict[str, Any]) -> "MqttSensor": interval=data["interval"], ) + def to_dict(self) -> Dict[str, Any]: + return { + "sensorid": self.sensor_id, + "topic": self.topic.to_dict(), + "interval": self.interval, + } + class DeviceConfiguration: def __init__( @@ -261,96 +343,6 @@ def __init__( self.mqtt_brokers = mqtt_brokers self.mqtt_sensors = mqtt_sensors - @staticmethod - def from_json(json_data: any) -> "DeviceConfiguration": - return DeviceConfiguration( - info=Info.from_dict(json_data["info"]), - core=Core.from_dict(json_data["core"]), - mqtt_buttons=[ - MqttButton.from_dict(button) for button in json_data["mqttbuttons"] - ], - mqtt_displays=[ - MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"] - ], - mqtt_brokers=[ - MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"] - ], - mqtt_sensors=[ - MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] - ], - ) - - def to_json(self) -> str: - def serialize(obj): - if hasattr(obj, "__dict__"): - d = obj.__dict__.copy() - - # Convert the root keys - if isinstance(obj, DeviceConfiguration): - d["mqttbuttons"] = [ - serialize(button) for button in d.pop("mqtt_buttons") - ] - d["mqttdisplays"] = [ - serialize(display) for display in d.pop("mqtt_displays") - ] - d["mqttbrokers"] = [ - serialize(broker) for broker in d.pop("mqtt_brokers") - ] - d["mqttsensors"] = [ - serialize(sensor) for sensor in d.pop("mqtt_sensors") - ] - - if isinstance(obj, Info): - d["id"] = d.pop("device_id") - d["ipaddress"] = d.pop("ip_address") - d["largedisplay"] = d.pop("large_display") - - elif isinstance(obj, Connector): - d["id"] = d.pop("identifier") - d["type"] = d.pop("connector_type") - - elif isinstance(obj, Sensor): - d["sensorid"] = d.pop("sensor_id") - - elif isinstance(obj, Core): - d["autobackup"] = d.pop("auto_backup") - d["brightnesslargedisplay"] = d.pop("brightness_large_display") - d["brightnessminidisplay"] = d.pop("brightness_mini_display") - d["ledcolorfront"] = d.pop("led_color_front") - d["ledcolorwall"] = d.pop("led_color_wall") - - # Custom mappings for MqttButton class - elif isinstance(obj, MqttButton): - d["id"] = d.pop("button_id") - d["toplabel"] = d.pop("top_label") - d["ledcolorfront"] = d.pop("led_color_front") - d["ledcolorwall"] = d.pop("led_color_wall") - d["longdelay"] = d.pop("long_delay") - d["longrepeat"] = d.pop("long_repeat") - - elif isinstance(obj, Topic): - d["brokerid"] = d.pop("broker_id") - d["eventtype"] = d.pop("event_type") - - elif isinstance(obj, MqttDisplay): - d["fontsize"] = d.pop("font_size") - d["topics"] = [serialize(topic) for topic in d["topics"]] - - elif isinstance(obj, MqttBroker): - d["brokerid"] = d.pop("broker_id") - d["wsport"] = d.pop("ws_port") - - elif isinstance(obj, MqttSensor): - d["sensorid"] = d.pop("sensor_id") - d["topic"] = serialize(d["topic"]) - - # Filter out None values - return {k: v for k, v in d.items() if v is not None} - else: - return str(obj) - - return json.dumps(self, default=serialize, indent=4) - def firmware_version(self) -> Version: return version.parse(self.info.firmware) @@ -402,3 +394,40 @@ def remove_topic_for(self, event_type: EventType) -> None: self.core.topics = [ topic for topic in self.core.topics if topic.event_type != event_type ] + + @staticmethod + def from_dict(json_data: any) -> "DeviceConfiguration": + return DeviceConfiguration( + info=Info.from_dict(json_data["info"]), + core=Core.from_dict(json_data["core"]), + mqtt_buttons=[ + MqttButton.from_dict(button) for button in json_data["mqttbuttons"] + ], + mqtt_displays=[ + MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"] + ], + mqtt_brokers=[ + MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"] + ], + mqtt_sensors=[ + MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] + ], + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "info": self.info.to_dict(), + "core": self.core.to_dict(), + "mqttbuttons": [button.to_dict() for button in self.mqtt_buttons], + "mqttdisplays": [display.to_dict() for display in self.mqtt_displays], + "mqttbrokers": [broker.to_dict() for broker in self.mqtt_brokers], + "mqttsensors": [sensor.to_dict() for sensor in self.mqtt_sensors], + } + + def to_json(self) -> str: + return json.dumps( + self, + sort_keys=True, + cls=CustomEncoder, + indent=4, + ) \ No newline at end of file diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index 6f3d3e1..25dc516 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -1,12 +1,13 @@ import json from typing import List, Dict, Any - -from packaging import version -from packaging.version import Version - -from .connector_type import ConnectorType -from .model_interface import Button -from .model_v1_07 import Connector, Sensor, Topic, MqttButton, MqttBroker, MqttSensor, DeviceConfiguration as DeviceConfiguration_v1_07 +from .model_v1_07 import ( + Connector, + Sensor, + Topic, MqttButton, + MqttBroker, + MqttSensor, + DeviceConfiguration as DeviceConfiguration_v1_07 +) class Info: @@ -42,6 +43,17 @@ def from_dict(data: Dict[str, Any]) -> "Info": sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]], ) + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.device_id, + "mac": self.mac, + "ipaddress": self.ip_address, + "firmware": self.firmware, + "largedisplay": self.large_display, + "connectors": [connector.to_dict() for connector in self.connectors], + "sensors": [sensor.to_dict() for sensor in self.sensors], + } + class Core: def __init__( @@ -74,6 +86,17 @@ def from_dict(data: Dict[str, Any]) -> "Core": topics=[Topic.from_dict(topic) for topic in data["topics"]], ) + def to_dict(self) -> Dict[str, Any]: + return { + "name": self.name, + "location": self.location, + "autobackup": self.auto_backup, + "brightness": self.brightness, + "color": self.color, + "statusbar": self.statusbar, + "topics": [topic.to_dict() for topic in self.topics], + } + class MqttDisplay: def __init__( @@ -118,8 +141,24 @@ def from_dict(data: Dict[str, Any]) -> "MqttDisplay": topics=[Topic.from_dict(topic) for topic in data["topics"]], ) + def to_dict(self) -> Dict[str, Any]: + return { + "x": self.x, + "y": self.y, + "boxtype": self.box_type, + "fontsize": self.font_size, + "align": self.align, + "width": self.width, + "label": self.label, + "unit": self.unit, + "round": self.round, + "page": self.page, + "topics": [topic.to_dict() for topic in self.topics], + } + class DeviceConfiguration(DeviceConfiguration_v1_07): + # Info, Core and MqttDisplay are different, so we need to redefine those def __init__( self, info: Info, @@ -136,8 +175,9 @@ def __init__( self.mqtt_brokers = mqtt_brokers self.mqtt_sensors = mqtt_sensors + # Same here, use the new classes for Info, Core and MqttDisplay @staticmethod - def from_json(json_data: any) -> "DeviceConfiguration": + def from_dict(json_data: any) -> "DeviceConfiguration": return DeviceConfiguration( info=Info.from_dict(json_data["info"]), core=Core.from_dict(json_data["core"]), @@ -154,103 +194,3 @@ def from_json(json_data: any) -> "DeviceConfiguration": MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] ], ) - - def to_json(self) -> str: - def serialize(obj): - if hasattr(obj, "__dict__"): - d = obj.__dict__.copy() - - if isinstance(obj, DeviceConfiguration): - d["info"] = serialize(d.pop("info")) - d["core"] = serialize(d.pop("core")) - d["mqttbuttons"] = [ - serialize(button) for button in d.pop("mqtt_buttons") - ] - d["mqttdisplays"] = [ - serialize(display) for display in d.pop("mqtt_displays") - ] - d["mqttbrokers"] = [ - serialize(broker) for broker in d.pop("mqtt_brokers") - ] - d["mqttsensors"] = [ - serialize(sensor) for sensor in d.pop("mqtt_sensors") - ] - - if isinstance(obj, Info): - d["id"] = d.pop("device_id") - d["mac"] = d.pop("mac") - d["ipaddress"] = d.pop("ip_address") - d["firmware"] = d.pop("firmware") - d["largedisplay"] = d.pop("large_display") - d["connectors"] = [ - serialize(connector) for connector in d.pop("connectors") - ] - d["sensors"] = [serialize(sensor) for sensor in d.pop("sensors")] - - elif isinstance(obj, Connector): - d["id"] = d.pop("identifier") - d["type"] = d.pop("connector_type") - - elif isinstance(obj, Sensor): - d["sensorid"] = d.pop("sensor_id") - d["description"] = d.pop("description") - - elif isinstance(obj, Core): - d["name"] = d.pop("name") - d["location"] = d.pop("location") - d["autobackup"] = d.pop("auto_backup") - d["brightness"] = d.pop("brightness") - d["color"] = d.pop("color") - d["statusbar"] = d.pop("statusbar") - d["topics"] = [serialize(topic) for topic in d.pop("topics")] - - elif isinstance(obj, MqttButton): - d["id"] = d.pop("button_id") - d["label"] = d.pop("label") - d["toplabel"] = d.pop("top_label") - d["ledcolorfront"] = d.pop("led_color_front") - d["ledcolorwall"] = d.pop("led_color_wall") - d["longdelay"] = d.pop("long_delay") - d["longrepeat"] = d.pop("long_repeat") - d["topics"] = [serialize(topic) for topic in d.pop("topics")] - - elif isinstance(obj, Topic): - d["brokerid"] = d.pop("broker_id") - d["topic"] = d.pop("topic") - d["payload"] = d.pop("payload") - d["eventtype"] = d.pop("event_type") - - elif isinstance(obj, MqttDisplay): - d["x"] = d.pop("x") - d["y"] = d.pop("y") - d["boxtype"] = d.pop("box_type") - d["fontsize"] = d.pop("font_size") - d["align"] = d.pop("align") - d["width"] = d.pop("width") - d["label"] = d.pop("label") - d["unit"] = d.pop("unit") - d["round"] = d.pop("round") - d["page"] = d.pop("page") - d["topics"] = [serialize(topic) for topic in d.pop("topics")] - - elif isinstance(obj, MqttBroker): - d["brokerid"] = d.pop("broker_id") - d["url"] = d.pop("url") - d["port"] = d.pop("port") - d["wsport"] = d.pop("ws_port") - d["username"] = d.pop("username") - d["password"] = d.pop("password") - - elif isinstance(obj, MqttSensor): - d["sensorid"] = d.pop("sensor_id") - d["interval"] = d.pop("interval") - d["topic"] = serialize(d["topic"]) - - return {k: v for k, v in d.items() if v is not None} - else: - return str(obj) - - return json.dumps(self, default=serialize) - - def firmware_version(self) -> Version: - return version.parse(self.info.firmware) diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py index d8dcd08..3c42a04 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py @@ -5,6 +5,24 @@ DeviceConfiguration, ) +def test_model_v1_07_from_to_json_should_be_same(): + # Load the JSON file + with open("resource/physicalconfig1.07.json") as file: + json_string = file.read() + json_data = json.loads(json_string) + + # Parse the JSON data into a DeviceConfiguration object + device_config = DeviceConfiguration.from_dict(json_data) + + # Serialize the DeviceConfiguration object back into a JSON string + new_json_string = device_config.to_json() + + # Parse the JSON data into a DeviceConfiguration object + new_json_data = json.loads(new_json_string) + new_device_config = DeviceConfiguration.from_dict(new_json_data) + + # Assert that the JSON data is the same + assert json_data == new_json_data @pytest.fixture def device_config(): @@ -12,7 +30,7 @@ def device_config(): with open("resource/physicalconfig1.07.json") as file: json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object - return DeviceConfiguration.from_json(json_data) + return DeviceConfiguration.from_dict(json_data) def test_buttons(device_config): diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py index 149cc13..034c5ff 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py @@ -7,19 +7,21 @@ def test_model_v1_12_from_to_json_should_be_same(): # Load the JSON file with open("resource/physicalconfig1.12.1.json") as file: - json_data = json.loads(file.read()) + json_string = file.read() + json_data = json.loads(json_string) # Parse the JSON data into a DeviceConfiguration object - device_config = DeviceConfiguration.from_json(json_data) + device_config = DeviceConfiguration.from_dict(json_data) # Serialize the DeviceConfiguration object back into a JSON string - json_string = device_config.to_json() + new_json_string = device_config.to_json() - original_json_data = json_data - new_json_data = json.loads(json_string) + # Parse the JSON data into a DeviceConfiguration object + new_json_data = json.loads(new_json_string) + new_device_config = DeviceConfiguration.from_dict(new_json_data) - # Assert that the JSON strings are the same - assert original_json_data == new_json_data + # Assert that the JSON data is the same + assert json_data == new_json_data def test_model_v1_12(): @@ -28,7 +30,7 @@ def test_model_v1_12(): json_data = json.loads(file.read()) # Parse the JSON data into a DeviceConfiguration object - device_config = DeviceConfiguration.from_json(json_data) + device_config = DeviceConfiguration.from_dict(json_data) # Assert the values from the parsed DeviceConfiguration object assert device_config.info.device_id == "btn_4584b8" From 80c3e327f9e3026680378c295230d7912da5e21f Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:23:41 +0200 Subject: [PATCH 10/29] Model complete, let's take a look at ConfigFlow --- .../button_plus_api/JSONCustomEncoder.py | 2 +- .../button_plus_api/model_detection.py | 2 + .../button_plus_api/model_interface.py | 39 ++++--- .../button_plus_api/model_v1_07.py | 27 +++-- .../button_plus_api/model_v1_12.py | 6 +- .../button_plus/buttonplushub.py | 9 +- custom_components/button_plus/config_flow.py | 57 +++++---- custom_components/button_plus/light.py | 16 +-- custom_components/button_plus/number.py | 10 +- custom_components/button_plus/sensor.py | 102 ---------------- custom_components/button_plus/switch.py | 110 ------------------ custom_components/button_plus/text.py | 38 +++--- .../button_plus_api/test_model_v1_07.py | 3 +- .../button_plus_api/test_model_v1_12.py | 1 - 14 files changed, 121 insertions(+), 301 deletions(-) delete mode 100644 custom_components/button_plus/sensor.py delete mode 100644 custom_components/button_plus/switch.py diff --git a/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py b/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py index 7f081fe..1f401e9 100644 --- a/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py +++ b/custom_components/button_plus/button_plus_api/JSONCustomEncoder.py @@ -4,6 +4,6 @@ # Python MAGIC to be able to use NORMAL serialisation (-: class CustomEncoder(json.JSONEncoder): def default(self, obj): - if hasattr(obj, 'to_dict'): + if hasattr(obj, "to_dict"): return obj.to_dict() return super().default(obj) diff --git a/custom_components/button_plus/button_plus_api/model_detection.py b/custom_components/button_plus/button_plus_api/model_detection.py index 6536c75..65752f0 100644 --- a/custom_components/button_plus/button_plus_api/model_detection.py +++ b/custom_components/button_plus/button_plus_api/model_detection.py @@ -11,7 +11,9 @@ def model_for_json(json_data: str) -> "DeviceConfigurationInterface": if device_version >= parseSemver("1.12.0"): from .model_v1_12 import DeviceConfiguration + return DeviceConfiguration.from_dict(data) else: from .model_v1_07 import DeviceConfiguration + return DeviceConfiguration.from_dict(data) diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 1c29e76..7b8f1e0 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -29,11 +29,11 @@ class MqttBroker: password: str def __init__( - self, - url: str, - port: int, - username: str, - password: str, + self, + url: str, + port: int, + username: str, + password: str, ): """Initialize the MQTT broker.""" pass @@ -44,28 +44,23 @@ class Topic: event_type: EventType def __init__( - self, - topic: str, - event_type: EventType, + self, + topic: str, + event_type: EventType, ): """Initialize the MQTT topic.""" pass class DeviceConfiguration: - @staticmethod - def from_dict(json_data: any) -> "DeviceConfiguration": - """Deserialize the DeviceConfiguration from a dictionary.""" - pass - - def to_json(self) -> str: - """Serialize the DeviceConfiguration to a JSON string.""" - pass - def firmware_version(self) -> Version: """Return the firmware version of the device.""" pass + def supports_brightness(self) -> bool: + """Return if the device supports brightness.""" + pass + def name(self) -> str: """Return the name of the device.""" pass @@ -113,3 +108,13 @@ def add_topic(self, topic: Topic) -> None: def remove_topic_for(self, event_type: EventType) -> None: """Remove the MQTT topic.""" pass + + @staticmethod + def from_dict(json_data: any) -> "DeviceConfiguration": + """Deserialize the DeviceConfiguration from a dictionary.""" + pass + + def to_json(self) -> str: + """Serialize the DeviceConfiguration to a JSON string.""" + pass + diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index c81452d..fb3e742 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -163,7 +163,11 @@ def to_dict(self) -> Dict[str, Any]: "ledcolorwall": self.led_color_wall, "color": self.color, # Only the Core object does not include the key when this list is empty (-: - **({"topics": [topic.to_dict() for topic in self.topics]} if len(self.topics) > 0 else {}) + **( + {"topics": [topic.to_dict() for topic in self.topics]} + if len(self.topics) > 0 + else {} + ), } @@ -300,7 +304,7 @@ def to_dict(self) -> Dict[str, Any]: "port": self.port, "wsport": self.ws_port, "username": self.username, - "password": self.password + "password": self.password, } @@ -346,6 +350,9 @@ def __init__( def firmware_version(self) -> Version: return version.parse(self.info.firmware) + def supports_brightness(self) -> bool: + return self.firmware_version() >= version.parse("1.11") + def name(self) -> str: return self.core.name or self.info.device_id @@ -382,12 +389,14 @@ def buttons(self) -> List[Button]: return [button for button in self.mqtt_buttons] def add_topic(self, topic: TopicInterface) -> None: - self.core.topics.add(Topic( - broker_id="ha-button-plus", - topic=topic.topic, - payload="", - event_type=topic.event_type - )) + self.core.topics.add( + Topic( + broker_id="ha-button-plus", + topic=topic.topic, + payload="", + event_type=topic.event_type, + ) + ) def remove_topic_for(self, event_type: EventType) -> None: # Remove the topic with EventType event_type @@ -430,4 +439,4 @@ def to_json(self) -> str: sort_keys=True, cls=CustomEncoder, indent=4, - ) \ No newline at end of file + ) diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index 25dc516..584c1fc 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -1,12 +1,12 @@ -import json from typing import List, Dict, Any from .model_v1_07 import ( Connector, Sensor, - Topic, MqttButton, + Topic, + MqttButton, MqttBroker, MqttSensor, - DeviceConfiguration as DeviceConfiguration_v1_07 + DeviceConfiguration as DeviceConfiguration_v1_07, ) diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index fafd2ad..4341da2 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -38,6 +38,9 @@ def __init__( self.top_label_entities = {} self.brightness_entities = {} + self.manufacturer=MANUFACTURER + self.model="Base Module" + device_registry = dr.async_get(hass) self.device = device_registry.async_get_or_create( @@ -45,10 +48,10 @@ def __init__( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, self.config.mac_address())}, identifiers={(DOMAIN, self.config.identifier())}, - manufacturer=MANUFACTURER, + manufacturer=self.manufacturer, suggested_area=self.config.location(), name=self._name, - model="Base Module", + model=self.model, sw_version=config.firmware_version(), ) @@ -102,7 +105,7 @@ def create_bar_module( model="Bar module", manufacturer=MANUFACTURER, suggested_area=hub.config.location(), - identifiers={(DOMAIN, f"{hub.identifier} BAR Module {connector_id}")}, + identifiers={(DOMAIN, f"{hub.hub} BAR Module {connector_id}")}, via_device=(DOMAIN, hub.hub_id), ) diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index a6b4b8f..7df217f 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -17,7 +17,12 @@ from .button_plus_api.api_client import ApiClient from .button_plus_api.local_api_client import LocalApiClient -from .button_plus_api.model_interface import ConnectorType, DeviceConfiguration, MqttBroker, Topic +from .button_plus_api.model_interface import ( + ConnectorType, + DeviceConfiguration, + MqttBroker, + Topic, +) from .button_plus_api.event_type import EventType from .const import DOMAIN @@ -251,33 +256,40 @@ def add_topics_to_core( self, device_config: DeviceConfiguration ) -> DeviceConfiguration: device_id = device_config.identifier() - min_version = "1.11" - if device_config.firmware_version() < version.parse(min_version): - _LOGGER.debug( - f"Current version {device_config.firmware_version()} doesn't support the brightness, it must be at least firmware version {min_version}" + if device_config.supports_brightness() is False: + _LOGGER.info( + "Current firmware version doesn't support brightness settings, it must be at least firmware version 1.11" ) return - device_config.add_topic(Topic( - topic=f"buttonplus/{device_id}/brightness/large", - event_type=EventType.BRIGHTNESS_LARGE_DISPLAY, - )) + device_config.add_topic( + Topic( + topic=f"buttonplus/{device_id}/brightness/large", + event_type=EventType.BRIGHTNESS_LARGE_DISPLAY, + ) + ) - device_config.add_topic(Topic( - topic=f"buttonplus/{device_id}/brightness/mini", - event_type=EventType.BRIGHTNESS_MINI_DISPLAY, - )) + device_config.add_topic( + Topic( + topic=f"buttonplus/{device_id}/brightness/mini", + event_type=EventType.BRIGHTNESS_MINI_DISPLAY, + ) + ) - device_config.add_topic(Topic( - topic=f"buttonplus/{device_id}/page/status", - event_type=EventType.PAGE_STATUS, - )) + device_config.add_topic( + Topic( + topic=f"buttonplus/{device_id}/page/status", + event_type=EventType.PAGE_STATUS, + ) + ) - device_config.add_topic(Topic( - topic=f"buttonplus/{device_id}/page/set", - event_type=EventType.SET_PAGE, - )) + device_config.add_topic( + Topic( + topic=f"buttonplus/{device_id}/page/set", + event_type=EventType.SET_PAGE, + ) + ) def add_topics_to_buttons( self, device_config: DeviceConfiguration @@ -295,7 +307,8 @@ def add_topics_to_buttons( # This means the connector ID is equal to floor(button_id / 2). Button ID's start at 0! So: # button 0 and 1 are on connector 0, button 2 and 3 are on connector 1 for button in filter( - lambda button: button.button_id // 2 in active_connectors, device_config.mqtt_buttons + lambda button: button.button_id // 2 in active_connectors, + device_config.mqtt_buttons, ): # Create topics for button main label button.topics.append( diff --git a/custom_components/button_plus/light.py b/custom_components/button_plus/light.py index ac9cd68..b0b9389 100644 --- a/custom_components/button_plus/light.py +++ b/custom_components/button_plus/light.py @@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ButtonPlusHub +from .button_plus_api.connector_type import ConnectorType from .const import DOMAIN, MANUFACTURER @@ -26,18 +27,19 @@ async def async_setup_entry( """Add switches for passed config_entry in HA.""" hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] - buttons = hub.config.mqtt_buttons + buttons = hub.config.buttons() for button in buttons: # _LOGGER.debug(f"Creating Lights with parameters: {button.button_id} {button.label} {hub.hub_id}") - lights.append(ButtonPlusWallLight(button.button_id, hub)) - lights.append(ButtonPlusFrontLight(button.button_id, hub)) + lights.append(ButtonPlusWallLight(button.button_id(), hub)) + lights.append(ButtonPlusFrontLight(button.button_id(), hub)) async_add_entities(lights) class ButtonPlusLight(LightEntity): def __init__(self, btn_id: int, hub: ButtonPlusHub, light_type: str): + connectors=hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) self._btn_id = btn_id self._hub = hub self._hub_id = hub.hub_id @@ -46,7 +48,7 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub, light_type: str): self.entity_id = f"light.{light_type}_{self._hub_id}_{btn_id}" self._attr_name = f"light-{light_type}-{btn_id}" self._state = False - self._connector = hub.config.info.connectors[btn_id // 2] + self._connector = connectors[btn_id // 2] @property def is_on(self) -> bool | None: @@ -86,15 +88,15 @@ def device_info(self): match self._connector.connector_type: case 1: - device_info["name"] = f"BAR Module {self._connector.connector_id}" + device_info["name"] = f"BAR Module {self._connector.identifier()}" device_info["connections"] = { - ("bar_module", self._connector.connector_id) + ("bar_module", self._connector.identifier()) } device_info["model"] = "BAR Module" device_info["identifiers"] = { ( DOMAIN, - f"{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.connector_id}", + f"{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.identifier()}", ) } case 2: diff --git a/custom_components/button_plus/number.py b/custom_components/button_plus/number.py index 22521c7..def12bf 100644 --- a/custom_components/button_plus/number.py +++ b/custom_components/button_plus/number.py @@ -30,10 +30,9 @@ async def async_setup_entry( hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] - min_version = "1.11" - if version.parse(hub.config.info.firmware) < version.parse(min_version): + if hub.config.supports_brightness() is False: _LOGGER.info( - f"Current version {hub.config.info.firmware} doesn't support the brightness, it must be at least firmware version {min_version}" + "Current firmware version doesn't support brightness settings, it must be at least firmware version 1.11" ) return @@ -44,7 +43,7 @@ async def async_setup_entry( large = ButtonPlusLargeBrightness(hub) brightness.append(large) - hub.add_brightness("large", mini) + hub.add_brightness("large", large) async_add_entities(brightness) @@ -57,7 +56,6 @@ def __init__(self, hub: ButtonPlusHub, brightness_type: str, event_type: EventTy 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}" @@ -85,7 +83,7 @@ def update(self) -> None: def device_info(self) -> DeviceInfo: """Return information to link this entity with the correct device.""" - identifiers: set[tuple[str, str]] = {} + identifiers: set[tuple[str, str]] = set() match self.event_type: case EventType.BRIGHTNESS_MINI_DISPLAY: diff --git a/custom_components/button_plus/sensor.py b/custom_components/button_plus/sensor.py deleted file mode 100644 index 12eeedf..0000000 --- a/custom_components/button_plus/sensor.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Platform for sensor integration.""" -# This file shows the setup for the sensors associated with the cover. -# They are setup in the same way with the call to the async_setup_entry function -# via HA from the module __init__. Each sensor has a device_class, this tells HA how -# to display it in the UI (for know types). The unit_of_measurement property tells HA -# what the unit is, so it can display the correct range. For predefined types (such as -# battery), the unit_of_measurement should match what's expected. - -from homeassistant.const import ( - DEVICE_CLASS_ILLUMINANCE, -) -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import Entity - -from .buttonplushub import ButtonPlusBase -from .const import DOMAIN - - -# See cover.py for more details. -# Note how both entities for each roller sensor (battry and illuminance) are added at -# the same time to the same list. This way only a single async_add_devices call is -# required. -async def async_setup_entry(hass, config_entry, async_add_entities): - """Add sensors for passed config_entry in HA.""" - hub = hass.data[DOMAIN][config_entry.entry_id] - - new_devices = [] - for device in hub.devices: - new_devices.append(IlluminanceSensor(device)) - - if new_devices: - async_add_entities(new_devices) - - -# This base class shows the common properties and methods for a sensor as used in this -# example. See each sensor for further details about properties and methods that -# have been overridden. -class SensorBase(Entity): - """Base representation of a Button+ Sensor.""" - - should_poll = False - - def __init__(self, buttonplus_base): - """Initialize the sensor.""" - self._base = buttonplus_base - - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. - @property - def device_info(self) -> DeviceInfo: - """Information about this entity/device.""" - return { - "identifiers": {(DOMAIN, self._base.button_plus_base_id)}, - # If desired, the name for the device could be different to the entity - "name": self._base.name, - "sw_version": self._base.firmware_version, - "model": self._base.model, - "manufacturer": self._base.hub.manufacturer, - } - - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - return self._base.online and self._base.hub.online @ property - - async def async_added_to_hass(self): - """Run when this Entity has been added to HA.""" - # Sensors should also register callbacks to HA when their state changes - self._base.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._base.remove_callback(self.async_write_ha_state) - - -# This is another sensor, but more simple compared to the battery above. See the -# comments above for how each field works. -class IlluminanceSensor(SensorBase): - """Representation of a Sensor.""" - - device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = "lx" - - def __init__(self, buttonplus_base=ButtonPlusBase): - """Initialize the sensor.""" - super().__init__(buttonplus_base) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._base.button_plus_base_id}_illuminance" - - # The name of the entity - self._attr_name = f"{self._base.name} Illuminance" - - @property - def state(self): - """Return the state of the sensor.""" - return self._base.illuminance diff --git a/custom_components/button_plus/switch.py b/custom_components/button_plus/switch.py deleted file mode 100644 index 0294345..0000000 --- a/custom_components/button_plus/switch.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Platform for switch integration.""" - -from __future__ import annotations - -import logging - -from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .button_plus_api.model import ConnectorEnum -from . import ButtonPlusHub - -from .const import DOMAIN, MANUFACTURER - -_LOGGER = logging.getLogger(__name__) - -switches = [] - - -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] - - active_connectors = [ - connector.connector_id - for connector in hub.config.info.connectors - if connector.connector_type_enum() in [ConnectorEnum.DISPLAY, ConnectorEnum.BAR] - ] - - buttons = filter( - lambda b: b.button_id // 2 in active_connectors, hub.config.mqtt_buttons - ) - - for button in buttons: - # _LOGGER.debug(f"Creating switch with parameters: {button.button_id} {button.label} {hub.hub_id}") - switches.append(ButtonPlusSwitch(button.button_id, hub)) - - async_add_entities(switches) - - -class ButtonPlusSwitch(SwitchEntity): - def __init__(self, btn_id: int, hub: ButtonPlusHub): - self._is_on = False - self._hub_id = hub.hub_id - self._hub = hub - self._btn_id = btn_id - self._attr_unique_id = f"switch-{self._hub_id}-{btn_id}" - self.entity_id = f"switch.{self._hub_id}_{btn_id}" - self._attr_name = f"switch-{btn_id}" - self._name = f"Button {btn_id}" - self._device_class = SwitchDeviceClass.SWITCH - self._connector = hub.config.info.connectors[btn_id // 2] - - @property - def name(self) -> str: - """Return the display name of this switch.""" - return self._name - - @property - def device_info(self): - """Return information to link this entity with the correct device.""" - device_info = { - "via_device": (DOMAIN, self._hub.hub_id), - "manufacturer": MANUFACTURER, - } - - match self._connector.connector_type_enum(): - case ConnectorEnum.BAR: - device_info["name"] = ( - f"{self._hub._name} BAR Module {self._connector.connector_id}" - ) - device_info["connections"] = { - ("bar_module", self._connector.connector_id) - } - device_info["model"] = "BAR Module" - device_info["identifiers"] = { - ( - DOMAIN, - f"{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.connector_id}", - ) - } - case ConnectorEnum.DISPLAY: - device_info["name"] = f"{self._hub._name} Display Module" - device_info["connections"] = {("display_module", 1)} - device_info["model"] = "Display Module" - device_info["identifiers"] = { - (DOMAIN, f"{self._hub.hub_id}_{self._btn_id}_display_module") - } - - return device_info - - @property - def is_on(self): - """If the switch is currently on or off.""" - return self._is_on - - def turn_on(self, **kwargs): - """Turn the switch on.""" - self._is_on = True - - def turn_off(self, **kwargs): - """Turn the switch off.""" - self._is_on = False diff --git a/custom_components/button_plus/text.py b/custom_components/button_plus/text.py index 718bc18..42a1e5e 100644 --- a/custom_components/button_plus/text.py +++ b/custom_components/button_plus/text.py @@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from . import ButtonPlusHub -from .button_plus_api.model import ConnectorEnum +from .button_plus_api.model_interface import ConnectorType from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,19 +30,17 @@ async def async_setup_entry( text_entities = [] hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] - active_connectors = [ - connector.connector_id - for connector in hub.config.info.connectors - if connector.connector_type_enum() in [ConnectorEnum.DISPLAY, ConnectorEnum.BAR] - ] + active_connectors = hub.config.connectors_for( + ConnectorType.BAR, ConnectorType.DISPLAY + ) buttons = filter( - lambda b: b.button_id // 2 in active_connectors, hub.config.mqtt_buttons + lambda b: b.button_id // 2 in active_connectors, hub.config.buttons() ) for button in buttons: _LOGGER.debug( - f"Creating Texts with parameters: {button.button_id} {button.top_label} {button.label} {hub.hub_id}" + f"Creating Texts with parameters: {button.button_id()} {button.top_label} {button.label} {hub.hub_id}" ) label_entity = ButtonPlusLabel(button.button_id, hub, button.label) @@ -66,18 +64,20 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub, btn_label: str, text_type: s self.entity_id = f"text.{text_type}_{self._hub_id}_{btn_id}" self._attr_name = f"text-{text_type}-{btn_id}" self._attr_native_value = btn_label - self._connector = hub.config.info.connectors[btn_id // 2] - self.unique_id = self.unique_id_gen() + self._connector = hub.config.connectors_for( + ConnectorType.DISPLAY, ConnectorType.BAR + )[btn_id // 2] + self._unique_id = self.unique_id_gen() def unique_id_gen(self): - match self._connector.connector_type_enum(): - case ConnectorEnum.BAR: + match self._connector.connector_type(): + case ConnectorType.BAR: return self.unique_id_gen_bar() - case ConnectorEnum.DISPLAY: + case ConnectorType.DISPLAY: return self.unique_id_gen_display() def unique_id_gen_bar(self): - return f"text_{self._hub_id}_{self._btn_id}_bar_module_{self._connector.connector_id}_{self._text_type}" + return f"text_{self._hub_id}_{self._btn_id}_bar_module_{self._connector.identifier()}_{self._text_type}" def unique_id_gen_display(self): return f"text_{self._hub_id}_{self._btn_id}_display_module_{self._text_type}" @@ -98,17 +98,17 @@ def update(self) -> None: def device_info(self) -> DeviceInfo: """Return information to link this entity with the correct device.""" - identifiers: set[tuple[str, str]] = {} + identifiers: set[tuple[str, str]] = set() - match self._connector.connector_type_enum(): - case ConnectorEnum.BAR: + match self._connector.connector_type(): + case ConnectorType.BAR: identifiers = { ( DOMAIN, - f"{self._hub.hub_id} BAR Module {self._connector.connector_id}", + f"{self._hub.hub_id} BAR Module {self._connector.identifier()}", ) } - case ConnectorEnum.DISPLAY: + case ConnectorType.DISPLAY: identifiers = {(DOMAIN, f"{self._hub.hub_id} Display Module")} return DeviceInfo( diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py index 3c42a04..5fd5d87 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_07.py @@ -5,6 +5,7 @@ DeviceConfiguration, ) + def test_model_v1_07_from_to_json_should_be_same(): # Load the JSON file with open("resource/physicalconfig1.07.json") as file: @@ -19,11 +20,11 @@ def test_model_v1_07_from_to_json_should_be_same(): # Parse the JSON data into a DeviceConfiguration object new_json_data = json.loads(new_json_string) - new_device_config = DeviceConfiguration.from_dict(new_json_data) # Assert that the JSON data is the same assert json_data == new_json_data + @pytest.fixture def device_config(): # Load and parse the JSON file diff --git a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py index 034c5ff..3ca2afc 100644 --- a/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py +++ b/tests/custom_components/button_plus/button_plus_api/test_model_v1_12.py @@ -18,7 +18,6 @@ def test_model_v1_12_from_to_json_should_be_same(): # Parse the JSON data into a DeviceConfiguration object new_json_data = json.loads(new_json_string) - new_device_config = DeviceConfiguration.from_dict(new_json_data) # Assert that the JSON data is the same assert json_data == new_json_data From 99ab8212727a82a62921e86c833a55f917db6df3 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:40:37 +0200 Subject: [PATCH 11/29] Complete functionality, now let's test --- custom_components/button_plus/config_flow.py | 31 ++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index 7df217f..4293b12 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -13,8 +13,8 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_EMAIL, CONF_PASSWORD from homeassistant.helpers import aiohttp_client from homeassistant.helpers.network import get_url -from packaging import version +from . import ModelDetection from .button_plus_api.api_client import ApiClient from .button_plus_api.local_api_client import LocalApiClient from .button_plus_api.model_interface import ( @@ -97,12 +97,10 @@ async def async_step_manual(self, user_input=None): ip, aiohttp_client.async_get_clientsession(self.hass) ) json_config = await api_client.fetch_config() - device_config: DeviceConfiguration = DeviceConfiguration.from_json( - json_config - ) + device_config: DeviceConfiguration = ModelDetection.model_for_json(json.loads(json_config)) - self.add_broker_to_config(device_config) - self.add_topics_to_core(device_config) + self.set_broker(device_config) + self.add_topics(device_config) self.add_topics_to_buttons(device_config) await api_client.push_config(device_config) @@ -227,14 +225,15 @@ async def setup_api_client(self, user_input): return ApiClient(aiohttp_client.async_get_clientsession(self.hass), cookie) - def validate_ip(self, ip) -> bool: + @staticmethod + def validate_ip(ip) -> bool: try: ipaddress.IPv4Address(ip) return True except ValueError: return False - def add_broker_to_config( + def set_broker( self, device_config: DeviceConfiguration ) -> DeviceConfiguration: mqtt_entry = self.mqtt_entry @@ -252,8 +251,9 @@ def add_broker_to_config( device_config.set_broker(broker) return device_config - def add_topics_to_core( - self, device_config: DeviceConfiguration + @staticmethod + def add_topics( + device_config: DeviceConfiguration ) -> DeviceConfiguration: device_id = device_config.identifier() @@ -291,10 +291,11 @@ def add_topics_to_core( ) ) + @staticmethod def add_topics_to_buttons( - self, device_config: DeviceConfiguration + device_config: DeviceConfiguration ) -> DeviceConfiguration: - device_id = device_config.info.device_id + device_id = device_config.identifier() active_connectors = device_config.connectors_for( ConnectorType.BAR, ConnectorType.DISPLAY @@ -307,8 +308,8 @@ def add_topics_to_buttons( # This means the connector ID is equal to floor(button_id / 2). Button ID's start at 0! So: # button 0 and 1 are on connector 0, button 2 and 3 are on connector 1 for button in filter( - lambda button: button.button_id // 2 in active_connectors, - device_config.mqtt_buttons, + lambda btn: btn.button_id // 2 in active_connectors, + device_config.buttons(), ): # Create topics for button main label button.topics.append( @@ -353,7 +354,7 @@ def add_topics_to_buttons( return device_config def get_mqtt_endpoint(self, endpoint: str) -> str: - # Internal add-on is not reachable from the Button+ device so we use the hass ip + # Internal add-on is not reachable from the Button+ device, so we use the hass ip if endpoint in self.local_brokers: _LOGGER.debug( f"mqtt host is internal so use {self.hass.config.api.host} instead of {endpoint}" From b2cbeca40a02bb0dbfa6873448ee7cb5a5adc3fc Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:23:39 +0200 Subject: [PATCH 12/29] Testing.... --- .../button_plus_api/model_interface.py | 34 +------------ .../button_plus_api/model_v1_07.py | 15 +++--- .../button_plus/buttonplushub.py | 2 +- custom_components/button_plus/config_flow.py | 50 ++++--------------- custom_components/button_plus/number.py | 1 - 5 files changed, 22 insertions(+), 80 deletions(-) diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 7b8f1e0..11be7d7 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -21,36 +21,10 @@ def button_id(self) -> int: """Return the identifier of the connector.""" pass - -class MqttBroker: - url: str - port: int - username: str - password: str - - def __init__( - self, - url: str, - port: int, - username: str, - password: str, - ): - """Initialize the MQTT broker.""" - pass - - class Topic: topic: str event_type: EventType - def __init__( - self, - topic: str, - event_type: EventType, - ): - """Initialize the MQTT topic.""" - pass - class DeviceConfiguration: def firmware_version(self) -> Version: @@ -93,15 +67,11 @@ def buttons(self) -> List[Button]: """Return the available buttons.""" pass - def get_broker(self) -> MqttBroker: - """Return the MQTT broker.""" - pass - - def set_broker(self, broker: MqttBroker) -> None: + def set_broker(self, url: str, port: int, username: str, password: str) -> None: """Set the MQTT broker.""" pass - def add_topic(self, topic: Topic) -> None: + def add_topic(self, topic: str, event_type: EventType) -> None: """Set the MQTT topic.""" pass diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index fb3e742..b6c9eab 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -87,8 +87,8 @@ def to_dict(self) -> Dict[str, Any]: } -class Topic: - def __init__(self, broker_id: str, topic: str, payload: str, event_type: int): +class Topic(TopicInterface): + def __init__(self, broker_id: str, topic: str, payload: str, event_type: EventType): self.broker_id = broker_id self.topic = topic self.payload = payload @@ -388,13 +388,16 @@ def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: def buttons(self) -> List[Button]: return [button for button in self.mqtt_buttons] - def add_topic(self, topic: TopicInterface) -> None: - self.core.topics.add( + def set_broker(self, url: str, port: int, username: str, password: str) -> None: + self.mqtt_brokers.append(MqttBroker(url, port, username, password)) + + def add_topic(self, topic: str, event_type: EventType) -> None: + self.core.topics.append( Topic( broker_id="ha-button-plus", - topic=topic.topic, + topic=topic, payload="", - event_type=topic.event_type, + event_type=event_type, ) ) diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 4341da2..2813179 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -52,7 +52,7 @@ def __init__( suggested_area=self.config.location(), name=self._name, model=self.model, - sw_version=config.firmware_version(), + sw_version=str(config.firmware_version()), ) # 1 or none display module diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index 4293b12..a23b355 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -20,8 +20,6 @@ from .button_plus_api.model_interface import ( ConnectorType, DeviceConfiguration, - MqttBroker, - Topic, ) from .button_plus_api.event_type import EventType from .const import DOMAIN @@ -97,7 +95,7 @@ async def async_step_manual(self, user_input=None): ip, aiohttp_client.async_get_clientsession(self.hass) ) json_config = await api_client.fetch_config() - device_config: DeviceConfiguration = ModelDetection.model_for_json(json.loads(json_config)) + device_config: DeviceConfiguration = ModelDetection.model_for_json(json_config) self.set_broker(device_config) self.add_topics(device_config) @@ -241,16 +239,13 @@ def set_broker( broker_username = mqtt_entry.data.get("username", "") broker_password = mqtt_entry.data.get("password", "") - broker = MqttBroker( - url=f"mqtt://{self.broker_endpoint}/", - port=broker_port, - username=broker_username, - password=broker_password, + device_config.set_broker( + f"mqtt://{self.broker_endpoint}/", + broker_port, + broker_username, + broker_password, ) - device_config.set_broker(broker) - return device_config - @staticmethod def add_topics( device_config: DeviceConfiguration @@ -263,33 +258,10 @@ def add_topics( ) return - device_config.add_topic( - Topic( - topic=f"buttonplus/{device_id}/brightness/large", - event_type=EventType.BRIGHTNESS_LARGE_DISPLAY, - ) - ) - - device_config.add_topic( - Topic( - topic=f"buttonplus/{device_id}/brightness/mini", - event_type=EventType.BRIGHTNESS_MINI_DISPLAY, - ) - ) - - device_config.add_topic( - Topic( - topic=f"buttonplus/{device_id}/page/status", - event_type=EventType.PAGE_STATUS, - ) - ) - - device_config.add_topic( - Topic( - topic=f"buttonplus/{device_id}/page/set", - event_type=EventType.SET_PAGE, - ) - ) + device_config.add_topic(f"buttonplus/{device_id}/brightness/large", EventType.BRIGHTNESS_LARGE_DISPLAY) + device_config.add_topic(f"buttonplus/{device_id}/brightness/mini", EventType.BRIGHTNESS_MINI_DISPLAY) + device_config.add_topic(f"buttonplus/{device_id}/page/status", EventType.PAGE_STATUS) + device_config.add_topic(f"buttonplus/{device_id}/page/set", EventType.SET_PAGE) @staticmethod def add_topics_to_buttons( @@ -351,8 +323,6 @@ def add_topics_to_buttons( } ) - return device_config - def get_mqtt_endpoint(self, endpoint: str) -> str: # Internal add-on is not reachable from the Button+ device, so we use the hass ip if endpoint in self.local_brokers: diff --git a/custom_components/button_plus/number.py b/custom_components/button_plus/number.py index def12bf..d41b5f6 100644 --- a/custom_components/button_plus/number.py +++ b/custom_components/button_plus/number.py @@ -10,7 +10,6 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.mqtt import client as mqtt -from packaging import version from .button_plus_api.event_type import EventType from . import ButtonPlusHub From d1041fd55fc3dbf3c14dbd807d1aa663a648231f Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:44:23 +0200 Subject: [PATCH 13/29] Broke the button config flow, fixed --- custom_components/button_plus/button.py | 3 ++- .../button_plus/button_plus_api/model_v1_07.py | 6 +++--- custom_components/button_plus/config_flow.py | 11 ++++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index 22c03e2..4e67364 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -85,7 +85,7 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub): self._name = f"Button {btn_id}" self._device_class = ButtonDeviceClass.IDENTIFY self._connector: Connector = hub.config.connector_for(btn_id // 2) - self._attr_unique_id = self.unique_id_gen() + self.unique_id = self.unique_id_gen() def unique_id_gen(self): match self._connector.connector_type(): @@ -143,6 +143,7 @@ async def _async_press_action(self) -> None: await super()._async_press_action() async def _async_release_action(self) -> None: + # Not implemented pass @property diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index b6c9eab..a43b986 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -12,11 +12,11 @@ class Connector: def __init__(self, identifier: int, connector_type: int): - self.identifier = identifier + self._identifier = identifier self.connector_type = connector_type def identifier(self) -> int: - return self.identifier + return self._identifier def connector_type(self) -> ConnectorType: return ConnectorType(self.connector_type) @@ -373,7 +373,7 @@ def connector_for(self, *identifier: int) -> Connector: ( connector for connector in self.info.connectors - if connector.identifier == identifier + if connector.identifier() == identifier ), None, ) diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index a23b355..83e5b62 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -233,7 +233,7 @@ def validate_ip(ip) -> bool: def set_broker( self, device_config: DeviceConfiguration - ) -> DeviceConfiguration: + ): mqtt_entry = self.mqtt_entry broker_port = mqtt_entry.data.get("port") broker_username = mqtt_entry.data.get("username", "") @@ -266,12 +266,13 @@ def add_topics( @staticmethod def add_topics_to_buttons( device_config: DeviceConfiguration - ) -> DeviceConfiguration: + ): device_id = device_config.identifier() - active_connectors = device_config.connectors_for( - ConnectorType.BAR, ConnectorType.DISPLAY - ) + active_connectors = [ + connector.identifier() + for connector in device_config.connectors_for(ConnectorType.BAR, ConnectorType.DISPLAY) + ] # Each button should have a connector, so check if the buttons connector is present, else skip it # Each connector has two buttons, and the *implicit API contract* is that connectors create buttons in From 61b66d594c28f91762f7d36ed063538822f702c3 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:46:33 +0200 Subject: [PATCH 14/29] Ran ruff linter --- .pre-commit-config.yaml | 4 +-- .../button_plus_api/model_interface.py | 2 +- .../button_plus/buttonplushub.py | 4 +-- custom_components/button_plus/config_flow.py | 33 +++++++++++-------- custom_components/button_plus/light.py | 2 +- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5541a0e..2926b82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,10 +6,10 @@ minimum_pre_commit_version: '3.2.0' repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.5 + rev: v0.5.4 hooks: # Run the linter. - id: ruff - args: [ --fix ] + args: [ check --fix ] # Run the formatter. - id: ruff-format \ No newline at end of file diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 11be7d7..50cd6ce 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -21,6 +21,7 @@ def button_id(self) -> int: """Return the identifier of the connector.""" pass + class Topic: topic: str event_type: EventType @@ -87,4 +88,3 @@ def from_dict(json_data: any) -> "DeviceConfiguration": def to_json(self) -> str: """Serialize the DeviceConfiguration to a JSON string.""" pass - diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 2813179..9ffc324 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -38,8 +38,8 @@ def __init__( self.top_label_entities = {} self.brightness_entities = {} - self.manufacturer=MANUFACTURER - self.model="Base Module" + self.manufacturer = MANUFACTURER + self.model = "Base Module" device_registry = dr.async_get(hass) diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index 83e5b62..5438338 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -95,7 +95,9 @@ async def async_step_manual(self, user_input=None): ip, aiohttp_client.async_get_clientsession(self.hass) ) json_config = await api_client.fetch_config() - device_config: DeviceConfiguration = ModelDetection.model_for_json(json_config) + device_config: DeviceConfiguration = ModelDetection.model_for_json( + json_config + ) self.set_broker(device_config) self.add_topics(device_config) @@ -231,9 +233,7 @@ def validate_ip(ip) -> bool: except ValueError: return False - def set_broker( - self, device_config: DeviceConfiguration - ): + def set_broker(self, device_config: DeviceConfiguration): mqtt_entry = self.mqtt_entry broker_port = mqtt_entry.data.get("port") broker_username = mqtt_entry.data.get("username", "") @@ -247,9 +247,7 @@ def set_broker( ) @staticmethod - def add_topics( - device_config: DeviceConfiguration - ) -> DeviceConfiguration: + def add_topics(device_config: DeviceConfiguration) -> DeviceConfiguration: device_id = device_config.identifier() if device_config.supports_brightness() is False: @@ -258,20 +256,27 @@ def add_topics( ) return - device_config.add_topic(f"buttonplus/{device_id}/brightness/large", EventType.BRIGHTNESS_LARGE_DISPLAY) - device_config.add_topic(f"buttonplus/{device_id}/brightness/mini", EventType.BRIGHTNESS_MINI_DISPLAY) - device_config.add_topic(f"buttonplus/{device_id}/page/status", EventType.PAGE_STATUS) + device_config.add_topic( + f"buttonplus/{device_id}/brightness/large", + EventType.BRIGHTNESS_LARGE_DISPLAY, + ) + device_config.add_topic( + f"buttonplus/{device_id}/brightness/mini", EventType.BRIGHTNESS_MINI_DISPLAY + ) + device_config.add_topic( + f"buttonplus/{device_id}/page/status", EventType.PAGE_STATUS + ) device_config.add_topic(f"buttonplus/{device_id}/page/set", EventType.SET_PAGE) @staticmethod - def add_topics_to_buttons( - device_config: DeviceConfiguration - ): + def add_topics_to_buttons(device_config: DeviceConfiguration): device_id = device_config.identifier() active_connectors = [ connector.identifier() - for connector in device_config.connectors_for(ConnectorType.BAR, ConnectorType.DISPLAY) + for connector in device_config.connectors_for( + ConnectorType.BAR, ConnectorType.DISPLAY + ) ] # Each button should have a connector, so check if the buttons connector is present, else skip it diff --git a/custom_components/button_plus/light.py b/custom_components/button_plus/light.py index b0b9389..88606d0 100644 --- a/custom_components/button_plus/light.py +++ b/custom_components/button_plus/light.py @@ -39,7 +39,7 @@ async def async_setup_entry( class ButtonPlusLight(LightEntity): def __init__(self, btn_id: int, hub: ButtonPlusHub, light_type: str): - connectors=hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) + connectors = hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) self._btn_id = btn_id self._hub = hub self._hub_id = hub.hub_id From c5e32c30792597b7fc2c7974ee7e9bb990b59e0d Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:53:38 +0200 Subject: [PATCH 15/29] Whoopsie --- custom_components/button_plus/button_plus_api/model_v1_07.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index a43b986..9cf44e3 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -26,7 +26,7 @@ def from_dict(data: Dict[str, Any]) -> "Connector": return Connector(identifier=data["id"], connector_type=data["type"]) def to_dict(self) -> Dict[str, Any]: - return {"id": self.identifier, "type": self.connector_type} + return {"id": self._identifier, "type": self.connector_type} class Sensor: From 63d99e7d5d18327b5b49214e36e0e93b7e4f6e53 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:13:34 +0200 Subject: [PATCH 16/29] Put back switch.py, but does not seem to matter. Labels still gone --- .../button_plus_api/model_interface.py | 4 + .../button_plus_api/model_v1_07.py | 3 + custom_components/button_plus/switch.py | 108 ++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 custom_components/button_plus/switch.py diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 50cd6ce..4355c42 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -64,6 +64,10 @@ def connector_for(self, *identifier: int) -> Connector: """Return the connectors of the given type.""" pass + def connectors(self) -> List[Connector]: + """Return the connectors of the given type.""" + pass + def buttons(self) -> List[Button]: """Return the available buttons.""" pass diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index 9cf44e3..d1eebb7 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -385,6 +385,9 @@ def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: if connector.connector_type in [connector_type] ] + def connectors(self) -> List[Connector]: + return self.info.connectors + def buttons(self) -> List[Button]: return [button for button in self.mqtt_buttons] diff --git a/custom_components/button_plus/switch.py b/custom_components/button_plus/switch.py new file mode 100644 index 0000000..b95c60e --- /dev/null +++ b/custom_components/button_plus/switch.py @@ -0,0 +1,108 @@ +"""Platform for switch integration.""" + +from __future__ import annotations + +import logging + +from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .button_plus_api.connector_type import ConnectorType +from . import ButtonPlusHub + +from .const import DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + +switches = [] + + +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] + + active_connectors = [ + connector.identifier() + for connector in hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) + ] + + buttons = filter( + lambda b: b.button_id // 2 in active_connectors, hub.config.buttons() + ) + + for button in buttons: + # _LOGGER.debug(f"Creating switch with parameters: {button.button_id} {button.label} {hub.identifier}") + switches.append(ButtonPlusSwitch(button.button_id, hub)) + + async_add_entities(switches) + + +class ButtonPlusSwitch(SwitchEntity): + def __init__(self, btn_id: int, hub: ButtonPlusHub): + self._is_on = False + self._hub_id = hub.hub_id + self._hub = hub + self._btn_id = btn_id + self._attr_unique_id = f"switch-{self._hub_id}-{btn_id}" + self.entity_id = f"switch.{self._hub_id}_{btn_id}" + self._attr_name = f"switch-{btn_id}" + self._name = f"Button {btn_id}" + self._device_class = SwitchDeviceClass.SWITCH + self._connector = hub.config.connectors()[btn_id // 2] + + @property + def name(self) -> str: + """Return the display name of this switch.""" + return self._name + + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + device_info = { + "via_device": (DOMAIN, self._hub.hub_id), + "manufacturer": MANUFACTURER, + } + + match self._connector.connector_type(): + case ConnectorType.BAR: + device_info["name"] = ( + f"{self._hub.name} BAR Module {self._connector.identifier()}" + ) + device_info["connections"] = { + ("bar_module", self._connector.identifier()) + } + device_info["model"] = "BAR Module" + device_info["identifiers"] = { + ( + DOMAIN, + f"{self._hub.identifier}_{self._btn_id}_bar_module_{self._connector.identifier()}", + ) + } + case ConnectorType.DISPLAY: + device_info["name"] = f"{self._hub.name} Display Module" + device_info["connections"] = {("display_module", 1)} + device_info["model"] = "Display Module" + device_info["identifiers"] = { + (DOMAIN, f"{self._hub.identifier}_{self._btn_id}_display_module") + } + + return device_info + + @property + def is_on(self): + """If the switch is currently on or off.""" + return self._is_on + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._is_on = True + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._is_on = False From a4fb262305991e1dd949dc30a65f708284546055 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:26:03 +0200 Subject: [PATCH 17/29] cleanup and fixes --- .../button_plus_api/model_interface.py | 4 +-- .../button_plus_api/model_v1_07.py | 28 +++++++++---------- .../button_plus_api/model_v1_12.py | 14 +++++----- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 4355c42..989084c 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Dict, Any from packaging.version import Version @@ -85,7 +85,7 @@ def remove_topic_for(self, event_type: EventType) -> None: pass @staticmethod - def from_dict(json_data: any) -> "DeviceConfiguration": + def from_dict(data: Dict[str, Any]) -> "DeviceConfiguration": """Deserialize the DeviceConfiguration from a dictionary.""" pass diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index d1eebb7..a658f5a 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -7,13 +7,13 @@ from .JSONCustomEncoder import CustomEncoder from .connector_type import ConnectorType from .event_type import EventType -from .model_interface import Button, Topic as TopicInterface +from .model_interface import Button class Connector: def __init__(self, identifier: int, connector_type: int): self._identifier = identifier - self.connector_type = connector_type + self._connector_type = connector_type def identifier(self) -> int: return self._identifier @@ -26,7 +26,7 @@ def from_dict(data: Dict[str, Any]) -> "Connector": return Connector(identifier=data["id"], connector_type=data["type"]) def to_dict(self) -> Dict[str, Any]: - return {"id": self._identifier, "type": self.connector_type} + return {"id": self._identifier, "type": self._connector_type} class Sensor: @@ -70,7 +70,7 @@ def from_dict(data: Dict[str, Any]) -> "Info": firmware=data["firmware"], large_display=data["largedisplay"], connectors=[ - Connector.from_dict(data=connector) for connector in data["connectors"] + Connector.from_dict(connector) for connector in data["connectors"] ], sensors=[Sensor.from_dict(sensor) for sensor in data["sensors"]], ) @@ -87,7 +87,7 @@ def to_dict(self) -> Dict[str, Any]: } -class Topic(TopicInterface): +class Topic: def __init__(self, broker_id: str, topic: str, payload: str, event_type: EventType): self.broker_id = broker_id self.topic = topic @@ -111,7 +111,7 @@ def to_dict(self) -> Dict[str, Any]: "brokerid": self.broker_id, "topic": self.topic, "payload": self.payload, - "eventtype": self.event_type, + "eventtype": self.event_type., } @@ -299,7 +299,7 @@ def from_dict(data: Dict[str, Any]) -> "MqttBroker": def to_dict(self) -> Dict[str, Any]: return { - "brokerid": self.broker_id, + "brokerid": self.broker_id or "ha-button-plus", "url": self.url, "port": self.port, "wsport": self.ws_port, @@ -411,21 +411,21 @@ def remove_topic_for(self, event_type: EventType) -> None: ] @staticmethod - def from_dict(json_data: any) -> "DeviceConfiguration": + def from_dict(data: Dict[str, Any]) -> "DeviceConfiguration": return DeviceConfiguration( - info=Info.from_dict(json_data["info"]), - core=Core.from_dict(json_data["core"]), + info=Info.from_dict(data["info"]), + core=Core.from_dict(data["core"]), mqtt_buttons=[ - MqttButton.from_dict(button) for button in json_data["mqttbuttons"] + MqttButton.from_dict(button) for button in data["mqttbuttons"] ], mqtt_displays=[ - MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"] + MqttDisplay.from_dict(display) for display in data["mqttdisplays"] ], mqtt_brokers=[ - MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"] + MqttBroker.from_dict(broker) for broker in data["mqttbrokers"] ], mqtt_sensors=[ - MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] + MqttSensor.from_dict(sensor) for sensor in data["mqttsensors"] ], ) diff --git a/custom_components/button_plus/button_plus_api/model_v1_12.py b/custom_components/button_plus/button_plus_api/model_v1_12.py index 584c1fc..0e35cd9 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_12.py +++ b/custom_components/button_plus/button_plus_api/model_v1_12.py @@ -177,20 +177,20 @@ def __init__( # Same here, use the new classes for Info, Core and MqttDisplay @staticmethod - def from_dict(json_data: any) -> "DeviceConfiguration": + def from_dict(data: Dict[str, Any]) -> "DeviceConfiguration": return DeviceConfiguration( - info=Info.from_dict(json_data["info"]), - core=Core.from_dict(json_data["core"]), + info=Info.from_dict(data["info"]), + core=Core.from_dict(data["core"]), mqtt_buttons=[ - MqttButton.from_dict(button) for button in json_data["mqttbuttons"] + MqttButton.from_dict(button) for button in data["mqttbuttons"] ], mqtt_displays=[ - MqttDisplay.from_dict(display) for display in json_data["mqttdisplays"] + MqttDisplay.from_dict(display) for display in data["mqttdisplays"] ], mqtt_brokers=[ - MqttBroker.from_dict(broker) for broker in json_data["mqttbrokers"] + MqttBroker.from_dict(broker) for broker in data["mqttbrokers"] ], mqtt_sensors=[ - MqttSensor.from_dict(sensor) for sensor in json_data["mqttsensors"] + MqttSensor.from_dict(sensor) for sensor in data["mqttsensors"] ], ) From be4ab9190535945ec6392d90f6e53fbd881f81dd Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:42:58 +0200 Subject: [PATCH 18/29] More fixes --- custom_components/button_plus/button.py | 6 +-- .../button_plus_api/connector_type.py | 2 +- .../button_plus_api/model_interface.py | 15 +++++-- .../button_plus_api/model_v1_07.py | 27 +++++++----- .../button_plus/buttonplushub.py | 21 +++++---- custom_components/button_plus/config_flow.py | 44 +++++++------------ custom_components/button_plus/number.py | 1 + custom_components/button_plus/switch.py | 8 ++-- custom_components/button_plus/text.py | 11 +++-- 9 files changed, 71 insertions(+), 64 deletions(-) diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index 4e67364..f83e9cd 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -40,7 +40,7 @@ async def async_setup_entry( active_connectors = [ connector.identifier() for connector in hub.config.connectors_for( - ConnectorType.BAR, ConnectorType.DISPLAY + ConnectorType.DISPLAY, ConnectorType.BAR ) ] @@ -49,7 +49,7 @@ async def async_setup_entry( ) for button in buttons: - _LOGGER.debug( + _LOGGER.info( f"Creating button with parameters: {button.button_id} {button.label} {hub.hub_id}" ) entity = ButtonPlusButton(button.button_id, hub) @@ -84,7 +84,7 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub): self._attr_name = f"button-{btn_id}" self._name = f"Button {btn_id}" self._device_class = ButtonDeviceClass.IDENTIFY - self._connector: Connector = hub.config.connector_for(btn_id // 2) + self._connector: Connector = hub.config.connector_for(identifier=btn_id // 2) self.unique_id = self.unique_id_gen() def unique_id_gen(self): diff --git a/custom_components/button_plus/button_plus_api/connector_type.py b/custom_components/button_plus/button_plus_api/connector_type.py index e8306ee..095bc29 100644 --- a/custom_components/button_plus/button_plus_api/connector_type.py +++ b/custom_components/button_plus/button_plus_api/connector_type.py @@ -1,7 +1,7 @@ from enum import Enum -class ConnectorType(Enum): +class ConnectorType(int, Enum): NOT_CONNECTED = 0 BAR = 1 DISPLAY = 2 diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 989084c..96fbcf5 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -17,8 +17,11 @@ def connector_type(self) -> ConnectorType: class Button: - def button_id(self) -> int: - """Return the identifier of the connector.""" + button_id: int + label: str + + def add_topic(self, topic: str, event_type: EventType, payload: str = "") -> None: + """Set the MQTT topic.""" pass @@ -60,7 +63,7 @@ def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: """Return the connectors of the given type.""" pass - def connector_for(self, *identifier: int) -> Connector: + def connector_for(self, identifier: int) -> Connector: """Return the connectors of the given type.""" pass @@ -84,6 +87,12 @@ def remove_topic_for(self, event_type: EventType) -> None: """Remove the MQTT topic.""" pass + def topics(self) -> List[Topic]: + """ + :return: List of topics for the device + """ + pass + @staticmethod def from_dict(data: Dict[str, Any]) -> "DeviceConfiguration": """Deserialize the DeviceConfiguration from a dictionary.""" diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index a658f5a..1023ca3 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -1,8 +1,7 @@ import json from typing import List, Dict, Any -from packaging import version -from packaging.version import Version +from packaging.version import parse as parseVersion, Version from .JSONCustomEncoder import CustomEncoder from .connector_type import ConnectorType @@ -11,7 +10,7 @@ class Connector: - def __init__(self, identifier: int, connector_type: int): + def __init__(self, identifier: int, connector_type: ConnectorType): self._identifier = identifier self._connector_type = connector_type @@ -23,7 +22,9 @@ def connector_type(self) -> ConnectorType: @staticmethod def from_dict(data: Dict[str, Any]) -> "Connector": - return Connector(identifier=data["id"], connector_type=data["type"]) + return Connector( + identifier=data["id"], connector_type=ConnectorType(data["type"]) + ) def to_dict(self) -> Dict[str, Any]: return {"id": self._identifier, "type": self._connector_type} @@ -103,7 +104,7 @@ def from_dict(data: Dict[str, Any]) -> "Topic": broker_id=data["brokerid"], topic=data["topic"], payload=data["payload"], - event_type=data["eventtype"], + event_type=EventType(data["eventtype"]), ) def to_dict(self) -> Dict[str, Any]: @@ -111,7 +112,7 @@ def to_dict(self) -> Dict[str, Any]: "brokerid": self.broker_id, "topic": self.topic, "payload": self.payload, - "eventtype": self.event_type., + "eventtype": self.event_type, } @@ -192,6 +193,9 @@ def __init__( self.long_repeat = long_repeat self.topics = topics + def add_topic(self, topic: str, event_type: EventType, payload: str = "") -> None: + self.topics.append(Topic("ha-button-plus", topic, payload, event_type)) + @staticmethod def from_dict(data: Dict[str, Any]) -> "MqttButton": return MqttButton( @@ -289,7 +293,7 @@ def __init__( @staticmethod def from_dict(data: Dict[str, Any]) -> "MqttBroker": return MqttBroker( - broker_id=data["brokerid"], + broker_id=data["brokerid"] or "ha-button-plus", url=data["url"], port=data["port"], ws_port=data["wsport"], @@ -348,10 +352,10 @@ def __init__( self.mqtt_sensors = mqtt_sensors def firmware_version(self) -> Version: - return version.parse(self.info.firmware) + return parseVersion(self.info.firmware) def supports_brightness(self) -> bool: - return self.firmware_version() >= version.parse("1.11") + return self.firmware_version() >= parseVersion("1.11") def name(self) -> str: return self.core.name or self.info.device_id @@ -368,7 +372,7 @@ def mac_address(self) -> str: def location(self) -> str: return self.core.location - def connector_for(self, *identifier: int) -> Connector: + def connector_for(self, identifier: int) -> Connector: return next( ( connector @@ -410,6 +414,9 @@ def remove_topic_for(self, event_type: EventType) -> None: topic for topic in self.core.topics if topic.event_type != event_type ] + def topics(self) -> List[Topic]: + return self.core.topics + @staticmethod def from_dict(data: Dict[str, Any]) -> "DeviceConfiguration": return DeviceConfiguration( diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 9ffc324..d537616 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as DeviceRegistry from .button_plus_api.connector_type import ConnectorType from .button_plus_api.local_api_client import LocalApiClient @@ -41,12 +41,14 @@ def __init__( self.manufacturer = MANUFACTURER self.model = "Base Module" - device_registry = dr.async_get(hass) + device_registry = DeviceRegistry.async_get(hass) self.device = device_registry.async_get_or_create( configuration_url=f"http://{self.config.ip_address()}/", config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, self.config.mac_address())}, + connections={ + (DeviceRegistry.CONNECTION_NETWORK_MAC, self.config.mac_address()) + }, identifiers={(DOMAIN, self.config.identifier())}, manufacturer=self.manufacturer, suggested_area=self.config.location(), @@ -68,15 +70,15 @@ def __init__( for connector_id in self.connector_identifiers_for(ConnectorType.BAR) ] - @staticmethod def create_display_module( - hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub + self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub ) -> None: _LOGGER.debug(f"Add display module from '{hub.hub_id}'") - device_registry = dr.async_get(hass) + device_registry = DeviceRegistry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, + connections={(DOMAIN, hub.config.identifier())}, name=f"{hub.name} Display Module", model="Display Module", manufacturer=MANUFACTURER, @@ -87,8 +89,8 @@ def create_display_module( return device - @staticmethod def create_bar_module( + self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub, @@ -97,15 +99,16 @@ def create_bar_module( _LOGGER.debug( f"Add bar module from '{hub.hub_id}' with connector '{connector_id}'" ) - device_registry = dr.async_get(hass) + device_registry = DeviceRegistry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, + connections={(DOMAIN, hub.config.identifier())}, name=f"{hub._name} BAR Module {connector_id}", model="Bar module", manufacturer=MANUFACTURER, suggested_area=hub.config.location(), - identifiers={(DOMAIN, f"{hub.hub} BAR Module {connector_id}")}, + identifiers={(DOMAIN, f"{hub.hub_id} BAR Module {connector_id}")}, via_device=(DOMAIN, hub.hub_id), ) diff --git a/custom_components/button_plus/config_flow.py b/custom_components/button_plus/config_flow.py index 5438338..1e81566 100644 --- a/custom_components/button_plus/config_flow.py +++ b/custom_components/button_plus/config_flow.py @@ -275,7 +275,7 @@ def add_topics_to_buttons(device_config: DeviceConfiguration): active_connectors = [ connector.identifier() for connector in device_config.connectors_for( - ConnectorType.BAR, ConnectorType.DISPLAY + ConnectorType.DISPLAY, ConnectorType.BAR ) ] @@ -290,43 +290,29 @@ def add_topics_to_buttons(device_config: DeviceConfiguration): device_config.buttons(), ): # Create topics for button main label - button.topics.append( - { - "brokerid": "ha-button-plus", - "topic": f"buttonplus/{device_id}/button/{button.button_id}/label", - "payload": "", - "eventtype": EventType.LABEL, - } + button.add_topic( + f"buttonplus/{device_id}/button/{button.button_id}/label", + EventType.LABEL, ) # Create topics for button top label - button.topics.append( - { - "brokerid": "ha-button-plus", - "topic": f"buttonplus/{device_id}/button/{button.button_id}/top_label", - "payload": "", - "eventtype": EventType.TOPLABEL, - } + button.add_topic( + f"buttonplus/{device_id}/button/{button.button_id}/top_label", + EventType.TOPLABEL, ) # Create topics for button click - button.topics.append( - { - "brokerid": "ha-button-plus", - "topic": f"buttonplus/{device_id}/button/{button.button_id}/click", - "payload": "press", - "eventtype": EventType.CLICK, - } + button.add_topic( + f"buttonplus/{device_id}/button/{button.button_id}/click", + EventType.CLICK, + "press", ) # Create topics for button click - button.topics.append( - { - "brokerid": "ha-button-plus", - "topic": f"buttonplus/{device_id}/button/{button.button_id}/long_press", - "payload": "press", - "eventtype": EventType.LONG_PRESS, - } + button.add_topic( + f"buttonplus/{device_id}/button/{button.button_id}/long_press", + EventType.LONG_PRESS, + "press", ) def get_mqtt_endpoint(self, endpoint: str) -> str: diff --git a/custom_components/button_plus/number.py b/custom_components/button_plus/number.py index d41b5f6..f22b854 100644 --- a/custom_components/button_plus/number.py +++ b/custom_components/button_plus/number.py @@ -55,6 +55,7 @@ def __init__(self, hub: ButtonPlusHub, brightness_type: str, event_type: EventTy 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.topics() self._attr_icon = "mdi:television-ambient-light" self._attr_unique_id = f"brightness_{brightness_type}-{self._hub_id}" diff --git a/custom_components/button_plus/switch.py b/custom_components/button_plus/switch.py index b95c60e..c522df3 100644 --- a/custom_components/button_plus/switch.py +++ b/custom_components/button_plus/switch.py @@ -29,7 +29,9 @@ async def async_setup_entry( active_connectors = [ connector.identifier() - for connector in hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) + for connector in hub.config.connectors_for( + ConnectorType.DISPLAY, ConnectorType.BAR + ) ] buttons = filter( @@ -81,7 +83,7 @@ def device_info(self): device_info["identifiers"] = { ( DOMAIN, - f"{self._hub.identifier}_{self._btn_id}_bar_module_{self._connector.identifier()}", + f"{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.identifier()}", ) } case ConnectorType.DISPLAY: @@ -89,7 +91,7 @@ def device_info(self): device_info["connections"] = {("display_module", 1)} device_info["model"] = "Display Module" device_info["identifiers"] = { - (DOMAIN, f"{self._hub.identifier}_{self._btn_id}_display_module") + (DOMAIN, f"{self._hub.hub_id}_{self._btn_id}_display_module") } return device_info diff --git a/custom_components/button_plus/text.py b/custom_components/button_plus/text.py index 42a1e5e..3a7b49e 100644 --- a/custom_components/button_plus/text.py +++ b/custom_components/button_plus/text.py @@ -30,9 +30,10 @@ async def async_setup_entry( text_entities = [] hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] - active_connectors = hub.config.connectors_for( - ConnectorType.BAR, ConnectorType.DISPLAY - ) + active_connectors = [ + connector.identifier() + for connector in hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) + ] buttons = filter( lambda b: b.button_id // 2 in active_connectors, hub.config.buttons() @@ -64,9 +65,7 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub, btn_label: str, text_type: s self.entity_id = f"text.{text_type}_{self._hub_id}_{btn_id}" self._attr_name = f"text-{text_type}-{btn_id}" self._attr_native_value = btn_label - self._connector = hub.config.connectors_for( - ConnectorType.DISPLAY, ConnectorType.BAR - )[btn_id // 2] + self._connector = hub.config.connectors()[btn_id // 2] self._unique_id = self.unique_id_gen() def unique_id_gen(self): From 97b894602a4bc930c47506c7fadc2f55a0c1fb70 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:40:34 +0200 Subject: [PATCH 19/29] More fixes and helpful docker startup script with mosquitto broker --- README.md | 16 ++++++++++ compose.yml | 10 ++++++ custom_components/button_plus/button.py | 8 ++--- .../button_plus_api/model_interface.py | 1 + .../button_plus_api/model_v1_07.py | 7 +++-- .../button_plus/buttonplushub.py | 31 ++++++++++--------- custom_components/button_plus/device.py | 6 ++-- custom_components/button_plus/light.py | 6 ++-- custom_components/button_plus/text.py | 23 +++++++------- docker_compose.sh | 21 +++++++++++++ mosquitto_config/mosquitto.conf | 10 ++++++ 11 files changed, 100 insertions(+), 39 deletions(-) create mode 100755 docker_compose.sh create mode 100644 mosquitto_config/mosquitto.conf diff --git a/README.md b/README.md index 687ce82..406417f 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,19 @@ Currently, this project is in development and **highly** unstable. ## Documentation [Documentation](https://github.com/koenhendriks/ha-button-plus/wiki) can be found in the [wiki](https://github.com/koenhendriks/ha-button-plus/wiki) of this repo. + +## Testing +You can start a mosquitto broker and Home Assistant with this integration in it with Docker Compose. +Make sure Docker is installed and run the `docker_compose.sh` script. This script will completely reset +the docker image every time, making sure there is a clean environment. + +When you run the script, after a while: +- A browser window will open, if not, browse to http://localhost:8123. +- Setup Home Assistant with a user and location. +- Add the MQTT integration, use hostname `mosquitto` (no user/pw required). +- Add the Button+ integration to test, the MQTT broker should be prefilled. +- Your terminal will attach to the Home Assistant docker, showing you logs +- Detach with CTRL + C, the docker images will automatically be stopped. + +**Note:** This will allow you to setup a real Button+ device and see if setup works. +Actual communication with the device will not work unless the broker is reachable from Button+. \ No newline at end of file diff --git a/compose.yml b/compose.yml index f82b0da..9c7a758 100644 --- a/compose.yml +++ b/compose.yml @@ -1,4 +1,14 @@ services: + mosquitto: + image: eclipse-mosquitto + container_name: mosquitto + volumes: + - ./mosquitto_config:/mosquitto/config + ports: + - 1883:1883 + - 9001:9001 + stdin_open: true + tty: true homeassistant: container_name: homeassistant image: "ghcr.io/home-assistant/home-assistant:stable" diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index f83e9cd..b23d26d 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -39,9 +39,7 @@ async def async_setup_entry( active_connectors = [ connector.identifier() - for connector in hub.config.connectors_for( - ConnectorType.DISPLAY, ConnectorType.BAR - ) + for connector in hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) ] buttons = filter( @@ -50,7 +48,7 @@ async def async_setup_entry( for button in buttons: _LOGGER.info( - f"Creating button with parameters: {button.button_id} {button.label} {hub.hub_id}" + f"Creating button with parameters: {button.button_id} {button.top_label} {button.label} {hub.hub_id}" ) entity = ButtonPlusButton(button.button_id, hub) button_entities.append(entity) @@ -84,7 +82,7 @@ def __init__(self, btn_id: int, hub: ButtonPlusHub): self._attr_name = f"button-{btn_id}" self._name = f"Button {btn_id}" self._device_class = ButtonDeviceClass.IDENTIFY - self._connector: Connector = hub.config.connector_for(identifier=btn_id // 2) + self._connector: Connector = hub.config.connector_for(btn_id // 2) self.unique_id = self.unique_id_gen() def unique_id_gen(self): diff --git a/custom_components/button_plus/button_plus_api/model_interface.py b/custom_components/button_plus/button_plus_api/model_interface.py index 96fbcf5..d9fe14e 100644 --- a/custom_components/button_plus/button_plus_api/model_interface.py +++ b/custom_components/button_plus/button_plus_api/model_interface.py @@ -18,6 +18,7 @@ def connector_type(self) -> ConnectorType: class Button: button_id: int + top_label: str label: str def add_topic(self, topic: str, event_type: EventType, payload: str = "") -> None: diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index 1023ca3..c637ab1 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -1,4 +1,5 @@ import json +import logging from typing import List, Dict, Any from packaging.version import parse as parseVersion, Version @@ -8,6 +9,7 @@ from .event_type import EventType from .model_interface import Button +_LOGGER: logging.Logger = logging.getLogger(__package__) class Connector: def __init__(self, identifier: int, connector_type: ConnectorType): @@ -18,7 +20,7 @@ def identifier(self) -> int: return self._identifier def connector_type(self) -> ConnectorType: - return ConnectorType(self.connector_type) + return ConnectorType(self._connector_type) @staticmethod def from_dict(data: Dict[str, Any]) -> "Connector": @@ -383,10 +385,11 @@ def connector_for(self, identifier: int) -> Connector: ) def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: + _LOGGER.debug(f"Filter all {len(self.info.connectors)} connectors by type {connector_type}") return [ connector for connector in self.info.connectors - if connector.connector_type in [connector_type] + if connector.connector_type() in connector_type ] def connectors(self) -> List[Connector]: diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index d537616..71c210c 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers import device_registry as DeviceRegistry +from homeassistant.helpers.device_registry import DeviceEntry from .button_plus_api.connector_type import ConnectorType from .button_plus_api.local_api_client import LocalApiClient @@ -61,24 +62,27 @@ def __init__( self.display_module = next( ( self.create_display_module(hass, entry, self) - for _ in self.connector_identifiers_for(ConnectorType.DISPLAY) + for _ in ButtonPlusHub.connector_identifiers_for(ConnectorType.DISPLAY, self) ), None, ) self.display_bar = [ (connector_id, self.create_bar_module(hass, entry, self, connector_id)) - for connector_id in self.connector_identifiers_for(ConnectorType.BAR) + for connector_id in ButtonPlusHub.connector_identifiers_for(ConnectorType.BAR, self) ] + _LOGGER.info(f"Hub {self._name} created with {len(self.display_bar)} bar modules") + + @staticmethod def create_display_module( - self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub - ) -> None: - _LOGGER.debug(f"Add display module from '{hub.hub_id}'") + hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub + ) -> DeviceEntry: + _LOGGER.warning(f"Create display module from {hub.hub_id}") device_registry = DeviceRegistry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(DOMAIN, hub.config.identifier())}, + #connections={(DOMAIN, hub.config.identifier())}, name=f"{hub.name} Display Module", model="Display Module", manufacturer=MANUFACTURER, @@ -89,21 +93,19 @@ def create_display_module( return device + @staticmethod def create_bar_module( - self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub, connector_id: int, - ) -> None: - _LOGGER.debug( - f"Add bar module from '{hub.hub_id}' with connector '{connector_id}'" - ) + ) -> DeviceEntry: + _LOGGER.warning(f"Create bar module from {hub.hub_id} with connector '{connector_id}'") device_registry = DeviceRegistry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(DOMAIN, hub.config.identifier())}, + #connections={(DOMAIN, hub.config.identifier())}, name=f"{hub._name} BAR Module {connector_id}", model="Bar module", manufacturer=MANUFACTURER, @@ -114,10 +116,11 @@ def create_bar_module( return device - def connector_identifiers_for(self, connector_type: ConnectorType) -> List[int]: + @staticmethod + def connector_identifiers_for(connector_type: ConnectorType, hub: ButtonPlusHub) -> List[int]: return [ connector.identifier() - for connector in self.config.connectors_for(connector_type) + for connector in hub.config.connectors_for(connector_type) ] @property diff --git a/custom_components/button_plus/device.py b/custom_components/button_plus/device.py index 8b27e7f..84066c1 100644 --- a/custom_components/button_plus/device.py +++ b/custom_components/button_plus/device.py @@ -4,7 +4,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .buttonplushub import ButtonPlusHub +from .buttonplushub import ButtonPlusHub, _LOGGER from .const import DOMAIN, MANUFACTURER @@ -16,8 +16,8 @@ def __init__( hub: ButtonPlusHub, connector_id: int, ) -> None: + _LOGGER.info(f"Init BarModuleDevice '{hub.hub_id}' with connector '{connector_id}'") self.device_registry = dr.async_get(hass) - self.device = self.device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(DOMAIN, hub.config.identifier())}, @@ -34,8 +34,8 @@ class DisplayModuleDevice: def __init__( self, hass: HomeAssistant, entry: ConfigEntry, hub: ButtonPlusHub ) -> None: + _LOGGER.info(f"Init DisplayModuleDevice {hub.hub_id}") self.device_registry = dr.async_get(hass) - self.device = self.device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(DOMAIN, hub.config.identifier())}, diff --git a/custom_components/button_plus/light.py b/custom_components/button_plus/light.py index 88606d0..602cde8 100644 --- a/custom_components/button_plus/light.py +++ b/custom_components/button_plus/light.py @@ -31,8 +31,8 @@ async def async_setup_entry( for button in buttons: # _LOGGER.debug(f"Creating Lights with parameters: {button.button_id} {button.label} {hub.hub_id}") - lights.append(ButtonPlusWallLight(button.button_id(), hub)) - lights.append(ButtonPlusFrontLight(button.button_id(), hub)) + lights.append(ButtonPlusWallLight(button.button_id, hub)) + lights.append(ButtonPlusFrontLight(button.button_id, hub)) async_add_entities(lights) @@ -86,7 +86,7 @@ def device_info(self): "manufacturer": MANUFACTURER, } - match self._connector.connector_type: + match self._connector.connector_type(): case 1: device_info["name"] = f"BAR Module {self._connector.identifier()}" device_info["connections"] = { diff --git a/custom_components/button_plus/text.py b/custom_components/button_plus/text.py index 3a7b49e..7b1a830 100644 --- a/custom_components/button_plus/text.py +++ b/custom_components/button_plus/text.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, List from homeassistant.components.text import TextEntity from homeassistant.config_entries import ConfigEntry @@ -27,7 +27,7 @@ async def async_setup_entry( ) -> None: """Add text entity for each top and main label from config_entry in HA.""" - text_entities = [] + text_entities: List[ButtonPlusText] = [] hub: ButtonPlusHub = hass.data[DOMAIN][config_entry.entry_id] active_connectors = [ @@ -40,17 +40,16 @@ async def async_setup_entry( ) for button in buttons: - _LOGGER.debug( - f"Creating Texts with parameters: {button.button_id()} {button.top_label} {button.label} {hub.hub_id}" + _LOGGER.info( + f"Creating texts with parameters: {button.button_id} {button.top_label} {button.label} {hub.hub_id}" ) label_entity = ButtonPlusLabel(button.button_id, hub, button.label) - top_label_entity = ButtonPlusTopLabel(button.button_id, hub, button.top_label) - text_entities.append(label_entity) - text_entities.append(top_label_entity) - hub.add_label(button.button_id, label_entity) + + top_label_entity = ButtonPlusTopLabel(button.button_id, hub, button.top_label) + text_entities.append(top_label_entity) hub.add_top_label(button.button_id, top_label_entity) async_add_entities(text_entities) @@ -58,15 +57,15 @@ async def async_setup_entry( class ButtonPlusText(TextEntity): def __init__(self, btn_id: int, hub: ButtonPlusHub, btn_label: str, text_type: str): - self._btn_id = btn_id - self._hub = hub self._hub_id = hub.hub_id + self._hub = hub + self._btn_id = btn_id self._text_type = text_type self.entity_id = f"text.{text_type}_{self._hub_id}_{btn_id}" self._attr_name = f"text-{text_type}-{btn_id}" self._attr_native_value = btn_label - self._connector = hub.config.connectors()[btn_id // 2] - self._unique_id = self.unique_id_gen() + self._connector = hub.config.connector_for(btn_id // 2) + self.unique_id = self.unique_id_gen() def unique_id_gen(self): match self._connector.connector_type(): diff --git a/docker_compose.sh b/docker_compose.sh new file mode 100755 index 0000000..0fe96a3 --- /dev/null +++ b/docker_compose.sh @@ -0,0 +1,21 @@ +#! /bin/bash + +docker compose down + +rm -rf ha_config/* +rm -rf ha_config/.* + +cat > ha_config/configuration.yaml<< EOF +logger: + default: debug + logs: + custom_components.button_plus: debug +EOF + +docker compose up -d + +open "http://localhost:8123/" + +docker compose attach homeassistant + +docker compose down diff --git a/mosquitto_config/mosquitto.conf b/mosquitto_config/mosquitto.conf new file mode 100644 index 0000000..593d5c1 --- /dev/null +++ b/mosquitto_config/mosquitto.conf @@ -0,0 +1,10 @@ +listener 1883 +listener 9001 +protocol websockets +persistence true +persistence_file mosquitto.db +persistence_location /mosquitto/data/ + +#Authentication +allow_anonymous true + From f9fe0c9c83b5ae3720b70484b3ca03c2928f3e86 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:47:58 +0200 Subject: [PATCH 20/29] Remove Docker info from Readme.md --- README.md | 16 ---------------- docker_compose.sh | 21 ++++++++++++++++++--- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 406417f..687ce82 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,3 @@ Currently, this project is in development and **highly** unstable. ## Documentation [Documentation](https://github.com/koenhendriks/ha-button-plus/wiki) can be found in the [wiki](https://github.com/koenhendriks/ha-button-plus/wiki) of this repo. - -## Testing -You can start a mosquitto broker and Home Assistant with this integration in it with Docker Compose. -Make sure Docker is installed and run the `docker_compose.sh` script. This script will completely reset -the docker image every time, making sure there is a clean environment. - -When you run the script, after a while: -- A browser window will open, if not, browse to http://localhost:8123. -- Setup Home Assistant with a user and location. -- Add the MQTT integration, use hostname `mosquitto` (no user/pw required). -- Add the Button+ integration to test, the MQTT broker should be prefilled. -- Your terminal will attach to the Home Assistant docker, showing you logs -- Detach with CTRL + C, the docker images will automatically be stopped. - -**Note:** This will allow you to setup a real Button+ device and see if setup works. -Actual communication with the device will not work unless the broker is reachable from Button+. \ No newline at end of file diff --git a/docker_compose.sh b/docker_compose.sh index 0fe96a3..347e878 100755 --- a/docker_compose.sh +++ b/docker_compose.sh @@ -1,5 +1,21 @@ #! /bin/bash +## Testing +# You can start a mosquitto broker and Home Assistant with this integration in it with Docker Compose. +# Make sure Docker is installed and run the `docker_compose.sh` script. This script will completely reset +# the docker image every time, making sure there is a clean environment. +# +# When you run the script, after a while: +# - A browser window will open, if not, browse to http://localhost:8123. +# - Setup Home Assistant with a user and location. +# - Add the MQTT integration, use hostname `mosquitto` (no user/pw required). +# - Add the Button+ integration to test, the MQTT broker should be prefilled. +# - Your terminal will attach to the Home Assistant docker, showing you logs +# - Detach with CTRL + C, the docker images will automatically be stopped. +# +# **Note:** This will allow you to setup a real Button+ device and see if setup works. +# Actual communication with the device will not work unless the broker is reachable from Button+. + docker compose down rm -rf ha_config/* @@ -13,9 +29,8 @@ logger: EOF docker compose up -d - +sleep 1 open "http://localhost:8123/" - -docker compose attach homeassistant +docker compose attach homeassistant # This blocks until you detach with CTRL + C docker compose down From f7953395ea47aaa21c3dd6d8c0d4eb43d7ef6e06 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:58:47 +0200 Subject: [PATCH 21/29] Ruff linter and cleanup --- custom_components/button_plus/button.py | 4 +- .../button_plus_api/model_v1_07.py | 5 +- .../button_plus/buttonplushub.py | 24 ++-- custom_components/button_plus/device.py | 4 +- custom_components/button_plus/switch.py | 110 ------------------ custom_components/button_plus/text.py | 4 +- 6 files changed, 30 insertions(+), 121 deletions(-) delete mode 100644 custom_components/button_plus/switch.py diff --git a/custom_components/button_plus/button.py b/custom_components/button_plus/button.py index b23d26d..93f5b3c 100644 --- a/custom_components/button_plus/button.py +++ b/custom_components/button_plus/button.py @@ -39,7 +39,9 @@ async def async_setup_entry( active_connectors = [ connector.identifier() - for connector in hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) + for connector in hub.config.connectors_for( + ConnectorType.DISPLAY, ConnectorType.BAR + ) ] buttons = filter( diff --git a/custom_components/button_plus/button_plus_api/model_v1_07.py b/custom_components/button_plus/button_plus_api/model_v1_07.py index c637ab1..9506c92 100644 --- a/custom_components/button_plus/button_plus_api/model_v1_07.py +++ b/custom_components/button_plus/button_plus_api/model_v1_07.py @@ -11,6 +11,7 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) + class Connector: def __init__(self, identifier: int, connector_type: ConnectorType): self._identifier = identifier @@ -385,7 +386,9 @@ def connector_for(self, identifier: int) -> Connector: ) def connectors_for(self, *connector_type: ConnectorType) -> List[Connector]: - _LOGGER.debug(f"Filter all {len(self.info.connectors)} connectors by type {connector_type}") + _LOGGER.debug( + f"Filter all {len(self.info.connectors)} connectors by type {connector_type}" + ) return [ connector for connector in self.info.connectors diff --git a/custom_components/button_plus/buttonplushub.py b/custom_components/button_plus/buttonplushub.py index 71c210c..3d2f3c6 100644 --- a/custom_components/button_plus/buttonplushub.py +++ b/custom_components/button_plus/buttonplushub.py @@ -62,16 +62,22 @@ def __init__( self.display_module = next( ( self.create_display_module(hass, entry, self) - for _ in ButtonPlusHub.connector_identifiers_for(ConnectorType.DISPLAY, self) + for _ in ButtonPlusHub.connector_identifiers_for( + ConnectorType.DISPLAY, self + ) ), None, ) self.display_bar = [ (connector_id, self.create_bar_module(hass, entry, self, connector_id)) - for connector_id in ButtonPlusHub.connector_identifiers_for(ConnectorType.BAR, self) + for connector_id in ButtonPlusHub.connector_identifiers_for( + ConnectorType.BAR, self + ) ] - _LOGGER.info(f"Hub {self._name} created with {len(self.display_bar)} bar modules") + _LOGGER.info( + f"Hub {self._name} created with {len(self.display_bar)} bar modules" + ) @staticmethod def create_display_module( @@ -82,7 +88,7 @@ def create_display_module( device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - #connections={(DOMAIN, hub.config.identifier())}, + # connections={(DOMAIN, hub.config.identifier())}, name=f"{hub.name} Display Module", model="Display Module", manufacturer=MANUFACTURER, @@ -100,12 +106,14 @@ def create_bar_module( hub: ButtonPlusHub, connector_id: int, ) -> DeviceEntry: - _LOGGER.warning(f"Create bar module from {hub.hub_id} with connector '{connector_id}'") + _LOGGER.warning( + f"Create bar module from {hub.hub_id} with connector '{connector_id}'" + ) device_registry = DeviceRegistry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - #connections={(DOMAIN, hub.config.identifier())}, + # connections={(DOMAIN, hub.config.identifier())}, name=f"{hub._name} BAR Module {connector_id}", model="Bar module", manufacturer=MANUFACTURER, @@ -117,7 +125,9 @@ def create_bar_module( return device @staticmethod - def connector_identifiers_for(connector_type: ConnectorType, hub: ButtonPlusHub) -> List[int]: + def connector_identifiers_for( + connector_type: ConnectorType, hub: ButtonPlusHub + ) -> List[int]: return [ connector.identifier() for connector in hub.config.connectors_for(connector_type) diff --git a/custom_components/button_plus/device.py b/custom_components/button_plus/device.py index 84066c1..9c88b48 100644 --- a/custom_components/button_plus/device.py +++ b/custom_components/button_plus/device.py @@ -16,7 +16,9 @@ def __init__( hub: ButtonPlusHub, connector_id: int, ) -> None: - _LOGGER.info(f"Init BarModuleDevice '{hub.hub_id}' with connector '{connector_id}'") + _LOGGER.info( + f"Init BarModuleDevice '{hub.hub_id}' with connector '{connector_id}'" + ) self.device_registry = dr.async_get(hass) self.device = self.device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/custom_components/button_plus/switch.py b/custom_components/button_plus/switch.py deleted file mode 100644 index c522df3..0000000 --- a/custom_components/button_plus/switch.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Platform for switch integration.""" - -from __future__ import annotations - -import logging - -from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .button_plus_api.connector_type import ConnectorType -from . import ButtonPlusHub - -from .const import DOMAIN, MANUFACTURER - -_LOGGER = logging.getLogger(__name__) - -switches = [] - - -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] - - active_connectors = [ - connector.identifier() - for connector in hub.config.connectors_for( - ConnectorType.DISPLAY, ConnectorType.BAR - ) - ] - - buttons = filter( - lambda b: b.button_id // 2 in active_connectors, hub.config.buttons() - ) - - for button in buttons: - # _LOGGER.debug(f"Creating switch with parameters: {button.button_id} {button.label} {hub.identifier}") - switches.append(ButtonPlusSwitch(button.button_id, hub)) - - async_add_entities(switches) - - -class ButtonPlusSwitch(SwitchEntity): - def __init__(self, btn_id: int, hub: ButtonPlusHub): - self._is_on = False - self._hub_id = hub.hub_id - self._hub = hub - self._btn_id = btn_id - self._attr_unique_id = f"switch-{self._hub_id}-{btn_id}" - self.entity_id = f"switch.{self._hub_id}_{btn_id}" - self._attr_name = f"switch-{btn_id}" - self._name = f"Button {btn_id}" - self._device_class = SwitchDeviceClass.SWITCH - self._connector = hub.config.connectors()[btn_id // 2] - - @property - def name(self) -> str: - """Return the display name of this switch.""" - return self._name - - @property - def device_info(self): - """Return information to link this entity with the correct device.""" - device_info = { - "via_device": (DOMAIN, self._hub.hub_id), - "manufacturer": MANUFACTURER, - } - - match self._connector.connector_type(): - case ConnectorType.BAR: - device_info["name"] = ( - f"{self._hub.name} BAR Module {self._connector.identifier()}" - ) - device_info["connections"] = { - ("bar_module", self._connector.identifier()) - } - device_info["model"] = "BAR Module" - device_info["identifiers"] = { - ( - DOMAIN, - f"{self._hub.hub_id}_{self._btn_id}_bar_module_{self._connector.identifier()}", - ) - } - case ConnectorType.DISPLAY: - device_info["name"] = f"{self._hub.name} Display Module" - device_info["connections"] = {("display_module", 1)} - device_info["model"] = "Display Module" - device_info["identifiers"] = { - (DOMAIN, f"{self._hub.hub_id}_{self._btn_id}_display_module") - } - - return device_info - - @property - def is_on(self): - """If the switch is currently on or off.""" - return self._is_on - - def turn_on(self, **kwargs): - """Turn the switch on.""" - self._is_on = True - - def turn_off(self, **kwargs): - """Turn the switch off.""" - self._is_on = False diff --git a/custom_components/button_plus/text.py b/custom_components/button_plus/text.py index 7b1a830..3ce7488 100644 --- a/custom_components/button_plus/text.py +++ b/custom_components/button_plus/text.py @@ -32,7 +32,9 @@ async def async_setup_entry( active_connectors = [ connector.identifier() - for connector in hub.config.connectors_for(ConnectorType.DISPLAY, ConnectorType.BAR) + for connector in hub.config.connectors_for( + ConnectorType.DISPLAY, ConnectorType.BAR + ) ] buttons = filter( From b36bab2fafc4345a1addbe91a8350b877b22fece Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:01:05 +0200 Subject: [PATCH 22/29] CI needs to know which package is the source package --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5d9c20f..795bc6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,9 @@ dependencies = [ "packaging" ] +[tool.setuptools] +py-modules = ["custom_components"] + [tool.pytest.ini_options] pythonpath = [ "." From 79848ab37fb93b46bcf6368b045f04a7916ec1ec Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:03:56 +0200 Subject: [PATCH 23/29] Run tests in CI --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d9e309d..1366026 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Hatch run: pipx install hatch - #- name: Run tests - # run: hatch run test:test + - name: Run tests + run: hatch test - name: Build dist run: hatch build From b756f213d028e0ff793d1a56366e6a581149d698 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:55:53 +0200 Subject: [PATCH 24/29] Home Assistant requires Python 3.11 or higher, use latest LTS 3.12 --- .github/workflows/build.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1366026..d81ba68 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,6 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' - name: Install Hatch run: pipx install hatch - name: Run tests From 8d68c8588694a88da8b715b5919aef122dfd0c51 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:57:53 +0200 Subject: [PATCH 25/29] Install dependencies before running tests --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d81ba68..75da212 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,6 +14,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' + - name: Install dependencies + run: pip install . - name: Install Hatch run: pipx install hatch - name: Run tests From 5750ac2e6d4734fd7d80766e839171e35e98cdc7 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:03:17 +0200 Subject: [PATCH 26/29] Enable workflow on own repo for testing --- .github/workflows/build.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 75da212..2b3c70c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,9 +2,8 @@ name: Build and test on: push: - branches: ["main"] pull_request: - branches: ["main"] + workflow_dispatch: jobs: build: From 541ce25b21964b886f8284d9592e7a39f9c41745 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:08:29 +0200 Subject: [PATCH 27/29] Missing setuptools package in requirements --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 795bc6e..5577a9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ dynamic = ["version"] dependencies = [ "homeassistant", + "setuptools", "pre-commit", "packaging" ] From f043a1e6a4f627b64f0d353c7834f0ad1678e3f5 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:18:08 +0200 Subject: [PATCH 28/29] Yes, GHA, I really want you to use Python 3.12. Also, cache dependencies --- .github/workflows/build.yaml | 3 ++- pyproject.toml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2b3c70c..bbc1ba2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.12.4' + cache: 'pip' # caching pip dependencies, speeds things up - name: Install dependencies run: pip install . - name: Install Hatch diff --git a/pyproject.toml b/pyproject.toml index 5577a9e..053c2e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "ha-button-plus" dynamic = ["version"] +requires-python = ">= 3.12" dependencies = [ "homeassistant", From b8e8863a4fbc574164a1543f1689a86bd564d211 Mon Sep 17 00:00:00 2001 From: Stijn Spijker <767645+scspijker@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:21:21 +0200 Subject: [PATCH 29/29] Works, so reset build.yaml triggers --- .github/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bbc1ba2..9dde72e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,8 +2,9 @@ name: Build and test on: push: + branches: ["main"] pull_request: - workflow_dispatch: + branches: ["main"] jobs: build: