oin/oin_thermostat/mqtt.py

204 lines
6.6 KiB
Python
Raw Normal View History

2024-12-07 15:22:08 +01:00
import json
import logging
2024-12-08 11:51:05 +01:00
from collections.abc import Callable
2024-12-09 16:12:38 +01:00
from typing import Any, TypedDict
2024-12-07 15:22:08 +01:00
import paho.mqtt.client as mqtt
2024-12-09 12:16:26 +01:00
from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode
2024-12-07 15:22:08 +01:00
from .screen import Screen
from .select import Selector
logger = logging.getLogger(__name__)
2024-12-09 16:12:38 +01:00
class HAOptions(TypedDict):
dev: dict[str, str]
o: dict[str, str]
availability_topic: str
state_topic: str
cmps: dict[str, dict[str, str]]
2024-12-07 15:22:08 +01:00
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"
2024-12-09 12:16:26 +01:00
self.client = mqtt.Client(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-09 16:12:38 +01:00
def ha_options(self) -> HAOptions:
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-09 12:16:26 +01:00
def connect(self) -> int:
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-09 12:16:26 +01:00
if host is None:
logger.error("Host not found in config.")
logger.error(f"\t{self.config}")
return 1
if not isinstance(port, int):
logger.warning(f"Invalid port config : <{port}> ; using port 1883.")
port = 1883
2024-12-08 11:51:05 +01:00
logger.debug(f"Connecting to <{host}> on port <{port}>.")
2024-12-09 12:16:26 +01:00
code = self.client.connect(host, port)
if code != 0:
logger.error(f"Could not connect to host <{host}> on port <{port}>.")
return 1
2024-12-07 15:22:08 +01:00
2024-12-09 12:16:26 +01:00
code = self.subscribe(entity_topic(self.entity), self.state_update)
if code != 0:
return 1
code = self.subscribe(
2024-12-08 11:51:05 +01:00
[entity_topic(entity) for entity in self.secondary_entities],
self.secondary_state_update,
)
2024-12-09 12:16:26 +01:00
if code != 0:
return 1
2024-12-07 15:22:08 +01:00
2024-12-09 12:16:26 +01:00
m_info = self.publish_json(
"homeassistant/device/oin/config", self.ha_options, retain=True
)
m_info.wait_for_publish(60)
if not m_info.is_published():
logger.error("Config message timed out")
return 1
m_info = self.publish(self.availability_topic, "online", retain=True)
m_info.wait_for_publish(60)
if not m_info.is_published():
logger.error("Availability message timed out")
return 1
2024-12-09 16:18:19 +01:00
logger.info("Connected to Home Assistant.")
2024-12-09 12:16:26 +01:00
return 0
def publish(
self, topic: str, data: str, retain: bool = False
) -> mqtt.MQTTMessageInfo:
logger.debug(f"Sending message on topic <{topic}>.")
return self.client.publish(topic, data, retain=retain)
def publish_json(
self, topic: str, data: Any, retain: bool = False
) -> mqtt.MQTTMessageInfo:
return self.publish(topic, json.dumps(data), retain)
def subscribe(
self,
topic: str | list[str],
callback: Callable[[mqtt.Client, Any, mqtt.MQTTMessage], None],
) -> MQTTErrorCode:
2024-12-08 11:51:05 +01:00
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}>.")
2024-12-09 12:16:26 +01:00
return code
2024-12-07 15:22:08 +01:00
2024-12-09 12:16:26 +01:00
def loop(self) -> MQTTErrorCode:
2024-12-08 11:51:05 +01:00
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-09 16:18:19 +01:00
else:
logger.info("MQTT client loop successfully exited")
2024-12-09 12:16:26 +01:00
return 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-09 12:16:26 +01:00
data = message.payload.decode()
logger.debug(f"Message received on topic <{message.topic}>: {data}.")
2024-12-07 15:22:08 +01:00
subtopic = message.topic.rsplit("/", maxsplit=1)[1]
match subtopic:
case "current_temperature":
2024-12-09 12:16:26 +01:00
self.screen.value = json.loads(data)
2024-12-07 15:22:08 +01:00
case "temperature":
2024-12-09 12:16:26 +01:00
if (value := json.loads(data)) != self.selector.temperature:
2024-12-07 15:22:08 +01:00
self.screen.tmp_value = value
self.selector.temperature = value
case "hvac_action":
2024-12-09 12:16:26 +01:00
self.screen.mode = json.loads(data)
2024-12-07 15:22:08 +01:00
case "preset_modes":
2024-12-09 12:16:26 +01:00
if (value := json.loads(data)) != self.selector.preset_modes:
2024-12-07 15:22:08 +01:00
self.selector.preset_modes = value
case "preset_mode":
2024-12-09 12:16:26 +01:00
if (value := json.loads(data)) != self.selector.mode:
2024-12-07 15:22:08 +01:00
self.selector.mode = value
case "state":
2024-12-09 12:16:26 +01:00
match data:
2024-12-07 15:22:08 +01:00
case "heat":
self.selector.switch = True
case "off":
self.selector.switch = False
2024-12-09 13:25:00 +01:00
case other:
logger.warning(f"Unknown state received: <{other}>.")
case _:
pass
2024-12-07 15:22:08 +01:00
def secondary_state_update(
2024-12-08 11:51:05 +01:00
self, client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage
) -> None:
2024-12-09 12:16:26 +01:00
data = message.payload.decode()
logger.debug(f"Message received on topic <{message.topic}>: {data}.")
2024-12-07 15:22:08 +01:00
_, grp, ent, subtopic = message.topic.split("/")
idx = self.secondary_entities.index(f"{grp}.{ent}")
if subtopic == "state":
2024-12-09 12:16:26 +01:00
self.screen.secondary |= {idx: data}
2024-12-07 15:22:08 +01:00
2024-12-08 11:51:05 +01:00
def send_data(self, data: Any) -> mqtt.MQTTMessageInfo:
2024-12-09 12:16:26 +01:00
return self.publish_json(self.state_topic, data)
2024-12-07 15:22:08 +01:00
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}"