mirror of
https://github.com/fmtr/haco.git
synced 2026-10-25 18:40:15 -04:00
Refine data model/announce and subscriptions and add sandbox command/wrapper/callback/state workflow - and adapt for aiomqtt v2
This commit is contained in:
parent
a568e35d0b
commit
4d35286ff9
10 changed files with 136 additions and 39 deletions
|
|
@ -1,4 +1,4 @@
|
|||
CHANNEL="prod"
|
||||
CHANNEL="dev"
|
||||
#ORG="fmtr"
|
||||
PACKAGE="haco"
|
||||
PORT="06"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ class Capability(Base):
|
|||
|
||||
state: AnnounceTopicState | None = field(default_factory=AnnounceTopicState)
|
||||
command: AnnounceTopicCommand | None = field(default_factory=AnnounceTopicCommand)
|
||||
subscriptions: dict | None = field(default=None, metadata=dict(exclude=True), init=False)
|
||||
|
||||
parent: Control | None = None
|
||||
announce = None
|
||||
|
||||
|
|
@ -23,6 +25,9 @@ class Capability(Base):
|
|||
self.parent = control
|
||||
for topic in (t for t in (self.state, self.command) if t):
|
||||
topic.set_parent(self)
|
||||
|
||||
self.subscriptions = self.get_subscriptions()
|
||||
|
||||
self.announce = self.get_announce()
|
||||
|
||||
def get_announce(self) -> dict:
|
||||
|
|
@ -34,3 +39,9 @@ class Capability(Base):
|
|||
@property
|
||||
def control(self):
|
||||
return self.parent
|
||||
|
||||
def get_subscriptions(self) -> dict:
|
||||
data = {}
|
||||
for topic in (t for t in (self.state, self.command) if t):
|
||||
data |= topic.subscriptions
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -1,16 +1,41 @@
|
|||
from fmtr.tools import mqtt
|
||||
import aiomqtt
|
||||
|
||||
from fmtr.tools import mqtt, Constants
|
||||
from haco import constants
|
||||
|
||||
aiomqtt.Client
|
||||
|
||||
class ClientHaco(mqtt.Client):
|
||||
|
||||
def __init__(self, *args, device=None, will=None, client_id=None, **kwargs):
|
||||
def __init__(self, *args, device=None, will=None, identifier=None, **kwargs):
|
||||
self.will = will or mqtt.Will(topic=str(constants.TOPIC_AVAIL), payload="offline", retain=True, qos=1, )
|
||||
client_id = client_id or constants.CLIENT_ID
|
||||
identifier = identifier or constants.CLIENT_ID
|
||||
self.device = device
|
||||
self.device.set_parent(self)
|
||||
super().__init__(*args, will=self.will, client_id=client_id, **kwargs)
|
||||
super().__init__(*args, will=self.will, identifier=identifier, **kwargs)
|
||||
|
||||
@property
|
||||
def topic(self):
|
||||
return constants.TOPIC_CLIENT
|
||||
|
||||
async def handle(self):
|
||||
"""Listen for command messages from HA."""
|
||||
|
||||
# subs = control.subscriptions
|
||||
|
||||
for topic_sub in self.device.subscriptions.keys():
|
||||
print(f"Subscribing to {topic_sub}")
|
||||
await self.subscribe(topic_sub)
|
||||
|
||||
async for message in self.messages:
|
||||
payload = message.payload.decode()
|
||||
print(f"{message.topic.value}{Constants.ARROW}{payload}")
|
||||
topic_command = self.device.subscriptions[message.topic.value]
|
||||
echo_val = topic_command.wrap_back(message)
|
||||
echo_val
|
||||
|
||||
topic_state = topic_command.state
|
||||
await topic_state.wrap_back(echo_val)
|
||||
|
||||
# Echo back as new state
|
||||
# await self.publish(STATE_TOPIC, payload, retain=True)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,21 @@ class Climate(Control):
|
|||
def temperature_command(self, value):
|
||||
return value
|
||||
|
||||
def mode_command(self, value):
|
||||
return value
|
||||
|
||||
def fan_mode_command(self, value):
|
||||
return value
|
||||
|
||||
def fan_mode_state(self, value):
|
||||
return value
|
||||
|
||||
def power_command(self, value):
|
||||
return value
|
||||
|
||||
def power_state(self, value):
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_capabilities(cls):
|
||||
return [
|
||||
|
|
@ -31,4 +46,5 @@ class Climate(Control):
|
|||
Capability(name="target_humidity"),
|
||||
Capability(name="mode"),
|
||||
Capability(name="fan_mode"),
|
||||
Capability(name="power"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,17 +16,29 @@ class Control(Base):
|
|||
name: str
|
||||
platform: str
|
||||
|
||||
device: Device = field(default=None)
|
||||
device: Device = field(default=None, init=False)
|
||||
capabilities: None | list[Capability] = field(default=None, metadata=dict(exclude=True))
|
||||
unique_id: str | None = field(default=None, init=False)
|
||||
availability_topic: str | None = field(default=None, init=False)
|
||||
|
||||
announce: dict | None = field(default=None, metadata=dict(exclude=True))
|
||||
subscriptions: dict | None = field(default=None, metadata=dict(exclude=True), init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.capabilities = self.get_capabilities()
|
||||
|
||||
def set_parent(self, device):
|
||||
self.device = device
|
||||
|
||||
for cap in self.capabilities:
|
||||
cap.set_parent(self)
|
||||
|
||||
self.unique_id = self.get_unique_id()
|
||||
self.availability_topic = self.get_availability_topic()
|
||||
self.subscriptions = self.get_subscriptions()
|
||||
|
||||
|
||||
|
||||
self.announce = self.get_announce()
|
||||
|
||||
|
||||
|
|
@ -49,9 +61,7 @@ class Control(Base):
|
|||
Capability(name='default')
|
||||
]
|
||||
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
def get_unique_id(self) -> str:
|
||||
return f"{self.device.name_san}-{self.name_san}"
|
||||
|
||||
@property
|
||||
|
|
@ -66,12 +76,10 @@ class Control(Base):
|
|||
def announce_topic(self):
|
||||
return f"homeassistant/{self.platform}/{self.unique_id}/config"
|
||||
|
||||
@property
|
||||
def availability_topic(self) -> str:
|
||||
def get_availability_topic(self) -> str:
|
||||
return self.device.client.will.topic
|
||||
|
||||
@property
|
||||
def subscriptions(self) -> dict:
|
||||
def get_subscriptions(self) -> dict:
|
||||
|
||||
data = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ class Device(Base):
|
|||
identifiers: list[str] | None = field(default=None, init=False)
|
||||
|
||||
controls: List[Control] = field(default_factory=list, metadata=dict(exclude=True))
|
||||
parent: ClientHaco | None = field(metadata=dict(exclude=True))
|
||||
announce: dict | None = field(default=None, metadata=dict(exclude=True))
|
||||
|
||||
# def __post_init__(self):
|
||||
# self.identifiers=['ident']
|
||||
parent: ClientHaco | None = field(metadata=dict(exclude=True), init=False)
|
||||
announce: dict | None = field(default=None, metadata=dict(exclude=True), init=False)
|
||||
subscriptions: dict | None = field(default=None, metadata=dict(exclude=True), init=False)
|
||||
|
||||
|
||||
def set_parent(self, client):
|
||||
self.parent = client
|
||||
|
|
@ -35,6 +35,7 @@ class Device(Base):
|
|||
for control in self.controls:
|
||||
control.set_parent(self)
|
||||
self.announce = self.get_announce()
|
||||
self.subscriptions = self.get_subscriptions()
|
||||
|
||||
self
|
||||
|
||||
|
|
@ -57,8 +58,7 @@ class Device(Base):
|
|||
def client(self):
|
||||
return self.parent
|
||||
|
||||
@cached_property
|
||||
def subscriptions(self) -> dict:
|
||||
def get_subscriptions(self) -> dict:
|
||||
data = {}
|
||||
for control in self.controls:
|
||||
data |= control.subscriptions
|
||||
|
|
|
|||
44
haco/sb3.py
44
haco/sb3.py
|
|
@ -1,16 +1,44 @@
|
|||
import asyncio
|
||||
|
||||
import aiomqtt
|
||||
|
||||
from fmtr.tools.json_tools import to_json
|
||||
from haco.client import ClientHaco
|
||||
from haco.climate import Climate
|
||||
from haco.constants import MQTT_HOST
|
||||
from haco.control import Control
|
||||
from haco.device import Device
|
||||
|
||||
control = Control(name="test", platform="test")
|
||||
device = Device(name="test", parent=None, controls=[control])
|
||||
|
||||
client = ClientHaco(
|
||||
async def main():
|
||||
climate_a = Climate(name="Dev Climate A", fan_modes=['a', 'b', 'c'])
|
||||
climate_b = Climate(name="Dev Climate B", fan_modes=['a', 'b', 'c'])
|
||||
device = Device(name="Dev Device", controls=[climate_a, climate_b])
|
||||
|
||||
hostname=MQTT_HOST,
|
||||
client = ClientHaco(hostname=MQTT_HOST, device=device)
|
||||
|
||||
device=device
|
||||
)
|
||||
client
|
||||
|
||||
client
|
||||
while True:
|
||||
try:
|
||||
async with client:
|
||||
|
||||
for topic, data in device.announce.items():
|
||||
data_json = to_json(data)
|
||||
print(f'Announcing {topic} with {data_json}')
|
||||
await client.publish(topic, payload=data_json, retain=True)
|
||||
|
||||
# Online now
|
||||
await client.publish(client.will.topic, "online", retain=True)
|
||||
|
||||
await client.publish('haco/development/3012edb1a6d4-1ed9c3469373/dev-device/dev-climate-b/power/command', "OFF", retain=True)
|
||||
|
||||
# Wait for commands
|
||||
await client.handle()
|
||||
|
||||
except aiomqtt.MqttError as e:
|
||||
print(f"⚠️ MQTT disconnected: {e}; retrying in 5s")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ class AnnounceTopic:
|
|||
|
||||
parent: Capability | None = None
|
||||
announce: dict | None = field(default=None, metadata=dict(exclude=True))
|
||||
subscriptions: dict | None = field(default=None, metadata=dict(exclude=True), init=False)
|
||||
|
||||
def set_parent(self, control):
|
||||
self.parent = control
|
||||
def set_parent(self, capability: Capability):
|
||||
self.parent = capability
|
||||
self.announce = self.get_announce()
|
||||
self.subscriptions = self.get_subscriptions()
|
||||
|
||||
def fill(self, mask):
|
||||
return mask.format(**self.fills)
|
||||
|
|
@ -47,8 +49,7 @@ class AnnounceTopic:
|
|||
def callback_name(self):
|
||||
return f'{self.capability.name}_{self.IO}'
|
||||
|
||||
@property
|
||||
def subscriptions(self):
|
||||
def get_subscriptions(self):
|
||||
return {}
|
||||
|
||||
@cached_property
|
||||
|
|
@ -60,18 +61,27 @@ class AnnounceTopic:
|
|||
class AnnounceTopicState(AnnounceTopic):
|
||||
IO: ClassVar[str] = 'state'
|
||||
|
||||
@property
|
||||
def subscriptions(self):
|
||||
def get_subscriptions(self):
|
||||
return {}
|
||||
|
||||
async def wrap_back(self, value):
|
||||
method = getattr(self.capability.control, self.callback_name, None)
|
||||
value_raw = method(value)
|
||||
await self.capability.control.device.client.publish(self.topic, value_raw)
|
||||
return value_raw
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class AnnounceTopicCommand(AnnounceTopic):
|
||||
IO: ClassVar[str] = 'command'
|
||||
|
||||
@property
|
||||
def subscriptions(self):
|
||||
def state(self):
|
||||
return self.capability.state
|
||||
|
||||
def wrap_back(self, message):
|
||||
method = getattr(self.capability.control, self.callback_name, None)
|
||||
if not method:
|
||||
return {}
|
||||
return {self.topic: method}
|
||||
value = method(message.payload.decode('utf-8'))
|
||||
return value
|
||||
|
||||
def get_subscriptions(self):
|
||||
return {self.topic: self}
|
||||
|
|
|
|||
3
setup.py
3
setup.py
|
|
@ -4,8 +4,7 @@ setup = Setup(
|
|||
org=None,
|
||||
dependencies=dict(
|
||||
install=[
|
||||
'fmtr.tools[version.dev,logging,sets,yaml,debug,caching,api]',
|
||||
'aiomqtt<2'
|
||||
'fmtr.tools[version.dev,logging,sets,yaml,debug,caching,api,mqtt]',
|
||||
]
|
||||
),
|
||||
description='Home Assistant Control Objects',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue