homeassistant/custom_components/nodered/__init__.py

259 lines
7.8 KiB
Python

"""
Component to integrate with node-red.
For more details about this component, please refer to
https://github.com/zachowj/hass-node-red
"""
import asyncio
import logging
import os
from typing import Any, Dict, Optional, Union
from integrationhelper.const import CC_STARTUP_VERSION
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_STATE,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from .const import (
CONF_COMPONENT,
CONF_CONFIG,
CONF_DEVICE_INFO,
CONF_NAME,
CONF_NODE_ID,
CONF_REMOVE,
CONF_SERVER_ID,
CONF_VERSION,
DEFAULT_NAME,
DOMAIN,
DOMAIN_DATA,
ISSUE_URL,
NODERED_DISCOVERY_UPDATED,
NODERED_ENTITY,
REQUIRED_FILES,
VERSION,
)
from .discovery import (
ALREADY_DISCOVERED,
CHANGE_ENTITY_TYPE,
CONFIG_ENTRY_IS_SETUP,
NODERED_DISCOVERY,
start_discovery,
stop_discovery,
)
from .websocket import register_websocket_handlers
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Stub to allow setting up this component.
Configuration through YAML is not supported.
"""
return True
async def async_setup_entry(hass, config_entry):
"""Set up this integration using UI."""
# Print startup message
_LOGGER.info(
CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL)
)
# Check that all required files are present
file_check = await hass.async_add_executor_job(check_files, hass)
if not file_check:
return False
# Create DATA dict
hass.data[DOMAIN_DATA] = {}
register_websocket_handlers(hass)
await start_discovery(hass, hass.data[DOMAIN_DATA], config_entry)
hass.bus.async_fire(DOMAIN, {CONF_TYPE: "loaded", CONF_VERSION: VERSION})
return True
def check_files(hass):
"""Return bool that indicates if all files are present."""
# Verify that the user downloaded all files.
base = f"{hass.config.path()}/custom_components/{DOMAIN}/"
missing = []
for file in REQUIRED_FILES:
fullpath = "{}{}".format(base, file)
if not os.path.exists(fullpath):
missing.append(file)
if missing:
_LOGGER.critical(f"The following files are missing: {str(missing)}")
returnvalue = False
else:
returnvalue = True
return returnvalue
async def async_remove_entry(hass, config_entry):
"""Handle removal of an entry."""
if hass.data[DOMAIN_DATA][CONFIG_ENTRY_IS_SETUP]:
await asyncio.wait(
[
hass.config_entries.async_forward_entry_unload(config_entry, platform)
for platform in hass.data[DOMAIN_DATA][CONFIG_ENTRY_IS_SETUP]
]
)
stop_discovery(hass)
del hass.data[DOMAIN_DATA]
hass.bus.async_fire(DOMAIN, {CONF_TYPE: "unloaded"})
class NodeRedEntity(Entity):
"""nodered Sensor class."""
def __init__(self, hass, config):
"""Initialize the entity."""
self.hass = hass
self.attr = {}
self._config = config[CONF_CONFIG]
self._component = None
self._device_info = config.get(CONF_DEVICE_INFO)
self._state = None
self._server_id = config[CONF_SERVER_ID]
self._node_id = config[CONF_NODE_ID]
self._remove_signal_discovery_update = None
self._remove_signal_entity_update = None
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return False
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID to use for this sensor."""
return f"{DOMAIN}-{self._server_id}-{self._node_id}"
@property
def device_class(self) -> Optional[str]:
"""Return the class of this binary_sensor."""
return self._config.get(CONF_DEVICE_CLASS)
@property
def name(self) -> Optional[str]:
"""Return the name of the sensor."""
return self._config.get(CONF_NAME, f"{DEFAULT_NAME} {self._node_id}")
@property
def state(self) -> Union[None, str, int, float]:
"""Return the state of the sensor."""
return self._state
@property
def icon(self) -> Optional[str]:
"""Return the icon of the sensor."""
return self._config.get(CONF_ICON)
@property
def unit_of_measurement(self) -> Optional[str]:
"""Return the unit this state is expressed in."""
return self._config.get(CONF_UNIT_OF_MEASUREMENT)
@property
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the state attributes."""
return self.attr
@property
def device_info(self) -> Optional[Dict[str, Any]]:
"""Return device specific attributes."""
info = None
if self._device_info is not None and "id" in self._device_info:
# Use the id property to create the device identifier then delete it
info = {"identifiers": {(DOMAIN, self._device_info["id"])}}
del self._device_info["id"]
info.update(self._device_info)
return info
@callback
def handle_entity_update(self, msg):
"""Update entity state."""
_LOGGER.debug(f"Entity Update: {msg}")
self.attr = msg.get("attributes", {})
self._state = msg[CONF_STATE]
self.async_write_ha_state()
@callback
def handle_discovery_update(self, msg, connection):
"""Update entity config."""
if CONF_REMOVE not in msg:
self._config = msg[CONF_CONFIG]
self.async_write_ha_state()
return
# Otherwise, remove entity
if msg[CONF_REMOVE] == CHANGE_ENTITY_TYPE:
# recreate entity if component type changed
@callback
def recreate_entity():
"""Create entity with new type."""
del msg[CONF_REMOVE]
async_dispatcher_send(
self.hass,
NODERED_DISCOVERY.format(msg[CONF_COMPONENT]),
msg,
connection,
)
self.async_on_remove(recreate_entity)
self.hass.async_create_task(self.async_remove())
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self._remove_signal_entity_update = async_dispatcher_connect(
self.hass,
NODERED_ENTITY.format(self._server_id, self._node_id),
self.handle_entity_update,
)
self._remove_signal_discovery_update = async_dispatcher_connect(
self.hass,
NODERED_DISCOVERY_UPDATED.format(self.unique_id),
self.handle_discovery_update,
)
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
if self._remove_signal_entity_update is not None:
self._remove_signal_entity_update()
if self._remove_signal_discovery_update is not None:
self._remove_signal_discovery_update()
del self.hass.data[DOMAIN_DATA][ALREADY_DISCOVERED][self.unique_id]
# Remove the entity_id from the entity registry
registry = await self.hass.helpers.entity_registry.async_get_registry()
entity_id = registry.async_get_entity_id(
self._component, DOMAIN, self.unique_id,
)
if entity_id:
registry.async_remove(entity_id)