Skip to content

Commit

Permalink
Started moving HASS entity row tiles to using MQTT state stream
Browse files Browse the repository at this point in the history
  • Loading branch information
jinglemansweep committed Nov 2, 2023
1 parent 58fe162 commit 95c1594
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 80 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ packages = []

[tool.poetry.dependencies]
python = "^3.9"
jinja2 = "^3.1.2"
numpy = "^1.24.2"
python-dotenv = "^0.21.1"
pillow = "^9.4.0"
Expand Down
7 changes: 4 additions & 3 deletions wideboy/homeassistant/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ def _on_connect(self, client: mqtt.Client, userdata, flags, rc):
logger.info(
f"mqtt:connected connected={client.is_connected()} userdata={userdata} flags={flags} rc={str(rc)}"
)
self.subscribe(f"homeassistant/#", 0)
# DISABLED, could be too slow
# self.subscribe(f"{settings.mqtt.topic_prefix}/{self.device_id}/#", 0)

def _on_message(self, client: mqtt.Client, userdata, msg):
topic, payload = str(msg.topic), msg.payload.decode("utf-8")
logger.debug(
f"mqtt:message_received topic={topic} payload={payload} userdata={userdata}"
)
# logger.debug(
# f"mqtt:message_received topic={topic} payload={payload} userdata={userdata}"
# )
pygame.event.post(
Event(EVENT_MQTT_MESSAGE_RECEIVED, dict(topic=topic, payload=payload))
)
128 changes: 71 additions & 57 deletions wideboy/scenes/default/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,88 +115,104 @@ def setup(self):

hass_row_main_entities = [
dict(
entity_id="sensor.steps_louis",
icon=MaterialIcons.MDI_DIRECTIONS_WALK,
icon_color=Color(255, 0, 255, 255),
template="{{ states('sensor.steps_louis') }}",
template="{{ states['sensor.steps_louis'] }}",
watch_entities=["sensor.steps_louis"],
),
# active when public IP is home IP
dict(
entity_id="sensor.privacy_ip_info",
icon=MaterialIcons.MDI_LOCK,
icon_color=Color(255, 0, 0, 255),
template="VPN DOWN ({{ states('sensor.privacy_ip_info') }})",
cb_active=lambda state: state.state == settings.secrets.home_ip,
template="VPN DOWN ({{ states['sensor.privacy_ip_info'] }})",
cb_active=lambda states: states["sensor.privacy_ip_info"]
== settings.secrets.home_ip,
watch_entities=["sensor.privacy_ip_info"],
),
dict(
entity_id="sensor.transmission_down_speed",
icon=MaterialIcons.MDI_VPN_LOCK,
icon_color=Color(255, 255, 255, 255),
template="{{ states('sensor.transmission_down_speed') | int }}Mbps",
cb_active=lambda state: float(state.state) > 0,
template="{{ states['sensor.transmission_down_speed'] }}Mbps",
cb_active=lambda states: float(states["sensor.transmission_down_speed"])
> 0,
watch_entities=["sensor.transmission_down_speed"],
),
dict(
entity_id="sensor.ds920plus_volume_used",
icon=MaterialIcons.MDI_DNS,
icon_color=Color(255, 255, 0, 255),
template="{{ states('sensor.ds920plus_volume_used') }}%",
cb_active=lambda state: float(state.state) > 66.66,
template="{{ states['sensor.ds920plus_volume_used'] }}%",
cb_active=lambda states: float(states["sensor.ds920plus_volume_used"])
> 66.66,
watch_entities=["sensor.ds920plus_volume_used"],
),
dict(
entity_id="sensor.speedtest_download_average",
icon=MaterialIcons.MDI_DOWNLOAD,
icon_color=Color(0, 255, 0, 255),
template="{{ states('sensor.speedtest_download_average') | int }}Mbps",
cb_active=lambda state: float(state.state) < 600,
template="{{ states['sensor.speedtest_download_average'] }}Mbps",
cb_active=lambda states: float(
states["sensor.speedtest_download_average"]
)
< 600,
watch_entities=["sensor.speedtest_download_average"],
),
dict(
entity_id="sensor.speedtest_upload_average",
icon=MaterialIcons.MDI_UPLOAD,
icon_color=Color(255, 0, 0, 255),
template="{{ states('sensor.speedtest_upload_average') | int }}Mbps",
cb_active=lambda state: float(state.state) < 600,
template="{{ states['sensor.speedtest_upload_average'] }}Mbps",
cb_active=lambda states: float(
states["sensor.speedtest_upload_average"]
)
< 600,
watch_entities=["sensor.speedtest_upload_average"],
),
dict(
entity_id="sensor.speedtest_ping_average",
icon=MaterialIcons.MDI_WIFI,
icon_color=Color(0, 0, 255, 255),
template="{{ states('sensor.speedtest_ping_average') | int }}ms",
cb_active=lambda state: float(state.state) > 10,
),
dict(
entity_id="sensor.bin_collection_days",
icon=MaterialIcons.MDI_DELETE,
icon_color=Color(192, 192, 192, 255),
template="{{ state_attr('calendar.bin_collection', 'message') }}",
cb_active=lambda state: float(state.state) <= 1,
template="{{ states['sensor.speedtest_ping_average' ] }}ms",
cb_active=lambda states: float(states["sensor.speedtest_ping_average"])
> 10,
watch_entities=["sensor.speedtest_ping_average"],
),
# dict(
# icon=MaterialIcons.MDI_DELETE,
# icon_color=Color(192, 192, 192, 255),
# template="{{ state_attr('calendar.bin_collection', 'message') }}",
# cb_active=lambda state: float(state.state) <= 1,
# watch_entities=["sensor.bin_collection_days"],
# ),
dict(
entity_id="binary_sensor.back_door_contact_sensor_contact",
icon=MaterialIcons.MDI_DOOR,
icon_color=Color(255, 64, 64, 255),
template="Back",
cb_active=lambda state: state.state == "on",
cb_active=lambda states: states[
"binary_sensor.back_door_contact_sensor_contact"
]
== "on",
watch_entities=["binary_sensor.back_door_contact_sensor_contact"],
),
dict(
entity_id="binary_sensor.front_door_contact_sensor_contact",
icon=MaterialIcons.MDI_DOOR,
icon_color=Color(255, 64, 64, 255),
template="Front",
cb_active=lambda state: state.state == "on",
cb_active=lambda states: states[
"binary_sensor.front_door_contact_sensor_contact"
]
== "on",
watch_entities=["binary_sensor.front_door_contact_sensor_contact"],
),
dict(
entity_id="input_boolean.house_manual",
icon=MaterialIcons.MDI_TOGGLE_ON,
icon_color=Color(255, 0, 0, 255),
template="MANUAL",
cb_active=lambda state: state.state == "on",
cb_active=lambda states: states["input_boolean.house_manual"] == "on",
watch_entities=["input_boolean.house_manual"],
),
dict(
entity_id="switch.lounge_fans",
icon=MaterialIcons.MDI_AC_UNIT,
icon_color=Color(196, 196, 255, 255),
template="ON",
cb_active=lambda state: state.state == "on",
cb_active=lambda states: states["switch.lounge_fans"] == "on",
watch_entities=["switch.lounge_fans"],
),
]

Expand All @@ -208,32 +224,36 @@ def setup(self):
)
self.group.add(self.hass_row_main)


hass_row_power_entities = [
dict(
entity_id="sensor.octopus_energy_electricity_current_demand",
icon=MaterialIcons.MDI_BOLT,
icon_color=Color(192, 192, 192, 255),
template="{{ states('sensor.octopus_energy_electricity_current_demand') | int }}w",
template="{{ states['sensor.octopus_energy_electricity_current_demand'] }}w",
watch_entities=["sensor.octopus_energy_electricity_current_demand"],
),
dict(
entity_id="sensor.octopus_energy_electricity_current_rate",
icon=MaterialIcons.MDI_SYMBOL_AT,
icon_color=Color(192, 192, 192, 255),
template="£{{ '{:.2f}'.format(states('sensor.octopus_energy_electricity_current_rate') | float) }}",
template="£{{ '{:.2f}'.format(states['sensor.octopus_energy_electricity_current_rate'] | float) }}",
watch_entities=["sensor.octopus_energy_electricity_current_rate"],
),
dict(
entity_id="sensor.octopus_energy_electricity_current_demand",
icon=MaterialIcons.MDI_CURRENCY_DOLLAR,
icon_color=Color(255, 64, 64, 255),
template="£{{ '{:.2f}'.format(int(states('sensor.octopus_energy_electricity_current_demand')) / 1000 * float(states('sensor.octopus_energy_electricity_current_rate'))) }}",
template="£{{ (states['sensor.octopus_energy_electricity_current_demand'] | int / 1000) * (states['sensor.octopus_energy_electricity_current_rate'] | float) }}",
watch_entities=[
"sensor.octopus_energy_electricity_current_demand",
"sensor.octopus_energy_electricity_current_rate",
],
),
dict(
entity_id="sensor.octopus_energy_electricity_current_accumulative_cost",
icon=MaterialIcons.MDI_SCHEDULE,
icon_color=Color(255, 64, 64, 255),
template="£{{ '{:.2f}'.format(states('sensor.octopus_energy_electricity_current_accumulative_cost') | float) }}",
)
template="£{{ '{:.2f}'.format(states['sensor.octopus_energy_electricity_current_accumulative_cost'] | float) }}",
watch_entities=[
"sensor.octopus_energy_electricity_current_accumulative_cost"
],
),
]

self.hass_row_power = HomeAssistantEntityRowSprite(
Expand All @@ -246,41 +266,35 @@ def setup(self):

hass_row_battery_entities = [
dict(
entity_id="sensor.delta_2_max_downstairs_battery_level",
icon=MaterialIcons.MDI_BATTERY,
icon_color=Color(192, 192, 192, 255),
template="{{ states('sensor.delta_2_max_downstairs_battery_level') | int }}%",
template="{{ states['sensor.delta_2_max_downstairs_battery_level'] | int }}%",
),
dict(
entity_id="sensor.delta_2_max_downstairs_cycles",
icon=MaterialIcons.MDI_LOOP,
icon_color=Color(192, 192, 192, 255),
template="{{ states('sensor.delta_2_max_downstairs_cycles') | int }}"
template="{{ states['sensor.delta_2_max_downstairs_cycles'] | int }}",
),
dict(
entity_id="sensor.delta_2_max_downstairs_discharge_remaining_time",
icon=MaterialIcons.MDI_HOURGLASS,
icon_color=Color(255, 64, 64, 255),
template="{{ (states('sensor.delta_2_max_downstairs_discharge_remaining_time') | int) // 60 }}h{{ '{:2d}'.format((states('sensor.delta_2_max_downstairs_discharge_remaining_time') | int) % 60) }}m",
template="{{ states['sensor.delta_2_max_downstairs_discharge_remaining_time'] | int }}m",
cb_active=lambda state: float(state.state) > 0,
),
dict(
entity_id="sensor.delta_2_max_downstairs_charge_remaining_time",
icon=MaterialIcons.MDI_HOURGLASS,
icon_color=Color(64, 255, 64, 255),
template="{{ (states('sensor.delta_2_max_downstairs_charge_remaining_time') | int) // 60 }}h{{ '{:2d}'.format((states('sensor.delta_2_max_downstairs_charge_remaining_time') | int) % 60) }}m",
template="{{ ( states['sensor.delta_2_max_downstairs_discharge_remaining_time'] | int ) }}m",
cb_active=lambda state: float(state.state) > 0,
),
dict(
entity_id="sensor.delta_2_max_downstairs_ac_in_power",
icon=MaterialIcons.MDI_POWER,
icon_color=Color(255, 64, 64, 255),
template="{{ states('sensor.delta_2_max_downstairs_ac_in_power') | int }}w",
template="{{ states['sensor.delta_2_max_downstairs_ac_in_power'] | int }}w",
cb_active=lambda state: float(state.state) > 0,
)
),
]


self.hass_row_battery = HomeAssistantEntityRowSprite(
self,
Rect(512, 48, 128, 16),
Expand Down
77 changes: 57 additions & 20 deletions wideboy/sprites/homeassistant/entity_row.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import logging
from pygame import Clock, Color, Event, Rect, Surface, SRCALPHA
from typing import Optional, List, Dict
from wideboy.constants import EVENT_EPOCH_MINUTE, EVENT_EPOCH_SECOND
from jinja2 import Template
from typing import Optional, List, Set, Dict, Any
from wideboy.constants import (
EVENT_EPOCH_MINUTE,
EVENT_EPOCH_SECOND,
EVENT_MQTT_MESSAGE_RECEIVED,
)

# from wideboy.mqtt.homeassistant import HASS
from wideboy.scenes.base import BaseScene
Expand Down Expand Up @@ -30,13 +35,16 @@ def __init__(
) -> None:
super().__init__(scene, rect)
self.entities = entities
self.entity_states: Dict[str, Any] = dict()
self.entity_watches: Set[str] = set()
self.font_name = font_name
self.font_size = font_size
self.color_fg = color_fg
self.color_bg = color_bg
self.color_outline = color_outline
self.padding_right = padding_right
self.show_all = show_all
self.setup_watches()
self.render()

def update(
Expand All @@ -48,33 +56,65 @@ def update(
) -> None:
super().update(frame, clock, delta, events)
for event in events:
if event.type == EVENT_MQTT_MESSAGE_RECEIVED:
changed = self.parse_state_message(event.topic, event.payload)
if changed:
self.render()
if event.type == EVENT_EPOCH_MINUTE:
self.render()

def setup_watches(self) -> None:
for entity in self.entities:
self.entity_watches.update(entity.get("watch_entities", []))
for watched_entities in self.entity_watches:
self.entity_states[watched_entities] = ""

def parse_state_message(self, topic, payload) -> bool | None:
topic_exploded = topic.split("/")
if len(topic_exploded) < 3:
return None
device_class, device_id = topic_exploded[1:3]
entity_id = f"{device_class}.{device_id}"
changed = False
if entity_id in self.entity_watches:
if entity_id in self.entity_states:
changed = self.entity_states[entity_id] != payload
if changed:
logger.debug(
f"hass:entity_row watch_entity_change entity={entity_id} state={self.entity_states[entity_id]}"
)
else:
changed = True
self.entity_states[entity_id] = payload
return changed

def render(self) -> None:
w, h = 1, 2
surfaces = []
for entity in self.entities:
callback = entity.get("cb_active", lambda e: True)
template = entity.get("template", None)
try:
with self.scene.engine.hass.client as hass:
hass_state = hass.get_state(entity_id=entity["entity_id"])
# logger.debug(
# f"hass:entity entity_id={entity['entity_id']} state={hass_state.dict()}"
# )
active = self.show_all or callback(hass_state)
label = None
if not active:
continue
try:
active = self.show_all or callback(self.entity_states)
except Exception as e:
logger.warn(f"hass:entity_row callback error={e}")
active = False

if template:
logger.debug(f"hass:entity_row template={template}")
try:
with self.scene.engine.hass.client as hass:
label = hass.get_rendered_template(template)
jinja_template = Template(template)
label = jinja_template.render(states=self.entity_states)
logger.debug(
f"hass:entity_row template={template} label={label}"
)
except Exception as e:
logger.warn(f"failed to render template {template}", exc_info=e)
label = "ERR"
logger.warn(f"hass:entity_row template={template}", exc_info=e)
label = "?"

if not active:
continue

entity_surface = render_hass_tile(
icon_codepoint=entity.get("icon", None),
icon_color=entity.get("icon_color", None),
Expand All @@ -86,10 +126,7 @@ def render(self) -> None:
h = max(h, entity_surface.get_rect().height)
surfaces.append(entity_surface)
except Exception as ex:
logger.warn(
f"failed to render entity {entity['entity_id']}",
exc_info=ex
)
logger.warn(f"failed to render entity", exc_info=ex)
self.image = Surface((w, h - 1), SRCALPHA)
self.image.fill(self.color_bg)
x = 0
Expand Down

0 comments on commit 95c1594

Please sign in to comment.