Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Button+ V1.12 API and introduce compatibility layer #66

Merged
merged 29 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4314564
Add versioning to button_plus_api
scspijker Jul 23, 2024
7e0c766
Add model version detection
scspijker Jul 23, 2024
504c27d
Add dependency to pyproject.toml
scspijker Jul 23, 2024
f9f5bd2
Added Docker Compose file for easy local testing
scspijker Jul 23, 2024
9c02c82
More compatibility interface done
scspijker Jul 24, 2024
41e7097
Ruff fix and format
scspijker Jul 24, 2024
c711fea
Some fixes and cleanup before stopping for today
scspijker Jul 24, 2024
04308c4
More steps in the right direction, simplified model_V1_12
scspijker Jul 25, 2024
70029bc
Removed a LOT of code by reusing JSON serialisation from 1.07
scspijker Jul 26, 2024
80c3e32
Model complete, let's take a look at ConfigFlow
scspijker Jul 26, 2024
99ab821
Complete functionality, now let's test
scspijker Jul 26, 2024
b2cbeca
Testing....
scspijker Jul 26, 2024
d1041fd
Broke the button config flow, fixed
scspijker Jul 26, 2024
61b66d5
Ran ruff linter
scspijker Jul 26, 2024
c5e32c3
Whoopsie
scspijker Jul 26, 2024
63d99e7
Put back switch.py, but does not seem to matter. Labels still gone
scspijker Jul 26, 2024
a4fb262
cleanup and fixes
scspijker Jul 26, 2024
be4ab91
More fixes
scspijker Jul 26, 2024
97b8946
More fixes and helpful docker startup script with mosquitto broker
scspijker Aug 1, 2024
f9fe0c9
Remove Docker info from Readme.md
scspijker Aug 1, 2024
f795339
Ruff linter and cleanup
scspijker Aug 1, 2024
b36bab2
CI needs to know which package is the source package
scspijker Aug 1, 2024
79848ab
Run tests in CI
scspijker Aug 1, 2024
b756f21
Home Assistant requires Python 3.11 or higher, use latest LTS 3.12
scspijker Aug 1, 2024
8d68c85
Install dependencies before running tests
scspijker Aug 1, 2024
5750ac2
Enable workflow on own repo for testing
scspijker Aug 1, 2024
541ce25
Missing setuptools package in requirements
scspijker Aug 1, 2024
f043a1e
Yes, GHA, I really want you to use Python 3.12. Also, cache dependencies
scspijker Aug 1, 2024
b8e8863
Works, so reset build.yaml triggers
scspijker Aug 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 4 additions & 55 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ha_config

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -155,77 +157,24 @@ 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
.run
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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"
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"

7 changes: 5 additions & 2 deletions custom_components/button_plus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
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
Expand All @@ -23,7 +26,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")
)

Expand Down
38 changes: 20 additions & 18 deletions custom_components/button_plus/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -37,19 +37,20 @@ 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.DISPLAY, ConnectorType.BAR
)
]

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 button with parameters: {button.button_id} {button.label} {hub.hub_id}"
_LOGGER.info(
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)
Expand Down Expand Up @@ -83,18 +84,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._connector: 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_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"
Expand All @@ -112,17 +113,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(
Expand All @@ -142,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from enum import Enum


class ConnectorEnum(Enum):
class ConnectorType(int, Enum):
NOT_CONNECTED = 0
BAR = 1
DISPLAY = 2
19 changes: 19 additions & 0 deletions custom_components/button_plus/button_plus_api/model_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import json
from packaging.version import parse as parseSemver
from .model_interface import DeviceConfiguration as DeviceConfigurationInterface


class ModelDetection:
@staticmethod
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_dict(data)
else:
from .model_v1_07 import DeviceConfiguration

return DeviceConfiguration.from_dict(data)
104 changes: 104 additions & 0 deletions custom_components/button_plus/button_plus_api/model_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from typing import List, Dict, Any

from packaging.version import Version

from custom_components.button_plus.button_plus_api.event_type import EventType
from custom_components.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:
button_id: int
top_label: str
label: str

def add_topic(self, topic: str, event_type: EventType, payload: str = "") -> None:
"""Set the MQTT topic."""
pass


class Topic:
topic: str
event_type: EventType


class DeviceConfiguration:
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

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 connectors(self) -> List[Connector]:
"""Return the connectors of the given type."""
pass

def buttons(self) -> List[Button]:
"""Return the available buttons."""
pass

def set_broker(self, url: str, port: int, username: str, password: str) -> None:
"""Set the MQTT broker."""
pass

def add_topic(self, topic: str, event_type: EventType) -> None:
"""Set the MQTT topic."""
pass

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."""
pass

def to_json(self) -> str:
"""Serialize the DeviceConfiguration to a JSON string."""
pass
Loading
Loading