initial commit
This commit is contained in:
@@ -0,0 +1,841 @@
|
||||
"""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
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,111 @@
|
||||
"""Allows to configure a binary sensor using GPIO."""
|
||||
import json
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
|
||||
# pylint: disable=R0801
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
|
||||
from .common import HASPEntity
|
||||
from .const import CONF_HWID, CONF_INPUT, CONF_TOPIC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
HASP_BINARY_INPUT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("state"): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
||||
):
|
||||
"""Set up Plate Relays as switch based on a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
HASPBinarySensor(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
gpio,
|
||||
device_class,
|
||||
)
|
||||
for device_class in entry.data[CONF_INPUT]
|
||||
for gpio in entry.data[CONF_INPUT][device_class]
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HASPBinarySensor(HASPEntity, BinarySensorEntity):
|
||||
"""Representation of an openHASP relay."""
|
||||
|
||||
# pylint: disable=R0913
|
||||
def __init__(self, name, hwid, topic, gpio, dev_class):
|
||||
"""Initialize the relay."""
|
||||
super().__init__(name, hwid, topic, gpio)
|
||||
self._device_class = dev_class
|
||||
self._attr_name = f"{name} binary_sensor {self._gpio}"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
async def refresh(self):
|
||||
"""Force sync of plate state back to binary sensor."""
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/input{self._gpio}",
|
||||
"",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def state_message_received(msg):
|
||||
"""Process State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_BINARY_INPUT_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("%s state = %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/input{self._gpio}", state_message_received
|
||||
)
|
||||
)
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/input{self._gpio}",
|
||||
"",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Support for current page numbers."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
|
||||
from .common import HASPEntity
|
||||
from .const import CONF_HWID, CONF_TOPIC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
):
|
||||
"""Set up Plate Relays as switch based on a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
HASPRestartButton(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HASPRestartButton(HASPEntity, ButtonEntity):
|
||||
"""Representation of page number."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_device_class = ButtonDeviceClass.RESTART
|
||||
|
||||
def __init__(self, name, hwid, topic) -> None:
|
||||
"""Initialize the Restart Button."""
|
||||
super().__init__(name, hwid, topic, "restart")
|
||||
self._attr_name = f"{name} restart"
|
||||
self._available = True
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/restart",
|
||||
"",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,105 @@
|
||||
"""HASP-LVGL Commonalities."""
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity, ToggleEntity
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
CONF_PLATE,
|
||||
DOMAIN,
|
||||
EVENT_HASP_PLATE_OFFLINE,
|
||||
EVENT_HASP_PLATE_ONLINE,
|
||||
HASP_IDLE_STATES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
HASP_IDLE_SCHEMA = vol.Schema(vol.Any(*HASP_IDLE_STATES))
|
||||
|
||||
|
||||
class HASPEntity(Entity):
|
||||
"""Generic HASP entity (base class)."""
|
||||
|
||||
def __init__(self, name, hwid: str, topic: str, part=None) -> None:
|
||||
"""Initialize the HASP entity."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._hwid = hwid
|
||||
self._topic = topic
|
||||
self._state = None
|
||||
self._available = False
|
||||
self._subscriptions = []
|
||||
self._attr_unique_id = f"{self._hwid}.{part}"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, self._hwid)},
|
||||
}
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if entity is available."""
|
||||
return self._available
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def online(event):
|
||||
if event.data[CONF_PLATE] == self._hwid:
|
||||
self._available = True
|
||||
if self._state:
|
||||
await self.refresh()
|
||||
else:
|
||||
self.async_write_ha_state() # Just to update availability
|
||||
_LOGGER.debug("%s is available, %s", self.entity_id, "refresh" if self._state else "stale")
|
||||
|
||||
self._subscriptions.append(
|
||||
self.hass.bus.async_listen(EVENT_HASP_PLATE_ONLINE, online)
|
||||
)
|
||||
|
||||
@callback
|
||||
async def offline(event):
|
||||
if event.data[CONF_PLATE] == self._hwid:
|
||||
self._available = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._subscriptions.append(
|
||||
self.hass.bus.async_listen(EVENT_HASP_PLATE_OFFLINE, offline)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Run when entity about to be removed."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
for subscription in self._subscriptions:
|
||||
subscription()
|
||||
|
||||
|
||||
class HASPToggleEntity(HASPEntity, ToggleEntity):
|
||||
"""Representation of HASP ToggleEntity."""
|
||||
|
||||
def __init__(self, name, hwid, topic, gpio):
|
||||
"""Initialize the relay."""
|
||||
super().__init__(name, hwid, topic, gpio)
|
||||
self._gpio = gpio
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on."""
|
||||
self._state = True
|
||||
await self.refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn off."""
|
||||
self._state = False
|
||||
await self.refresh()
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Config flow to configure OpenHASP component."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, exceptions
|
||||
from homeassistant.components.mqtt import valid_subscribe_topic
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
CONF_DIMLIGHTS,
|
||||
CONF_HWID,
|
||||
CONF_IDLE_BRIGHTNESS,
|
||||
CONF_INPUT,
|
||||
CONF_LIGHTS,
|
||||
CONF_NODE,
|
||||
CONF_PAGES,
|
||||
CONF_PAGES_PATH,
|
||||
CONF_RELAYS,
|
||||
CONF_TOPIC,
|
||||
DEFAULT_IDLE_BRIGHNESS,
|
||||
DISCOVERED_DIM,
|
||||
DISCOVERED_HWID,
|
||||
DISCOVERED_INPUT,
|
||||
DISCOVERED_LIGHT,
|
||||
DISCOVERED_MANUFACTURER,
|
||||
DISCOVERED_MODEL,
|
||||
DISCOVERED_NODE,
|
||||
DISCOVERED_PAGES,
|
||||
DISCOVERED_POWER,
|
||||
DISCOVERED_URL,
|
||||
DISCOVERED_VERSION,
|
||||
DOMAIN,
|
||||
MAJOR,
|
||||
MINOR,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_jsonl(path):
|
||||
"""Validate that the value is an existing file."""
|
||||
if path is None:
|
||||
raise InvalidJSONL()
|
||||
file_in = os.path.expanduser(str(path))
|
||||
|
||||
if not os.path.isfile(file_in):
|
||||
raise InvalidJSONL("not a file")
|
||||
if not os.access(file_in, os.R_OK):
|
||||
raise InvalidJSONL("file not readable")
|
||||
return file_in
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class OpenHASPFlowHandler(config_entries.ConfigFlow):
|
||||
"""Config flow for OpenHASP component."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
def __init__(self):
|
||||
"""Init OpenHASPFlowHandler."""
|
||||
self._errors = {}
|
||||
self.config_data = {
|
||||
DISCOVERED_MANUFACTURER: "openHASP",
|
||||
DISCOVERED_MODEL: None,
|
||||
CONF_RELAYS: [],
|
||||
}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by User."""
|
||||
_LOGGER.error("Discovery Only")
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
"hasp/broadcast/command/discovery",
|
||||
"discovery",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
return self.async_abort(reason="discovery_only")
|
||||
|
||||
async def async_step_mqtt(self, discovery_info=None):
|
||||
"""Handle a flow initialized by MQTT discovery."""
|
||||
_discovered = json.loads(discovery_info.payload)
|
||||
_LOGGER.debug("Discovered: %s", _discovered)
|
||||
|
||||
await self.async_set_unique_id(_discovered[DISCOVERED_HWID], raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
version = _discovered.get(DISCOVERED_VERSION)
|
||||
if version.split(".")[0:2] != [MAJOR, MINOR]:
|
||||
_LOGGER.error(
|
||||
"Version mismatch! Your plate: %s - openHASP Component: %s",
|
||||
version,
|
||||
f"{MAJOR}.{MINOR}.x",
|
||||
)
|
||||
raise data_entry_flow.AbortFlow("mismatch_version")
|
||||
|
||||
self.config_data[DISCOVERED_VERSION] = version
|
||||
|
||||
self.config_data[CONF_HWID] = _discovered[DISCOVERED_HWID]
|
||||
self.config_data[CONF_NODE] = self.config_data[CONF_NAME] = _discovered[
|
||||
DISCOVERED_NODE
|
||||
]
|
||||
self.config_data[
|
||||
CONF_TOPIC
|
||||
] = f"{discovery_info.topic.split('/')[0]}/{self.config_data[CONF_NODE]}"
|
||||
|
||||
self.config_data[DISCOVERED_URL] = _discovered.get(DISCOVERED_URL)
|
||||
self.config_data[DISCOVERED_MANUFACTURER] = _discovered.get(
|
||||
DISCOVERED_MANUFACTURER
|
||||
)
|
||||
self.config_data[DISCOVERED_MODEL] = _discovered.get(DISCOVERED_MODEL)
|
||||
self.config_data[CONF_PAGES] = _discovered.get(DISCOVERED_PAGES)
|
||||
self.config_data[CONF_RELAYS] = _discovered.get(DISCOVERED_POWER)
|
||||
self.config_data[CONF_LIGHTS] = _discovered.get(DISCOVERED_LIGHT)
|
||||
self.config_data[CONF_DIMLIGHTS] = _discovered.get(DISCOVERED_DIM)
|
||||
self.config_data[CONF_INPUT] = _discovered.get(DISCOVERED_INPUT)
|
||||
|
||||
return await self.async_step_personalize()
|
||||
|
||||
async def async_step_personalize(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
self._errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.config_data = {**self.config_data, **user_input}
|
||||
|
||||
if self.config_data[
|
||||
CONF_NAME
|
||||
] not in self.hass.config_entries.async_entries(DOMAIN):
|
||||
# Remove / from base topic
|
||||
if user_input[CONF_TOPIC].endswith("/"):
|
||||
user_input[CONF_TOPIC] = user_input[CONF_TOPIC][:-1]
|
||||
|
||||
try:
|
||||
valid_subscribe_topic(self.config_data[CONF_TOPIC])
|
||||
|
||||
if CONF_PAGES_PATH in user_input:
|
||||
self.config_data[CONF_PAGES_PATH] = validate_jsonl(
|
||||
user_input[CONF_PAGES_PATH]
|
||||
)
|
||||
|
||||
await self.async_set_unique_id(self.config_data[CONF_HWID])
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=self.config_data
|
||||
)
|
||||
|
||||
except vol.Invalid:
|
||||
return self.async_abort(reason="invalid_discovery_info")
|
||||
|
||||
except InvalidJSONL:
|
||||
self._errors[CONF_PAGES_PATH] = "invalid_jsonl_path"
|
||||
else:
|
||||
self._errors[CONF_NAME] = "name_exists"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="personalize",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_TOPIC, default=self.config_data.get(CONF_TOPIC, "hasp")
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_NAME, default=self.config_data.get(CONF_NAME)
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_IDLE_BRIGHTNESS, default=DEFAULT_IDLE_BRIGHNESS
|
||||
): vol.All(int, vol.Range(min=0, max=255)),
|
||||
vol.Optional(CONF_PAGES_PATH): str,
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Set the OptionsFlowHandler."""
|
||||
return OpenHASPOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OpenHASPOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""ConfigOptions flow for openHASP."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize openHASP options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
# Actually check path is a file
|
||||
|
||||
try:
|
||||
if len(user_input[CONF_PAGES_PATH]):
|
||||
user_input[CONF_PAGES_PATH] = validate_jsonl(
|
||||
user_input[CONF_PAGES_PATH]
|
||||
)
|
||||
except InvalidJSONL:
|
||||
return self.async_abort(reason="invalid_jsonl_path")
|
||||
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_IDLE_BRIGHTNESS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_IDLE_BRIGHTNESS,
|
||||
self.config_entry.data[CONF_IDLE_BRIGHTNESS],
|
||||
),
|
||||
): vol.All(int, vol.Range(min=0, max=255)),
|
||||
vol.Optional(
|
||||
CONF_PAGES_PATH,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_PAGES_PATH,
|
||||
self.config_entry.data.get(CONF_PAGES_PATH, ""),
|
||||
),
|
||||
): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class InvalidJSONL(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot load JSONL."""
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Constants for HASP Open Hardware Edition custom component."""
|
||||
|
||||
# Version
|
||||
MAJOR = "0"
|
||||
MINOR = "6"
|
||||
|
||||
DOMAIN = "openhasp"
|
||||
|
||||
CONF_COMPONENT = "component"
|
||||
CONF_OBJID = "obj"
|
||||
CONF_PROPERTIES = "properties"
|
||||
CONF_EVENT = "event"
|
||||
CONF_TRACK = "track"
|
||||
CONF_TOPIC = "topic"
|
||||
CONF_PAGES = "pages"
|
||||
CONF_PAGES_PATH = "path"
|
||||
CONF_OBJECTS = "objects"
|
||||
CONF_VAL = "val"
|
||||
CONF_IDLE_BRIGHTNESS = "idle_brightness"
|
||||
CONF_AWAKE_BRIGHTNESS = "awake_brightness"
|
||||
CONF_PLATE = "plate"
|
||||
CONF_RELAYS = "relay"
|
||||
CONF_LIGHTS = "light"
|
||||
CONF_DIMLIGHTS = "dimlight"
|
||||
CONF_LEDS = "led"
|
||||
CONF_PWMS = "pwm"
|
||||
CONF_GPIO = "gpio"
|
||||
CONF_NODE = "node"
|
||||
CONF_HWID = "hwid"
|
||||
CONF_INPUT = "input"
|
||||
|
||||
DATA_LISTENER = "listener"
|
||||
DATA_IMAGES = "images"
|
||||
|
||||
DEFAULT_TOPIC = "hasp"
|
||||
DEFAULT_PATH = "pages.jsonl"
|
||||
DEFAULT_IDLE_BRIGHNESS = 25
|
||||
|
||||
DISCOVERED_NODE = "node"
|
||||
DISCOVERED_MODEL = "mdl"
|
||||
DISCOVERED_MANUFACTURER = "mf"
|
||||
DISCOVERED_HWID = "hwid"
|
||||
DISCOVERED_PAGES = "pages"
|
||||
DISCOVERED_INPUT = "input"
|
||||
DISCOVERED_POWER = "power"
|
||||
DISCOVERED_LIGHT = "light"
|
||||
DISCOVERED_DIM = "dim"
|
||||
DISCOVERED_VERSION = "sw"
|
||||
DISCOVERED_INPUT = "input"
|
||||
DISCOVERED_URL = "uri"
|
||||
|
||||
HASP_NUM_PAGES = "numPages"
|
||||
HASP_VAL = "val"
|
||||
HASP_EVENT = "event"
|
||||
HASP_EVENT_ON = "on"
|
||||
HASP_EVENT_OFF = "off"
|
||||
HASP_EVENT_DOWN = "down"
|
||||
HASP_EVENT_UP = "up"
|
||||
HASP_EVENT_SHORT = "short"
|
||||
HASP_EVENT_LONG = "long"
|
||||
HASP_EVENT_CHANGED = "changed"
|
||||
HASP_EVENT_RELEASE = "release"
|
||||
HASP_EVENT_HOLD = "hold"
|
||||
HASP_EVENTS = (
|
||||
HASP_EVENT_ON,
|
||||
HASP_EVENT_OFF,
|
||||
HASP_EVENT_DOWN,
|
||||
HASP_EVENT_UP,
|
||||
HASP_EVENT_SHORT,
|
||||
HASP_EVENT_LONG,
|
||||
HASP_EVENT_CHANGED,
|
||||
HASP_EVENT_RELEASE,
|
||||
HASP_EVENT_HOLD,
|
||||
)
|
||||
HASP_IDLE_OFF = "off"
|
||||
HASP_IDLE_SHORT = "short"
|
||||
HASP_IDLE_LONG = "long"
|
||||
HASP_IDLE_STATES = (HASP_IDLE_OFF, HASP_IDLE_SHORT, HASP_IDLE_LONG)
|
||||
HASP_ONLINE = "online"
|
||||
HASP_OFFLINE = "offline"
|
||||
HASP_LWT = (HASP_ONLINE, HASP_OFFLINE)
|
||||
|
||||
ATTR_PAGE = "page"
|
||||
ATTR_CURRENT_DIM = "dim"
|
||||
ATTR_IDLE = "idle"
|
||||
ATTR_PATH = "path"
|
||||
ATTR_AWAKE_BRIGHTNESS = "awake brightness"
|
||||
ATTR_IDLE_BRIGHTNESS = "idle brightness"
|
||||
ATTR_COMMAND_KEYWORD = "keyword"
|
||||
ATTR_COMMAND_PARAMETERS = "parameters"
|
||||
ATTR_CONFIG_SUBMODULE = "submodule"
|
||||
ATTR_CONFIG_PARAMETERS = "parameters"
|
||||
ATTR_IMAGE = "image"
|
||||
ATTR_OBJECT = "obj"
|
||||
ATTR_WIDTH = "width"
|
||||
ATTR_HEIGHT = "height"
|
||||
|
||||
SERVICE_WAKEUP = "wakeup"
|
||||
SERVICE_CLEAR_PAGE = "clear_page"
|
||||
SERVICE_LOAD_PAGE = "load_pages"
|
||||
SERVICE_PAGE_CHANGE = "change_page"
|
||||
SERVICE_PAGE_NEXT = "next_page"
|
||||
SERVICE_PAGE_PREV = "prev_page"
|
||||
SERVICE_COMMAND = "command"
|
||||
SERVICE_CONFIG = "config"
|
||||
SERVICE_PUSH_IMAGE = "push_image"
|
||||
|
||||
EVENT_HASP_PLATE_ONLINE = "openhasp_plate_online"
|
||||
EVENT_HASP_PLATE_OFFLINE = "openhasp_plate_offline"
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Image processing and serving functions."""
|
||||
|
||||
import logging
|
||||
import struct
|
||||
import tempfile
|
||||
|
||||
from PIL import Image
|
||||
from aiohttp import hdrs, web
|
||||
from homeassistant.components.http.static import CACHE_HEADERS
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
import requests
|
||||
|
||||
from .const import DATA_IMAGES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def image_to_rgb565(in_image, size):
|
||||
"""Transform image to rgb565 format according to LVGL requirements."""
|
||||
try:
|
||||
if in_image.startswith("http"):
|
||||
im = Image.open(requests.get(in_image, stream=True).raw)
|
||||
else:
|
||||
im = Image.open(in_image)
|
||||
except Exception:
|
||||
_LOGGER.error("Failed to open %s", in_image)
|
||||
return None
|
||||
|
||||
original_width, original_height = im.size
|
||||
width, height = size
|
||||
|
||||
width = min(w for w in [width, original_width] if w is not None and w > 0)
|
||||
height = min(h for h in [height, original_height] if h is not None and h > 0)
|
||||
|
||||
im.thumbnail((height, width), Image.ANTIALIAS)
|
||||
width, height = im.size # actual size after resize
|
||||
|
||||
out_image = tempfile.NamedTemporaryFile(mode="w+b")
|
||||
|
||||
out_image.write(struct.pack("I", height << 21 | width << 10 | 4))
|
||||
|
||||
img = im.convert("RGB")
|
||||
|
||||
for pix in img.getdata():
|
||||
r = (pix[0] >> 3) & 0x1F
|
||||
g = (pix[1] >> 2) & 0x3F
|
||||
b = (pix[2] >> 3) & 0x1F
|
||||
out_image.write(struct.pack("H", (r << 11) | (g << 5) | b))
|
||||
|
||||
_LOGGER.debug("image_to_rgb565 out_image: %s", out_image.name)
|
||||
|
||||
out_image.flush()
|
||||
|
||||
return out_image
|
||||
|
||||
|
||||
class ImageServeView(HomeAssistantView):
|
||||
"""View to download images."""
|
||||
|
||||
url = "/api/openhasp/serve/{image_id}"
|
||||
name = "api:openhasp:serve"
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize image serve view."""
|
||||
|
||||
async def get(self, request: web.Request, image_id: str):
|
||||
"""Serve image."""
|
||||
|
||||
hass = request.app["hass"]
|
||||
target_file = hass.data[DOMAIN][DATA_IMAGES].get(image_id)
|
||||
if target_file is None:
|
||||
_LOGGER.error("Unknown image_id %s", image_id)
|
||||
return web.HTTPNotFound()
|
||||
|
||||
_LOGGER.debug("Get Image %s form %s", image_id, target_file.name)
|
||||
|
||||
return web.FileResponse(
|
||||
target_file.name, headers={**CACHE_HEADERS, hdrs.CONTENT_TYPE: "image/bmp"}
|
||||
)
|
||||
@@ -0,0 +1,496 @@
|
||||
"""Support for HASP LVGL moodlights."""
|
||||
import json
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_HS_COLOR,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
import homeassistant.util.color as color_util
|
||||
import voluptuous as vol
|
||||
|
||||
from .common import HASP_IDLE_SCHEMA, HASPToggleEntity
|
||||
from .const import (
|
||||
ATTR_AWAKE_BRIGHTNESS,
|
||||
ATTR_IDLE_BRIGHTNESS,
|
||||
CONF_DIMLIGHTS,
|
||||
CONF_HWID,
|
||||
CONF_IDLE_BRIGHTNESS,
|
||||
CONF_LIGHTS,
|
||||
CONF_TOPIC,
|
||||
HASP_IDLE_LONG,
|
||||
HASP_IDLE_OFF,
|
||||
HASP_IDLE_SHORT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HASP_MOODLIGHT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("state"): cv.boolean,
|
||||
vol.Required("r"): vol.All(int, vol.Range(min=0, max=255)),
|
||||
vol.Required("g"): vol.All(int, vol.Range(min=0, max=255)),
|
||||
vol.Required("b"): vol.All(int, vol.Range(min=0, max=255)),
|
||||
vol.Required("brightness"): vol.All(int, vol.Range(min=0, max=255)),
|
||||
vol.Optional("color"): str,
|
||||
},
|
||||
)
|
||||
|
||||
HASP_BACKLIGHT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("state"): cv.boolean,
|
||||
vol.Required("brightness"): vol.All(int, vol.Range(min=0, max=255)),
|
||||
}
|
||||
)
|
||||
|
||||
HASP_LIGHT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("state"): cv.boolean,
|
||||
vol.Optional("brightness"): vol.All(int, vol.Range(min=0, max=255)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=R0801, W0613
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
||||
):
|
||||
"""Set up Plate Light sensors based on a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
HASPBackLight(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
entry.options.get(
|
||||
CONF_IDLE_BRIGHTNESS, entry.data[CONF_IDLE_BRIGHTNESS]
|
||||
),
|
||||
),
|
||||
HASPMoodLight(
|
||||
entry.data[CONF_NAME], entry.data[CONF_HWID], entry.data[CONF_TOPIC]
|
||||
),
|
||||
]
|
||||
+ [
|
||||
HASPLight(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
gpio,
|
||||
)
|
||||
for gpio in entry.data[CONF_LIGHTS]
|
||||
]
|
||||
+ [
|
||||
HASPDimmableLight(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
gpio,
|
||||
)
|
||||
for gpio in entry.data[CONF_DIMLIGHTS]
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HASPLight(HASPToggleEntity, LightEntity):
|
||||
"""Representation of openHASP Light."""
|
||||
|
||||
def __init__(self, name, hwid, topic, gpio):
|
||||
"""Initialize the light."""
|
||||
super().__init__(name, hwid, topic, gpio)
|
||||
self._attr_name = f"{name} light {gpio}"
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/output{self._gpio}",
|
||||
json.dumps(HASP_LIGHT_SCHEMA({"state": int(self._state)})),
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def light_state_message_received(msg):
|
||||
"""Process State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_LIGHT_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("received light %s: %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/output{self._gpio}", light_state_message_received
|
||||
)
|
||||
)
|
||||
|
||||
# Force immediatable state update from plate
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/output{self._gpio}",
|
||||
"",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
|
||||
class HASPDimmableLight(HASPToggleEntity, LightEntity):
|
||||
"""Representation of openHASP Light."""
|
||||
|
||||
def __init__(self, name, hwid, topic, gpio):
|
||||
"""Initialize the dimmable light."""
|
||||
super().__init__(name, hwid, topic, gpio)
|
||||
self._brightness = None
|
||||
self._attr_supported_features = SUPPORT_BRIGHTNESS
|
||||
self._gpio = gpio
|
||||
self._attr_name = f"{name} dimmable light {self._gpio}"
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
_LOGGER.debug(
|
||||
"refresh dim %s state = %s, brightness = %s",
|
||||
self.name,
|
||||
self._state,
|
||||
self._brightness,
|
||||
)
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/output{self._gpio}",
|
||||
json.dumps(
|
||||
HASP_LIGHT_SCHEMA(
|
||||
{"state": self._state, "brightness": self._brightness}
|
||||
)
|
||||
),
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def dimmable_light_message_received(msg):
|
||||
"""Process State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_LIGHT_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("received dimmable light %s: %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self._brightness = message["brightness"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/output{self._gpio}",
|
||||
dimmable_light_message_received,
|
||||
)
|
||||
)
|
||||
|
||||
# Force immediatable state update from plate
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/output{self._gpio}",
|
||||
"",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on the dimmable light."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
self._state = True
|
||||
await self.refresh()
|
||||
|
||||
|
||||
class HASPBackLight(HASPToggleEntity, LightEntity, RestoreEntity):
|
||||
"""Representation of HASP LVGL Backlight."""
|
||||
|
||||
def __init__(self, name, hwid, topic, brightness):
|
||||
"""Initialize the light."""
|
||||
super().__init__(name, hwid, topic, "backlight")
|
||||
self._awake_brightness = 255
|
||||
self._brightness = None
|
||||
self._idle_brightness = brightness
|
||||
self._attr_supported_features = SUPPORT_BRIGHTNESS
|
||||
self._attr_name = f"{name} backlight"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attributes = {
|
||||
ATTR_AWAKE_BRIGHTNESS: self._awake_brightness,
|
||||
ATTR_IDLE_BRIGHTNESS: self._idle_brightness,
|
||||
}
|
||||
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
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:
|
||||
self._state = state.state
|
||||
self._brightness = state.attributes.get(ATTR_BRIGHTNESS)
|
||||
self._awake_brightness = state.attributes.get(ATTR_AWAKE_BRIGHTNESS, 255)
|
||||
_LOGGER.debug(
|
||||
"Restoring %s self.brigthness = %s; awake_brightness = %s",
|
||||
self.name,
|
||||
self._brightness,
|
||||
self._awake_brightness,
|
||||
)
|
||||
if not self._brightness:
|
||||
self._brightness = self._awake_brightness
|
||||
|
||||
await self.async_listen_idleness()
|
||||
|
||||
cmd_topic = f"{self._topic}/command"
|
||||
state_topic = f"{self._topic}/state/backlight"
|
||||
|
||||
@callback
|
||||
async def backlight_message_received(msg):
|
||||
"""Process Backlight State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_BACKLIGHT_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("received backlight %s: %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self._brightness = message["brightness"]
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error(
|
||||
"While proccessing backlight: %s, original message was: %s",
|
||||
err,
|
||||
msg,
|
||||
)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
state_topic, backlight_message_received
|
||||
)
|
||||
)
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass, cmd_topic, "backlight", qos=0, retain=False
|
||||
)
|
||||
|
||||
async def async_listen_idleness(self):
|
||||
"""Listen to messages on MQTT for HASP idleness."""
|
||||
|
||||
@callback
|
||||
async def idle_message_received(msg):
|
||||
"""Process MQTT message from plate."""
|
||||
message = HASP_IDLE_SCHEMA(msg.payload)
|
||||
|
||||
if message == HASP_IDLE_OFF:
|
||||
brightness = self._awake_brightness
|
||||
backlight = 1
|
||||
elif message == HASP_IDLE_SHORT:
|
||||
brightness = self._idle_brightness
|
||||
backlight = 1
|
||||
elif message == HASP_IDLE_LONG:
|
||||
brightness = self._awake_brightness
|
||||
backlight = 0
|
||||
else:
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Idle state for %s is %s - Dimming to %s; Backlight to %s",
|
||||
self.name,
|
||||
message,
|
||||
brightness,
|
||||
backlight,
|
||||
)
|
||||
|
||||
new_state = {"state": backlight, "brightness": brightness}
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command",
|
||||
f"backlight {json.dumps(new_state)}",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/idle", idle_message_received
|
||||
)
|
||||
)
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
cmd_topic = f"{self._topic}/command"
|
||||
|
||||
new_state = {"state": self._state, "brightness": self._brightness}
|
||||
|
||||
_LOGGER.debug("refresh(%s) backlight - %s", self.name, new_state)
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
cmd_topic,
|
||||
f"backlight {json.dumps(new_state)}",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on the backlight."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
self._awake_brightness = (
|
||||
self._brightness
|
||||
) # save this value for later recall
|
||||
self._state = True
|
||||
await self.refresh()
|
||||
|
||||
|
||||
class HASPMoodLight(HASPToggleEntity, LightEntity, RestoreEntity):
|
||||
"""Representation of HASP LVGL Moodlight."""
|
||||
|
||||
def __init__(self, name, hwid, topic):
|
||||
"""Initialize the light."""
|
||||
super().__init__(name, hwid, topic, "moodlight")
|
||||
self._hs = None
|
||||
self._brightness = None
|
||||
self._attr_supported_features = SUPPORT_COLOR | SUPPORT_BRIGHTNESS
|
||||
self._attr_name = f"{name} moodlight"
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the color property."""
|
||||
return self._hs
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
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:
|
||||
self._state = state.state
|
||||
self._brightness = state.attributes.get(ATTR_BRIGHTNESS)
|
||||
self._hs = state.attributes.get(ATTR_HS_COLOR)
|
||||
_LOGGER.debug(
|
||||
"Restoring %s self.brigthness = %s; hs_color = %s",
|
||||
self.name,
|
||||
self._brightness,
|
||||
self._hs,
|
||||
)
|
||||
|
||||
@callback
|
||||
async def moodlight_message_received(msg):
|
||||
"""Process Moodlight State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_MOODLIGHT_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("received moodlight %s: %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self._hs = color_util.color_RGB_to_hs(
|
||||
message["r"], message["g"], message["b"]
|
||||
)
|
||||
self._brightness = message["brightness"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error("While proccessing moodlight: %s", err)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/moodlight", moodlight_message_received
|
||||
)
|
||||
)
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass, f"{self._topic}/command", "moodlight", qos=0, retain=False
|
||||
)
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
cmd_topic = f"{self._topic}/command"
|
||||
|
||||
new_state = {"state": self._state}
|
||||
if self._hs:
|
||||
rgb = color_util.color_hs_to_RGB(*self._hs)
|
||||
new_state = {**new_state, **dict(zip("rgb", rgb))}
|
||||
if self._brightness:
|
||||
new_state["brightness"] = self._brightness
|
||||
|
||||
_LOGGER.debug("refresh(%s) moodlight - %s", self.name, new_state)
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
cmd_topic,
|
||||
f"moodlight {json.dumps(new_state)}",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on the moodlight."""
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
self._hs = kwargs[ATTR_HS_COLOR]
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
self._state = True
|
||||
_LOGGER.debug(
|
||||
"Turn on %s - %s - %s",
|
||||
self._topic,
|
||||
color_util.color_hs_to_RGB(*self._hs) if self._hs else None,
|
||||
self._brightness,
|
||||
)
|
||||
await self.refresh()
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "openhasp",
|
||||
"name": "openHASP",
|
||||
"documentation": "https://haswitchplate.github.io/openHASP-docs/",
|
||||
"issue_tracker": "https://github.com/HASwitchPlate/openHASP-custom-component/issues",
|
||||
"dependencies": ["mqtt", "http"],
|
||||
"requirements": ["jsonschema>=3.2.0"],
|
||||
"version": "0.6.5",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"codeowners": ["@dgomes"],
|
||||
"mqtt": ["hasp/discovery"]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Support for current page numbers."""
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .common import HASPEntity
|
||||
from .const import CONF_HWID, CONF_TOPIC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HASPNumberDescriptionMixin:
|
||||
"""Mixin to describe a HASP Number entity."""
|
||||
|
||||
command_topic: str
|
||||
state_topic: str
|
||||
|
||||
@dataclass
|
||||
class HASPNumberDescription(NumberEntityDescription, HASPNumberDescriptionMixin):
|
||||
"""Class to describe an HASP Number Entity."""
|
||||
|
||||
|
||||
NUMBERS = [
|
||||
HASPNumberDescription(
|
||||
key="Page Number",
|
||||
name="Page Number",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
icon="mdi:numeric-1-box-multiple-outline",
|
||||
native_min_value=1,
|
||||
native_max_value=12,
|
||||
command_topic="/command/page",
|
||||
state_topic="/state/page",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
):
|
||||
"""Set up Plate Relays as switch based on a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
HASPNumber(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
desc,
|
||||
)
|
||||
for desc in NUMBERS
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HASPNumber(HASPEntity, NumberEntity, RestoreEntity):
|
||||
"""Representation of HASP number."""
|
||||
|
||||
def __init__(self, name, hwid, topic, description) -> None:
|
||||
"""Initialize the number."""
|
||||
super().__init__(name, hwid, topic, description.key)
|
||||
self.entity_description = description
|
||||
self._number = None
|
||||
self._attr_name = f"{name} {self.entity_description.name}"
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}{self.entity_description.command_topic}",
|
||||
"" if self._number is None else self._number,
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
_LOGGER.debug("refresh %s with <%s>", self.entity_id, self._number)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def page_state_message_received(msg):
|
||||
"""Process State."""
|
||||
|
||||
self._available = True
|
||||
_LOGGER.debug("%s current value = %s", self.entity_id, msg.payload)
|
||||
|
||||
self._number = int(msg.payload)
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}{self.entity_description.state_topic}",
|
||||
page_state_message_received,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
self._number = int(state.state)
|
||||
except Exception:
|
||||
_LOGGER.error("Could not restore page number for %s", self.entity_id)
|
||||
|
||||
await self.refresh()
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
"""Return the current number."""
|
||||
return self._number
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the perfume amount."""
|
||||
if not value.is_integer():
|
||||
raise ValueError(
|
||||
f"Can't set the {self.entity_description.name} to {value}. {self.entity_description.name} must be an integer."
|
||||
)
|
||||
self._number = int(value)
|
||||
await self.refresh()
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 12
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 254
|
||||
},
|
||||
"obj": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
load_pages:
|
||||
name: Load pages.jsonl
|
||||
description: Loads new design from pages.jsonl file from full path. The file must be located in an authorised location defined by allowlist_external_dirs in Home Assistant's main configuration.
|
||||
target:
|
||||
fields:
|
||||
path:
|
||||
name: Path
|
||||
description: Path to the file containing the plate layout in JSONL format
|
||||
required: true
|
||||
example: "/config/pages.jsonl"
|
||||
selector:
|
||||
text:
|
||||
|
||||
wakeup:
|
||||
name: Wakeup
|
||||
description: This is helpful e.g. when you want to wake up the display when an external event has occurred, like a presence or PIR motion sensor.
|
||||
target:
|
||||
|
||||
next_page:
|
||||
name: Next Page
|
||||
description: Changes plate to the next page
|
||||
target:
|
||||
|
||||
prev_page:
|
||||
name: Previous Page
|
||||
description: Changes plate to the previous page
|
||||
target:
|
||||
|
||||
clear_page:
|
||||
name: Clear Page
|
||||
description: Clears the contents of the specified page number.
|
||||
target:
|
||||
fields:
|
||||
page:
|
||||
name: Page
|
||||
description: Page number to clear (if not specified, clear all pages)
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 12
|
||||
|
||||
change_page:
|
||||
name: Change Page
|
||||
description: Changes plate directly to the specified page number.
|
||||
target:
|
||||
fields:
|
||||
page:
|
||||
name: Page
|
||||
description: Page number to change to
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 12
|
||||
|
||||
command:
|
||||
name: Command
|
||||
description: Sends commands directly to the plate entity (as a wrapper for MQTT commands sent to hasp/<nodename>/command)
|
||||
target:
|
||||
fields:
|
||||
keyword:
|
||||
name: Keyword
|
||||
description: Command keyword.
|
||||
required: true
|
||||
example: "backlight"
|
||||
selector:
|
||||
text:
|
||||
parameters:
|
||||
name: Parameters
|
||||
description: The parameters of the command.
|
||||
required: false
|
||||
example: "off"
|
||||
selector:
|
||||
text:
|
||||
|
||||
config:
|
||||
name: Configuration
|
||||
description: Sends configuration commands to plate entity (as a wrapper for MQTT commands sent to hasp/<nodename>/config/submodule)
|
||||
target:
|
||||
fields:
|
||||
submodule:
|
||||
name: submodule
|
||||
description: The submodule we intend to configure.
|
||||
required: true
|
||||
example: 'gui'
|
||||
selector:
|
||||
text:
|
||||
parameters:
|
||||
name: Parameters
|
||||
description: The parameters of the configuration setting.
|
||||
required: true
|
||||
example: '{"idle2":180}'
|
||||
selector:
|
||||
text:
|
||||
|
||||
push_image:
|
||||
name: Push Image
|
||||
description: Change the src image of an img object.
|
||||
target:
|
||||
fields:
|
||||
image:
|
||||
name: Image
|
||||
description: URL or Full Path of an image
|
||||
required: true
|
||||
example: "https://people.sc.fsu.edu/~jburkardt/data/jpg/lena.jpg"
|
||||
selector:
|
||||
text:
|
||||
obj:
|
||||
name: Object
|
||||
description: Object ID in the format p#b##
|
||||
required: true
|
||||
example: "p1b10"
|
||||
selector:
|
||||
text:
|
||||
width:
|
||||
name: Width
|
||||
description: Resize to width
|
||||
required: false
|
||||
example: "128"
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1280
|
||||
mode: box
|
||||
height:
|
||||
name: Height
|
||||
description: Resize to height
|
||||
required: false
|
||||
example: "128"
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1024
|
||||
mode: box
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"personalize": {
|
||||
"title": "Setup your OpenHASP Plate",
|
||||
"description": "Don't forget to add your plate to YAML configuration",
|
||||
"data": {
|
||||
"name": "Name of the plate",
|
||||
"topic": "OpenHASP MQTT base topic",
|
||||
"idle_brightness": "Brightness level when plate is idle",
|
||||
"path": "Full path to the JSONL file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "A Plate with that name is already configured",
|
||||
"invalid_discovery_info": "Invalid plate topic",
|
||||
"invalid_jsonl_path": "Invalid path to a JSONL file"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Plate already configured",
|
||||
"mismatch_version": "Plate firmware mismatch",
|
||||
"discovery_only": "Configuration is done through discovery"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "openHASP Plate Options",
|
||||
"data": {
|
||||
"idle_brightness": "Brightness level when plate is idle",
|
||||
"path": "Full path to the JSONL file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort":{
|
||||
"invalid_jsonl_path": "Invalid path to a JSONL file"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Allows to configure a switch using GPIO."""
|
||||
import json
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
# pylint: disable=R0801
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
|
||||
from .common import HASPToggleEntity
|
||||
from .const import CONF_HWID, CONF_RELAYS, CONF_TOPIC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
HASP_RELAY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("state"): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=R0801, W0613
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
||||
):
|
||||
"""Set up Plate Relays as switch based on a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
HASPSwitch(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
gpio,
|
||||
)
|
||||
for gpio in entry.data[CONF_RELAYS]
|
||||
]
|
||||
+ [
|
||||
HASPAntiBurn(
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HWID],
|
||||
entry.data[CONF_TOPIC],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HASPSwitch(HASPToggleEntity):
|
||||
"""Representation of an openHASP relay."""
|
||||
|
||||
def __init__(self, name, hwid, topic, gpio):
|
||||
"""Initialize the relay."""
|
||||
super().__init__(name, hwid, topic, gpio)
|
||||
self._attr_name = f"{name} switch {self._gpio}"
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
if self._state is None:
|
||||
# Don't do anything before we know the state
|
||||
return
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/output{self._gpio}",
|
||||
json.dumps(HASP_RELAY_SCHEMA({"state": int(self._state)})),
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def relay_state_message_received(msg):
|
||||
"""Process State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_RELAY_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("%s state = %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/output{self._gpio}", relay_state_message_received
|
||||
)
|
||||
)
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/output{self._gpio}",
|
||||
"",
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
|
||||
class HASPAntiBurn(HASPToggleEntity):
|
||||
"""Configuration switch of an openHASP antiburn feature."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_icon = "mdi:progress-wrench"
|
||||
|
||||
def __init__(self, name, hwid, topic):
|
||||
"""Initialize the protection."""
|
||||
super().__init__(name, hwid, topic, None)
|
||||
self._attr_name = f"{self._name} antiburn"
|
||||
|
||||
async def refresh(self):
|
||||
"""Sync local state back to plate."""
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/antiburn",
|
||||
int(self._state),
|
||||
# json.dumps(HASP_RELAY_SCHEMA({"state": int(self._state)})),
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
async def antiburn_state_message_received(msg):
|
||||
"""Process State."""
|
||||
|
||||
try:
|
||||
self._available = True
|
||||
message = HASP_RELAY_SCHEMA(json.loads(msg.payload))
|
||||
_LOGGER.debug("%s state = %s", self.name, message)
|
||||
|
||||
self._state = message["state"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
except vol.error.Invalid as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
self._subscriptions.append(
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"{self._topic}/state/antiburn", antiburn_state_message_received
|
||||
)
|
||||
)
|
||||
|
||||
self._state = False
|
||||
self._available = True
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
self.hass,
|
||||
f"{self._topic}/command/antiburn",
|
||||
int(self._state),
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"personalize": {
|
||||
"title": "Setup your OpenHASP Plate",
|
||||
"description": "Don't forget to add your plate to YAML configuration",
|
||||
"data": {
|
||||
"name": "Name of the plate",
|
||||
"topic": "OpenHASP MQTT base topic",
|
||||
"idle_brightness": "Brightness level when plate is idle",
|
||||
"path": "Full path to the JSONL file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "A Plate with that name is already configured",
|
||||
"invalid_discovery_info": "Invalid plate topic",
|
||||
"invalid_jsonl_path": "Invalid path to a JSONL file"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Plate already configured",
|
||||
"mismatch_version": "Plate firmware mismatch",
|
||||
"discovery_only": "Configuration is done through discovery"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "openHASP Plate Options",
|
||||
"data": {
|
||||
"idle_brightness": "Brightness level when plate is idle",
|
||||
"path": "Full path to the JSONL file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort":{
|
||||
"invalid_jsonl_path": "Invalid path to a JSONL file"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user