2024-12-07 15:22:08 +01:00
|
|
|
import json
|
|
|
|
import logging
|
2024-12-08 11:51:05 +01:00
|
|
|
import sys
|
|
|
|
from collections.abc import Callable
|
|
|
|
from typing import Any
|
2024-12-07 15:22:08 +01:00
|
|
|
|
|
|
|
import paho.mqtt.client as mqtt
|
|
|
|
|
|
|
|
from .screen import Screen
|
|
|
|
from .select import Selector
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class HAClient:
|
2024-12-08 10:43:31 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
entity: str,
|
|
|
|
secondary_entities: list[str] = [],
|
2024-12-08 11:51:05 +01:00
|
|
|
mqtt_config: dict[str, str] = dict(),
|
2024-12-08 10:43:31 +01:00
|
|
|
) -> None:
|
2024-12-07 15:22:08 +01:00
|
|
|
self.entity = entity
|
|
|
|
self.secondary_entities = secondary_entities
|
2024-12-08 11:17:25 +01:00
|
|
|
self.config = mqtt_config
|
2024-12-07 15:22:08 +01:00
|
|
|
|
|
|
|
self.state_topic = "oin/state"
|
|
|
|
self.availability_topic = "oin/availability"
|
|
|
|
|
|
|
|
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
2024-12-08 11:17:25 +01:00
|
|
|
username = self.config.get("username", None)
|
|
|
|
logger.debug(f"Setting up MQTT with user <{username}>")
|
2024-12-08 10:43:31 +01:00
|
|
|
self.client.username_pw_set(
|
2024-12-08 11:17:25 +01:00
|
|
|
username=username,
|
2024-12-08 10:43:31 +01:00
|
|
|
password=self.config.get("password", None),
|
|
|
|
)
|
2024-12-07 15:22:08 +01:00
|
|
|
|
|
|
|
self.screen = Screen()
|
|
|
|
self.selector = Selector(self.send_data)
|
|
|
|
|
|
|
|
@property
|
2024-12-08 10:43:31 +01:00
|
|
|
def ha_options(self) -> dict[str, str | dict[str, str]]:
|
2024-12-07 15:22:08 +01:00
|
|
|
return {
|
|
|
|
"dev": {
|
|
|
|
"ids": "oin",
|
|
|
|
"name": "Oin",
|
|
|
|
},
|
|
|
|
"o": {
|
|
|
|
"name": "Oin",
|
|
|
|
},
|
|
|
|
"availability_topic": self.availability_topic,
|
|
|
|
"state_topic": self.state_topic,
|
|
|
|
"cmps": self.selector.ha_options,
|
|
|
|
}
|
|
|
|
|
2024-12-08 10:43:31 +01:00
|
|
|
def connect(self) -> None:
|
2024-12-07 15:22:08 +01:00
|
|
|
self.client.will_set(self.availability_topic, "offline", retain=True)
|
2024-12-08 11:17:25 +01:00
|
|
|
|
|
|
|
host = self.config.get("host")
|
|
|
|
port = self.config.get("port", 1883)
|
2024-12-08 11:51:05 +01:00
|
|
|
logger.debug(f"Connecting to <{host}> on port <{port}>.")
|
2024-12-08 11:17:25 +01:00
|
|
|
self.client.connect(host, port)
|
2024-12-07 15:22:08 +01:00
|
|
|
|
|
|
|
self.subscribe(entity_topic(self.entity), self.state_update)
|
2024-12-08 11:51:05 +01:00
|
|
|
self.subscribe(
|
|
|
|
[entity_topic(entity) for entity in self.secondary_entities],
|
|
|
|
self.secondary_state_update,
|
|
|
|
)
|
2024-12-07 15:22:08 +01:00
|
|
|
|
|
|
|
self.publish("homeassistant/device/oin/config", self.ha_options, retain=True)
|
|
|
|
self.client.publish(self.availability_topic, "online", retain=True)
|
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
def publish(self, topic: str, data: Any, **kwargs) -> mqtt.MQTTMessageInfo:
|
2024-12-07 15:22:08 +01:00
|
|
|
logger.debug(f"Sending message on topic <{topic}>: {json.dumps(data)}")
|
2024-12-08 11:51:05 +01:00
|
|
|
return self.client.publish(topic, json.dumps(data), **kwargs)
|
|
|
|
|
|
|
|
def subscribe(self, topic: str | list[str], callback: Callable) -> None:
|
|
|
|
logger.debug(f"Subscribing to <{topic}>.")
|
|
|
|
|
|
|
|
match topic:
|
|
|
|
case str():
|
|
|
|
self.client.message_callback_add(topic, callback)
|
|
|
|
code, _ = self.client.subscribe(topic)
|
|
|
|
case list():
|
|
|
|
for top in topic:
|
|
|
|
self.client.message_callback_add(top, callback)
|
|
|
|
code, _ = self.client.subscribe([(top, 0) for top in topic])
|
2024-12-07 15:22:08 +01:00
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
if code != 0:
|
|
|
|
logger.error(f"Failed subscribing to topic <{topic}> with code <{code}>.")
|
|
|
|
sys.exit(1)
|
2024-12-07 15:22:08 +01:00
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
def loop(self) -> mqtt.MQTTErrorCode:
|
|
|
|
logger.info("Starting MQTT client loop.")
|
|
|
|
code = self.client.loop_forever(retry_first_connection=True)
|
2024-12-07 15:22:08 +01:00
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
if code != 0:
|
|
|
|
logger.error("MQTT client loop failed with code <{code}>.")
|
2024-12-07 15:22:08 +01:00
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
def state_update(
|
|
|
|
self, client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
2024-12-07 15:22:08 +01:00
|
|
|
logger.debug(f"Message received on topic <{message.topic}>: {message.payload}.")
|
|
|
|
|
|
|
|
subtopic = message.topic.rsplit("/", maxsplit=1)[1]
|
|
|
|
|
|
|
|
match subtopic:
|
|
|
|
case "current_temperature":
|
|
|
|
self.screen.value = parse(message)
|
|
|
|
case "temperature":
|
|
|
|
if (value := parse(message)) != self.selector.temperature:
|
|
|
|
self.screen.tmp_value = value
|
|
|
|
self.selector.temperature = value
|
|
|
|
case "hvac_action":
|
|
|
|
self.screen.mode = parse(message)
|
|
|
|
case "preset_modes":
|
|
|
|
if (value := parse(message)) != self.selector.preset_modes:
|
|
|
|
self.selector.preset_modes = value
|
|
|
|
case "preset_mode":
|
|
|
|
if (value := parse(message)) != self.selector.mode:
|
|
|
|
self.selector.mode = value
|
|
|
|
case "state":
|
|
|
|
match message.payload.decode():
|
|
|
|
case "heat":
|
|
|
|
self.selector.switch = True
|
|
|
|
case "off":
|
|
|
|
self.selector.switch = False
|
|
|
|
|
|
|
|
def secondary_state_update(
|
2024-12-08 11:51:05 +01:00
|
|
|
self, client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
2024-12-07 15:22:08 +01:00
|
|
|
logger.debug(f"Message received on topic <{message.topic}>: {message.payload}.")
|
|
|
|
|
|
|
|
_, grp, ent, subtopic = message.topic.split("/")
|
|
|
|
idx = self.secondary_entities.index(f"{grp}.{ent}")
|
|
|
|
|
|
|
|
if subtopic == "state":
|
|
|
|
self.screen.secondary |= {idx: message.payload.decode()}
|
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
def send_data(self, data: Any) -> mqtt.MQTTMessageInfo:
|
|
|
|
return self.publish(self.state_topic, data)
|
2024-12-07 15:22:08 +01:00
|
|
|
|
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
def parse(message: mqtt.MQTTMessage) -> Any:
|
2024-12-07 15:22:08 +01:00
|
|
|
return json.loads(message.payload.decode())
|
|
|
|
|
|
|
|
|
2024-12-08 11:51:05 +01:00
|
|
|
def entity_topic(entity: str, subtopic: str = "#") -> str:
|
2024-12-07 15:22:08 +01:00
|
|
|
topic = entity.replace(".", "/")
|
|
|
|
return f"homeassistant/{topic}/{subtopic}"
|