Files
hassos_config/custom_components/openhasp/__init__.py
2022-12-20 21:26:47 +01:00

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
)