236 lines
7.3 KiB
Python
236 lines
7.3 KiB
Python
"""Component for wiffi support."""
|
|
import asyncio
|
|
from datetime import timedelta
|
|
import errno
|
|
import logging
|
|
|
|
from wiffi import WiffiTcpServer
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import CONF_PORT, CONF_TIMEOUT
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
from homeassistant.helpers import device_registry
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
)
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.event import async_track_time_interval
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
from .const import (
|
|
CHECK_ENTITIES_SIGNAL,
|
|
CREATE_ENTITY_SIGNAL,
|
|
DEFAULT_TIMEOUT,
|
|
DOMAIN,
|
|
UPDATE_ENTITY_SIGNAL,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
PLATFORMS = ["sensor", "binary_sensor"]
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|
"""Set up wiffi from a config entry, config_entry contains data from config entry database."""
|
|
if not config_entry.update_listeners:
|
|
config_entry.add_update_listener(async_update_options)
|
|
|
|
# create api object
|
|
api = WiffiIntegrationApi(hass)
|
|
api.async_setup(config_entry)
|
|
|
|
# store api object
|
|
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = api
|
|
|
|
try:
|
|
await api.server.start_server()
|
|
except OSError as exc:
|
|
if exc.errno != errno.EADDRINUSE:
|
|
_LOGGER.error("Start_server failed, errno: %d", exc.errno)
|
|
return False
|
|
_LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT])
|
|
raise ConfigEntryNotReady from exc
|
|
|
|
for platform in PLATFORMS:
|
|
hass.async_create_task(
|
|
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
|
|
"""Update options."""
|
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|
"""Unload a config entry."""
|
|
api: WiffiIntegrationApi = hass.data[DOMAIN][config_entry.entry_id]
|
|
await api.server.close_server()
|
|
|
|
unload_ok = all(
|
|
await asyncio.gather(
|
|
*[
|
|
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
|
for platform in PLATFORMS
|
|
]
|
|
)
|
|
)
|
|
if unload_ok:
|
|
api = hass.data[DOMAIN].pop(config_entry.entry_id)
|
|
api.shutdown()
|
|
|
|
return unload_ok
|
|
|
|
|
|
def generate_unique_id(device, metric):
|
|
"""Generate a unique string for the entity."""
|
|
return f"{device.mac_address.replace(':', '')}-{metric.name}"
|
|
|
|
|
|
class WiffiIntegrationApi:
|
|
"""API object for wiffi handling. Stored in hass.data."""
|
|
|
|
def __init__(self, hass):
|
|
"""Initialize the instance."""
|
|
self._hass = hass
|
|
self._server = None
|
|
self._known_devices = {}
|
|
self._periodic_callback = None
|
|
|
|
def async_setup(self, config_entry):
|
|
"""Set up api instance."""
|
|
self._server = WiffiTcpServer(config_entry.data[CONF_PORT], self)
|
|
self._periodic_callback = async_track_time_interval(
|
|
self._hass, self._periodic_tick, timedelta(seconds=10)
|
|
)
|
|
|
|
def shutdown(self):
|
|
"""Shutdown wiffi api.
|
|
|
|
Remove listener for periodic callbacks.
|
|
"""
|
|
remove_listener = self._periodic_callback
|
|
if remove_listener is not None:
|
|
remove_listener()
|
|
|
|
async def __call__(self, device, metrics):
|
|
"""Process callback from TCP server if new data arrives from a device."""
|
|
if device.mac_address not in self._known_devices:
|
|
# add empty set for new device
|
|
self._known_devices[device.mac_address] = set()
|
|
|
|
for metric in metrics:
|
|
if metric.id not in self._known_devices[device.mac_address]:
|
|
self._known_devices[device.mac_address].add(metric.id)
|
|
async_dispatcher_send(self._hass, CREATE_ENTITY_SIGNAL, device, metric)
|
|
else:
|
|
async_dispatcher_send(
|
|
self._hass,
|
|
f"{UPDATE_ENTITY_SIGNAL}-{generate_unique_id(device, metric)}",
|
|
device,
|
|
metric,
|
|
)
|
|
|
|
@property
|
|
def server(self):
|
|
"""Return TCP server instance for start + close."""
|
|
return self._server
|
|
|
|
@callback
|
|
def _periodic_tick(self, now=None):
|
|
"""Check if any entity has timed out because it has not been updated."""
|
|
async_dispatcher_send(self._hass, CHECK_ENTITIES_SIGNAL)
|
|
|
|
|
|
class WiffiEntity(Entity):
|
|
"""Common functionality for all wiffi entities."""
|
|
|
|
def __init__(self, device, metric, options):
|
|
"""Initialize the base elements of a wiffi entity."""
|
|
self._id = generate_unique_id(device, metric)
|
|
self._device_info = {
|
|
"connections": {
|
|
(device_registry.CONNECTION_NETWORK_MAC, device.mac_address)
|
|
},
|
|
"identifiers": {(DOMAIN, device.mac_address)},
|
|
"manufacturer": "stall.biz",
|
|
"name": f"{device.moduletype} {device.mac_address}",
|
|
"model": device.moduletype,
|
|
"sw_version": device.sw_version,
|
|
}
|
|
self._name = metric.description
|
|
self._expiration_date = None
|
|
self._value = None
|
|
self._timeout = options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Entity has been added to hass."""
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{UPDATE_ENTITY_SIGNAL}-{self._id}",
|
|
self._update_value_callback,
|
|
)
|
|
)
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass, CHECK_ENTITIES_SIGNAL, self._check_expiration_date
|
|
)
|
|
)
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Disable polling because data driven ."""
|
|
return False
|
|
|
|
@property
|
|
def device_info(self):
|
|
"""Return wiffi device info which is shared between all entities of a device."""
|
|
return self._device_info
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return unique id for entity."""
|
|
return self._id
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return entity name."""
|
|
return self._name
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return true if value is valid."""
|
|
return self._value is not None
|
|
|
|
def reset_expiration_date(self):
|
|
"""Reset value expiration date.
|
|
|
|
Will be called by derived classes after a value update has been received.
|
|
"""
|
|
self._expiration_date = utcnow() + timedelta(minutes=self._timeout)
|
|
|
|
@callback
|
|
def _update_value_callback(self, device, metric):
|
|
"""Update the value of the entity."""
|
|
|
|
@callback
|
|
def _check_expiration_date(self):
|
|
"""Periodically check if entity value has been updated.
|
|
|
|
If there are no more updates from the wiffi device, the value will be
|
|
set to unavailable.
|
|
"""
|
|
if (
|
|
self._value is not None
|
|
and self._expiration_date is not None
|
|
and utcnow() > self._expiration_date
|
|
):
|
|
self._value = None
|
|
self.async_write_ha_state()
|