842 lines
28 KiB
Python
842 lines
28 KiB
Python
"""HASP components module."""
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import jsonschema
|
|
|
|
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
|
|
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
|
from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN
|
|
from homeassistant.core import callback
|
|
from homeassistant.exceptions import TemplateError
|
|
from homeassistant.helpers import device_registry as dr, entity_registry
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
|
from homeassistant.helpers.event import TrackTemplate, async_track_template_result
|
|
from homeassistant.helpers.network import get_url
|
|
from homeassistant.helpers.reload import async_integration_yaml_config
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
|
from homeassistant.helpers.service import async_call_from_config
|
|
from homeassistant.util import slugify
|
|
import voluptuous as vol
|
|
|
|
from .common import HASP_IDLE_SCHEMA
|
|
from .const import (
|
|
ATTR_CONFIG_SUBMODULE,
|
|
ATTR_HEIGHT,
|
|
ATTR_IDLE,
|
|
ATTR_IMAGE,
|
|
ATTR_OBJECT,
|
|
ATTR_PAGE,
|
|
ATTR_PATH,
|
|
ATTR_COMMAND_KEYWORD,
|
|
ATTR_COMMAND_PARAMETERS,
|
|
ATTR_CONFIG_PARAMETERS,
|
|
ATTR_WIDTH,
|
|
CONF_COMPONENT,
|
|
CONF_EVENT,
|
|
CONF_HWID,
|
|
CONF_OBJECTS,
|
|
CONF_OBJID,
|
|
CONF_PAGES,
|
|
CONF_PAGES_PATH,
|
|
CONF_PLATE,
|
|
CONF_PROPERTIES,
|
|
CONF_TOPIC,
|
|
CONF_TRACK,
|
|
DATA_IMAGES,
|
|
DATA_LISTENER,
|
|
DISCOVERED_MANUFACTURER,
|
|
DISCOVERED_MODEL,
|
|
DISCOVERED_URL,
|
|
DISCOVERED_VERSION,
|
|
DOMAIN,
|
|
EVENT_HASP_PLATE_OFFLINE,
|
|
EVENT_HASP_PLATE_ONLINE,
|
|
HASP_EVENT,
|
|
HASP_EVENT_DOWN,
|
|
HASP_EVENT_RELEASE,
|
|
HASP_EVENT_UP,
|
|
HASP_EVENTS,
|
|
HASP_LWT,
|
|
HASP_NUM_PAGES,
|
|
HASP_ONLINE,
|
|
HASP_VAL,
|
|
MAJOR,
|
|
MINOR,
|
|
SERVICE_CLEAR_PAGE,
|
|
SERVICE_LOAD_PAGE,
|
|
SERVICE_PAGE_CHANGE,
|
|
SERVICE_PAGE_NEXT,
|
|
SERVICE_PAGE_PREV,
|
|
SERVICE_PUSH_IMAGE,
|
|
SERVICE_WAKEUP,
|
|
SERVICE_COMMAND,
|
|
SERVICE_CONFIG,
|
|
)
|
|
from .image import ImageServeView, image_to_rgb565
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORMS = [
|
|
LIGHT_DOMAIN,
|
|
SWITCH_DOMAIN,
|
|
BINARY_SENSOR_DOMAIN,
|
|
NUMBER_DOMAIN,
|
|
BUTTON_DOMAIN,
|
|
]
|
|
|
|
|
|
def hasp_object(value):
|
|
"""Validade HASP-LVGL object format."""
|
|
if re.match("p[0-9]+b[0-9]+", value):
|
|
return value
|
|
raise vol.Invalid("Not an HASP-LVGL object p#b#")
|
|
|
|
|
|
# Configuration YAML schemas
|
|
EVENT_SCHEMA = cv.schema_with_slug_keys([cv.SERVICE_SCHEMA])
|
|
|
|
PROPERTY_SCHEMA = cv.schema_with_slug_keys(cv.template)
|
|
|
|
OBJECT_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(CONF_OBJID): hasp_object,
|
|
vol.Optional(CONF_TRACK, default=None): vol.Any(cv.entity_id, None),
|
|
vol.Optional(CONF_PROPERTIES, default={}): PROPERTY_SCHEMA,
|
|
vol.Optional(CONF_EVENT, default={}): EVENT_SCHEMA,
|
|
}
|
|
)
|
|
|
|
PLATE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_OBJECTS): vol.All(cv.ensure_list, [OBJECT_SCHEMA]),
|
|
},
|
|
)
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{DOMAIN: vol.Schema({cv.slug: PLATE_SCHEMA})}, extra=vol.ALLOW_EXTRA
|
|
)
|
|
|
|
# JSON Messages from HASP schemas
|
|
HASP_VAL_SCHEMA = vol.Schema(
|
|
{vol.Required(HASP_VAL): vol.All(int, vol.Range(min=0, max=1))},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
HASP_EVENT_SCHEMA = vol.Schema(
|
|
{vol.Required(HASP_EVENT): vol.Any(*HASP_EVENTS)}, extra=vol.ALLOW_EXTRA
|
|
)
|
|
|
|
HASP_STATUSUPDATE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required("node"): cv.string,
|
|
vol.Required("version"): cv.string,
|
|
vol.Required("uptime"): int,
|
|
vol.Required("canUpdate"): cv.boolean,
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
HASP_LWT_SCHEMA = vol.Schema(vol.Any(*HASP_LWT))
|
|
|
|
HASP_PAGE_SCHEMA = vol.Schema(vol.All(vol.Coerce(int), vol.Range(min=0, max=12)))
|
|
|
|
PUSH_IMAGE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_IMAGE): vol.Any(cv.url, cv.isfile),
|
|
vol.Required(ATTR_OBJECT): hasp_object,
|
|
vol.Optional(ATTR_WIDTH): cv.positive_int,
|
|
vol.Optional(ATTR_HEIGHT): cv.positive_int,
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
async def async_setup(hass, config):
|
|
"""Set up the MQTT async example component."""
|
|
conf = config.get(DOMAIN)
|
|
|
|
if conf is None:
|
|
# We still depend in YAML so we must fail
|
|
_LOGGER.error(
|
|
"openHASP requires you to setup your plate objects in your YAML configuration."
|
|
)
|
|
return False
|
|
|
|
hass.data[DOMAIN] = {CONF_PLATE: {}}
|
|
|
|
component = hass.data[DOMAIN][CONF_COMPONENT] = EntityComponent(
|
|
_LOGGER, DOMAIN, hass
|
|
)
|
|
|
|
component.async_register_entity_service(SERVICE_WAKEUP, {}, "async_wakeup")
|
|
component.async_register_entity_service(
|
|
SERVICE_PAGE_NEXT, {}, "async_change_page_next"
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_PAGE_PREV, {}, "async_change_page_prev"
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_PAGE_CHANGE, {vol.Required(ATTR_PAGE): int}, "async_change_page"
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_LOAD_PAGE, {vol.Required(ATTR_PATH): cv.isfile}, "async_load_page"
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_CLEAR_PAGE, {vol.Optional(ATTR_PAGE): int}, "async_clearpage"
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_COMMAND,
|
|
{
|
|
vol.Required(ATTR_COMMAND_KEYWORD): cv.string,
|
|
vol.Optional(ATTR_COMMAND_PARAMETERS, default=""): cv.string,
|
|
},
|
|
"async_command_service",
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_CONFIG,
|
|
{
|
|
vol.Required(ATTR_CONFIG_SUBMODULE): cv.string,
|
|
vol.Required(ATTR_CONFIG_PARAMETERS): cv.string,
|
|
},
|
|
"async_config_service",
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_PUSH_IMAGE, PUSH_IMAGE_SCHEMA, "async_push_image"
|
|
)
|
|
|
|
hass.data[DOMAIN][DATA_IMAGES] = dict()
|
|
hass.http.register_view(ImageServeView)
|
|
|
|
return True
|
|
|
|
|
|
async def async_update_options(hass, entry):
|
|
"""Handle options update."""
|
|
_LOGGER.debug("Reloading")
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
|
|
|
|
async def async_setup_entry(hass, entry) -> bool:
|
|
"""Set up OpenHASP via a config entry."""
|
|
plate = entry.data[CONF_NAME]
|
|
_LOGGER.debug("Setup %s", plate)
|
|
|
|
hass_config = await async_integration_yaml_config(hass, DOMAIN)
|
|
|
|
if DOMAIN not in hass_config or slugify(plate) not in hass_config[DOMAIN]:
|
|
_LOGGER.error(
|
|
"No YAML configuration for %s, \
|
|
please create an entry under 'openhasp' with the slug: %s",
|
|
plate,
|
|
slugify(plate),
|
|
)
|
|
return False
|
|
|
|
config = hass_config[DOMAIN][slugify(plate)]
|
|
|
|
# Register Plate device
|
|
device_registry = dr.async_get(hass)
|
|
device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
identifiers={(DOMAIN, entry.data[CONF_HWID])},
|
|
manufacturer=entry.data[DISCOVERED_MANUFACTURER],
|
|
model=entry.data[DISCOVERED_MODEL],
|
|
sw_version=entry.data[DISCOVERED_VERSION],
|
|
configuration_url=entry.data.get(DISCOVERED_URL),
|
|
name=plate,
|
|
)
|
|
|
|
# Add entity to component
|
|
component = hass.data[DOMAIN][CONF_COMPONENT]
|
|
plate_entity = SwitchPlate(hass, config, entry)
|
|
await component.async_add_entities([plate_entity])
|
|
hass.data[DOMAIN][CONF_PLATE][plate] = plate_entity
|
|
|
|
for domain in PLATFORMS:
|
|
hass.async_create_task(
|
|
hass.config_entries.async_forward_entry_setup(entry, domain)
|
|
)
|
|
|
|
listener = entry.add_update_listener(async_update_options)
|
|
entry.async_on_unload(listener)
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(hass, entry):
|
|
"""Remove a config entry."""
|
|
plate = entry.data[CONF_NAME]
|
|
|
|
_LOGGER.debug("Unload entry for plate %s", plate)
|
|
|
|
for domain in PLATFORMS:
|
|
await hass.config_entries.async_forward_entry_unload(entry, domain)
|
|
|
|
component = hass.data[DOMAIN][CONF_COMPONENT]
|
|
await component.async_remove_entity(hass.data[DOMAIN][CONF_PLATE][plate].entity_id)
|
|
|
|
# Remove Plate entity
|
|
del hass.data[DOMAIN][CONF_PLATE][plate]
|
|
|
|
return True
|
|
|
|
async def async_remove_entry(hass, entry):
|
|
plate = entry.data[CONF_NAME]
|
|
|
|
# Only remove services if it is the last
|
|
if len(hass.data[DOMAIN][CONF_PLATE]) == 1:
|
|
_LOGGER.debug("removing services")
|
|
hass.services.async_remove(DOMAIN, SERVICE_WAKEUP)
|
|
hass.services.async_remove(DOMAIN, SERVICE_PAGE_NEXT)
|
|
hass.services.async_remove(DOMAIN, SERVICE_PAGE_PREV)
|
|
hass.services.async_remove(DOMAIN, SERVICE_PAGE_CHANGE)
|
|
hass.services.async_remove(DOMAIN, SERVICE_LOAD_PAGE)
|
|
hass.services.async_remove(DOMAIN, SERVICE_CLEAR_PAGE)
|
|
hass.services.async_remove(DOMAIN, SERVICE_COMMAND)
|
|
|
|
device_registry = dr.async_get(hass)
|
|
dev = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, entry.data[CONF_HWID])}
|
|
)
|
|
if entry.entry_id in dev.config_entries:
|
|
_LOGGER.debug("Removing device %s", dev)
|
|
device_registry.async_remove_device(dev.id)
|
|
|
|
# Component does not remove entity from entity_registry, so we must do it
|
|
registry = await entity_registry.async_get_registry(hass)
|
|
registry.async_remove(hass.data[DOMAIN][CONF_PLATE][plate].entity_id)
|
|
|
|
# pylint: disable=R0902
|
|
class SwitchPlate(RestoreEntity):
|
|
"""Representation of an openHASP Plate."""
|
|
|
|
def __init__(self, hass, config, entry):
|
|
"""Initialize a plate."""
|
|
super().__init__()
|
|
self._entry = entry
|
|
self._topic = entry.data[CONF_TOPIC]
|
|
self._pages_jsonl = entry.options.get(
|
|
CONF_PAGES_PATH, entry.data.get(CONF_PAGES_PATH)
|
|
)
|
|
|
|
self._objects = []
|
|
for obj in config[CONF_OBJECTS]:
|
|
new_obj = HASPObject(hass, self._topic, obj)
|
|
|
|
self._objects.append(new_obj)
|
|
self._statusupdate = {HASP_NUM_PAGES: entry.data[CONF_PAGES]}
|
|
self._available = False
|
|
self._page = 1
|
|
|
|
self._subscriptions = []
|
|
|
|
with open(
|
|
pathlib.Path(__file__).parent.joinpath("pages_schema.json"), "r"
|
|
) as schema_file:
|
|
self.json_schema = json.load(schema_file)
|
|
|
|
self._attr_unique_id = entry.data[CONF_HWID]
|
|
self._attr_name = entry.data[CONF_NAME]
|
|
self._attr_icon = "mdi:gesture-tap-box"
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the component."""
|
|
return self._page
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return if entity is available."""
|
|
return self._available
|
|
|
|
async def async_will_remove_from_hass(self):
|
|
"""Run before entity is removed."""
|
|
_LOGGER.debug("Remove plate %s", self._entry.data[CONF_NAME])
|
|
|
|
for obj in self._objects:
|
|
await obj.disable_object()
|
|
|
|
for subscription in self._subscriptions:
|
|
subscription()
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Run when entity about to be added."""
|
|
await super().async_added_to_hass()
|
|
|
|
state = await self.async_get_last_state()
|
|
if state and state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]:
|
|
self._page = int(state.state)
|
|
|
|
@callback
|
|
async def page_update_received(msg):
|
|
"""Process page state."""
|
|
try:
|
|
self._page = HASP_PAGE_SCHEMA(msg.payload)
|
|
_LOGGER.debug("Page changed to %s", self._page)
|
|
self.async_write_ha_state()
|
|
except vol.error.Invalid as err:
|
|
_LOGGER.error("%s in %s", err, msg.payload)
|
|
|
|
self._subscriptions.append(
|
|
await self.hass.components.mqtt.async_subscribe(
|
|
f"{self._topic}/state/page", page_update_received
|
|
)
|
|
)
|
|
|
|
@callback
|
|
async def statusupdate_message_received(msg):
|
|
"""Process statusupdate."""
|
|
|
|
try:
|
|
message = HASP_STATUSUPDATE_SCHEMA(json.loads(msg.payload))
|
|
|
|
major, minor, _ = message["version"].split(".")
|
|
if (major, minor) != (MAJOR, MINOR):
|
|
self.hass.components.persistent_notification.create(
|
|
f"You require firmware version {MAJOR}.{MINOR}.x \
|
|
in plate {self._entry.data[CONF_NAME]} \
|
|
for this component to work properly.\
|
|
<br>Some features will simply not work!",
|
|
title="openHASP Firmware mismatch",
|
|
notification_id="openhasp_firmware_notification",
|
|
)
|
|
_LOGGER.error(
|
|
"%s firmware mismatch %s <> %s",
|
|
self._entry.data[CONF_NAME],
|
|
(major, minor),
|
|
(MAJOR, MINOR),
|
|
)
|
|
self._available = True
|
|
self._statusupdate = message
|
|
|
|
self._page = message[ATTR_PAGE]
|
|
self.async_write_ha_state()
|
|
|
|
# Update Plate device information
|
|
device_registry = dr.async_get(self.hass)
|
|
device_registry.async_get_or_create(
|
|
config_entry_id=self._entry.entry_id,
|
|
identifiers={(DOMAIN, self._entry.data[CONF_HWID])},
|
|
manufacturer=self._entry.data[DISCOVERED_MANUFACTURER],
|
|
model=self._entry.data[DISCOVERED_MODEL],
|
|
configuration_url=self._entry.data.get(DISCOVERED_URL),
|
|
sw_version=message["version"],
|
|
name=self._entry.data[CONF_NAME],
|
|
)
|
|
|
|
except vol.error.Invalid as err:
|
|
_LOGGER.error("While processing status update: %s", err)
|
|
|
|
self._subscriptions.append(
|
|
await self.hass.components.mqtt.async_subscribe(
|
|
f"{self._topic}/state/statusupdate", statusupdate_message_received
|
|
)
|
|
)
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, f"{self._topic}/command", "statusupdate", qos=0, retain=False
|
|
)
|
|
|
|
@callback
|
|
async def idle_message_received(msg):
|
|
"""Process idle message."""
|
|
try:
|
|
self._statusupdate[ATTR_IDLE] = HASP_IDLE_SCHEMA(msg.payload)
|
|
self.async_write_ha_state()
|
|
except vol.error.Invalid as err:
|
|
_LOGGER.error("While processing idle message: %s", err)
|
|
|
|
self._subscriptions.append(
|
|
await self.hass.components.mqtt.async_subscribe(
|
|
f"{self._topic}/state/idle", idle_message_received
|
|
)
|
|
)
|
|
|
|
@callback
|
|
async def lwt_message_received(msg):
|
|
"""Process LWT."""
|
|
_LOGGER.debug("Received LWT = %s", msg.payload)
|
|
try:
|
|
message = HASP_LWT_SCHEMA(msg.payload)
|
|
|
|
if message == HASP_ONLINE:
|
|
self._available = True
|
|
self.hass.bus.async_fire(
|
|
EVENT_HASP_PLATE_ONLINE,
|
|
{CONF_PLATE: self._entry.data[CONF_HWID]},
|
|
)
|
|
if self._pages_jsonl:
|
|
await self.async_load_page(self._pages_jsonl)
|
|
else:
|
|
await self.refresh()
|
|
|
|
for obj in self._objects:
|
|
await obj.enable_object()
|
|
else:
|
|
self._available = False
|
|
self.hass.bus.async_fire(
|
|
EVENT_HASP_PLATE_OFFLINE,
|
|
{CONF_PLATE: self._entry.data[CONF_HWID]},
|
|
)
|
|
for obj in self._objects:
|
|
await obj.disable_object()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
except vol.error.Invalid as err:
|
|
_LOGGER.error("While processing LWT: %s", err)
|
|
|
|
self._subscriptions.append(
|
|
await self.hass.components.mqtt.async_subscribe(
|
|
f"{self._topic}/LWT", lwt_message_received
|
|
)
|
|
)
|
|
|
|
@property
|
|
def state_attributes(self):
|
|
"""Return the state attributes."""
|
|
attributes = {}
|
|
|
|
if self._statusupdate:
|
|
attributes = {**attributes, **self._statusupdate}
|
|
|
|
if ATTR_PAGE in attributes:
|
|
del attributes[
|
|
ATTR_PAGE
|
|
] # Page is tracked in the state, don't confuse users
|
|
|
|
return attributes
|
|
|
|
async def async_wakeup(self):
|
|
"""Wake up the display."""
|
|
cmd_topic = f"{self._topic}/command"
|
|
_LOGGER.warning("Wakeup will be deprecated in 0.8.0") # remove in version 0.8.0
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, "wakeup", qos=0, retain=False
|
|
)
|
|
|
|
async def async_change_page_next(self):
|
|
"""Change page to next one."""
|
|
cmd_topic = f"{self._topic}/command/page"
|
|
_LOGGER.warning(
|
|
"page next service will be deprecated in 0.8.0"
|
|
) # remove in version 0.8.0
|
|
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, "page next", qos=0, retain=False
|
|
)
|
|
|
|
async def async_change_page_prev(self):
|
|
"""Change page to previous one."""
|
|
cmd_topic = f"{self._topic}/command/page"
|
|
_LOGGER.warning(
|
|
"page prev service will be deprecated in 0.8.0"
|
|
) # remove in version 0.8.0
|
|
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, "page prev", qos=0, retain=False
|
|
)
|
|
|
|
async def async_clearpage(self, page="all"):
|
|
"""Clear page."""
|
|
cmd_topic = f"{self._topic}/command"
|
|
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, f"clearpage {page}", qos=0, retain=False
|
|
)
|
|
|
|
if page == "all":
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, "page 1", qos=0, retain=False
|
|
)
|
|
|
|
async def async_change_page(self, page):
|
|
"""Change page to number."""
|
|
cmd_topic = f"{self._topic}/command/page"
|
|
|
|
if self._statusupdate:
|
|
num_pages = self._statusupdate[HASP_NUM_PAGES]
|
|
|
|
if page <= 0 or page > num_pages:
|
|
_LOGGER.error(
|
|
"Can't change to %s, available pages are 1 to %s", page, num_pages
|
|
)
|
|
return
|
|
|
|
self._page = page
|
|
|
|
_LOGGER.debug("Change page %s", self._page)
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, self._page, qos=0, retain=False
|
|
)
|
|
self.async_write_ha_state()
|
|
|
|
async def async_command_service(self, keyword, parameters):
|
|
"""Send commands directly to the plate entity."""
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass,
|
|
f"{self._topic}/command",
|
|
f"{keyword} {parameters}".strip(),
|
|
qos=0,
|
|
retain=False,
|
|
)
|
|
|
|
async def async_config_service(self, submodule, parameters):
|
|
"""Send configuration commands to plate entity."""
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass,
|
|
f"{self._topic}/config/{submodule}",
|
|
f"{parameters}".strip(),
|
|
qos=0,
|
|
retain=False,
|
|
)
|
|
|
|
async def async_push_image(self, image, obj, width=None, height=None):
|
|
"""Update object image."""
|
|
|
|
image_id = hashlib.md5(image.encode("utf-8")).hexdigest()
|
|
|
|
rgb_image = await self.hass.async_add_executor_job(
|
|
image_to_rgb565, image, (width, height)
|
|
)
|
|
|
|
self.hass.data[DOMAIN][DATA_IMAGES][image_id] = rgb_image
|
|
|
|
cmd_topic = f"{self._topic}/command/{obj}.src"
|
|
|
|
rgb_image_url = (
|
|
f"{get_url(self.hass, allow_external=False)}/api/openhasp/serve/{image_id}"
|
|
)
|
|
|
|
_LOGGER.debug("Push %s with %s", cmd_topic, rgb_image_url)
|
|
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, cmd_topic, rgb_image_url, qos=0, retain=False
|
|
)
|
|
|
|
async def refresh(self):
|
|
"""Refresh objects in the SwitchPlate."""
|
|
|
|
_LOGGER.info("Refreshing %s", self._entry.data[CONF_NAME])
|
|
for obj in self._objects:
|
|
await obj.refresh()
|
|
|
|
await self.async_change_page(self._page)
|
|
|
|
async def async_load_page(self, path):
|
|
"""Load pages file on the SwitchPlate, existing pages will not be cleared."""
|
|
cmd_topic = f"{self._topic}/command"
|
|
_LOGGER.info("Load page %s to %s", path, cmd_topic)
|
|
|
|
if not self.hass.config.is_allowed_path(path):
|
|
_LOGGER.error("'%s' is not an allowed directory", path)
|
|
return
|
|
|
|
async def send_lines(lines):
|
|
mqtt_payload_buffer = ""
|
|
for line in lines:
|
|
if len(mqtt_payload_buffer) + len(line) > 1000:
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass,
|
|
f"{cmd_topic}/jsonl",
|
|
mqtt_payload_buffer,
|
|
qos=0,
|
|
retain=False,
|
|
)
|
|
mqtt_payload_buffer = line
|
|
else:
|
|
mqtt_payload_buffer = mqtt_payload_buffer + line
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass,
|
|
f"{cmd_topic}/jsonl",
|
|
mqtt_payload_buffer,
|
|
qos=0,
|
|
retain=False,
|
|
)
|
|
|
|
try:
|
|
with open(path, "r") as pages_file:
|
|
if path.endswith(".json"):
|
|
json_data = json.load(pages_file)
|
|
jsonschema.validate(instance=json_data, schema=self.json_schema)
|
|
lines = []
|
|
for item in json_data:
|
|
if isinstance(item, dict):
|
|
lines.append(json.dumps(item) + "\n")
|
|
await send_lines(lines)
|
|
else:
|
|
await send_lines(pages_file)
|
|
await self.refresh()
|
|
|
|
except (IndexError, FileNotFoundError, IsADirectoryError, UnboundLocalError):
|
|
_LOGGER.error(
|
|
"File or data not present at the moment: %s",
|
|
os.path.basename(path),
|
|
)
|
|
|
|
except json.JSONDecodeError:
|
|
_LOGGER.error(
|
|
"Error decoding .json file: %s",
|
|
os.path.basename(path),
|
|
)
|
|
|
|
except jsonschema.ValidationError as e:
|
|
_LOGGER.error(
|
|
"Schema check failed for %s. Validation Error: %s",
|
|
os.path.basename(path),
|
|
e.message,
|
|
)
|
|
|
|
|
|
# pylint: disable=R0902
|
|
class HASPObject:
|
|
"""Representation of an HASP-LVGL object."""
|
|
|
|
def __init__(self, hass, plate_topic, config):
|
|
"""Initialize an object."""
|
|
|
|
self.hass = hass
|
|
self.obj_id = config[CONF_OBJID]
|
|
self.command_topic = f"{plate_topic}/command/{self.obj_id}."
|
|
self.state_topic = f"{plate_topic}/state/{self.obj_id}"
|
|
self.cached_properties = {}
|
|
|
|
self.properties = config.get(CONF_PROPERTIES)
|
|
self.event_services = config.get(CONF_EVENT)
|
|
self._tracked_property_templates = []
|
|
self._freeze_properties = []
|
|
self._subscriptions = []
|
|
|
|
async def enable_object(self):
|
|
"""Initialize object events and properties subscriptions."""
|
|
|
|
if self.event_services:
|
|
_LOGGER.debug("Setup event_services for '%s'", self.obj_id)
|
|
self._subscriptions.append(await self.async_listen_hasp_events())
|
|
|
|
for _property, template in self.properties.items():
|
|
self._tracked_property_templates.append(
|
|
await self.async_set_property(_property, template)
|
|
)
|
|
|
|
async def disable_object(self):
|
|
"""Remove subscriptions and event tracking."""
|
|
_LOGGER.debug("Disabling HASPObject %s", self.obj_id)
|
|
for subscription in self._subscriptions:
|
|
subscription()
|
|
self._subscriptions = []
|
|
|
|
for tracked_template in self._tracked_property_templates:
|
|
tracked_template.async_remove()
|
|
self._tracked_property_templates = []
|
|
|
|
async def async_set_property(self, _property, template):
|
|
"""Set HASP Object property to template value."""
|
|
|
|
@callback
|
|
async def _async_template_result_changed(event, updates):
|
|
track_template_result = updates.pop()
|
|
template = track_template_result.template
|
|
result = track_template_result.result
|
|
|
|
if isinstance(result, TemplateError) or result is None:
|
|
entity = event and event.data.get("entity_id")
|
|
_LOGGER.error(
|
|
"TemplateError('%s') "
|
|
"while processing template '%s' "
|
|
"in entity '%s'",
|
|
result,
|
|
template,
|
|
entity,
|
|
)
|
|
return
|
|
|
|
self.cached_properties[_property] = result
|
|
if _property in self._freeze_properties:
|
|
# Skip update to plate to avoid feedback loops
|
|
return
|
|
|
|
_LOGGER.debug(
|
|
"%s.%s - %s changed, updating with: %s",
|
|
self.obj_id,
|
|
_property,
|
|
template,
|
|
result,
|
|
)
|
|
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, self.command_topic + _property, result
|
|
)
|
|
|
|
property_template = async_track_template_result(
|
|
self.hass,
|
|
[TrackTemplate(template, None)],
|
|
_async_template_result_changed,
|
|
)
|
|
property_template.async_refresh()
|
|
|
|
return property_template
|
|
|
|
async def refresh(self):
|
|
"""Refresh based on cached values."""
|
|
for _property, result in self.cached_properties.items():
|
|
_LOGGER.debug("Refresh object %s.%s = %s", self.obj_id, _property, result)
|
|
await self.hass.components.mqtt.async_publish(
|
|
self.hass, self.command_topic + _property, result
|
|
)
|
|
|
|
async def async_listen_hasp_events(self):
|
|
"""Listen to messages on MQTT for HASP events."""
|
|
|
|
@callback
|
|
async def message_received(msg):
|
|
"""Process object state MQTT message."""
|
|
try:
|
|
message = HASP_EVENT_SCHEMA(json.loads(msg.payload))
|
|
|
|
if message[HASP_EVENT] == HASP_EVENT_DOWN:
|
|
# store properties that shouldn't be updated while button pressed
|
|
self._freeze_properties = message.keys()
|
|
elif message[HASP_EVENT] in [HASP_EVENT_UP, HASP_EVENT_RELEASE]:
|
|
self._freeze_properties = []
|
|
|
|
for event in self.event_services:
|
|
if event in message[HASP_EVENT]:
|
|
_LOGGER.debug(
|
|
"Service call for '%s' triggered by '%s' on '%s' with variables %s",
|
|
event,
|
|
msg.payload,
|
|
msg.topic,
|
|
message,
|
|
)
|
|
for service in self.event_services[event]:
|
|
await async_call_from_config(
|
|
self.hass,
|
|
service,
|
|
validate_config=False,
|
|
variables=message,
|
|
)
|
|
except vol.error.Invalid:
|
|
_LOGGER.debug(
|
|
"Could not handle openHASP event: '%s' on '%s'",
|
|
msg.payload,
|
|
msg.topic,
|
|
)
|
|
except json.decoder.JSONDecodeError as err:
|
|
_LOGGER.error(
|
|
"Error decoding received JSON message: %s on %s", err.doc, msg.topic
|
|
)
|
|
|
|
_LOGGER.debug("Subscribe to '%s' events on '%s'", self.obj_id, self.state_topic)
|
|
return await self.hass.components.mqtt.async_subscribe(
|
|
self.state_topic, message_received
|
|
)
|