ha-core/homeassistant/components/point/__init__.py

342 lines
11 KiB
Python

"""Support for Minut Point."""
import asyncio
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.helpers import config_validation as cv
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.helpers.typing import HomeAssistantType
from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp
from . import config_flow # noqa pylint_disable=unused-import
from .const import (
CONF_WEBHOOK_URL,
DOMAIN,
EVENT_RECEIVED,
POINT_DISCOVERY_NEW,
SCAN_INTERVAL,
SIGNAL_UPDATE_ENTITY,
SIGNAL_WEBHOOK,
)
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = "client_id"
CONF_CLIENT_SECRET = "client_secret"
DATA_CONFIG_ENTRY_LOCK = "point_config_entry_lock"
CONFIG_ENTRY_IS_SETUP = "point_config_entry_is_setup"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up the Minut Point component."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
config_flow.register_flow_implementation(
hass, DOMAIN, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
)
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Set up Point from a config entry."""
from pypoint import PointSession
def token_saver(token):
_LOGGER.debug("Saving updated token")
entry.data[CONF_TOKEN] = token
hass.config_entries.async_update_entry(entry, data={**entry.data})
# Force token update.
entry.data[CONF_TOKEN]["expires_in"] = -1
session = PointSession(
entry.data["refresh_args"]["client_id"],
token=entry.data[CONF_TOKEN],
auto_refresh_kwargs=entry.data["refresh_args"],
token_saver=token_saver,
)
if not session.is_authorized:
_LOGGER.error("Authentication Error")
return False
hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
await async_setup_webhook(hass, entry, session)
client = MinutPointClient(hass, entry, session)
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client})
hass.async_create_task(client.update())
return True
async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, session):
"""Set up a webhook to handle binary sensor events."""
if CONF_WEBHOOK_ID not in entry.data:
entry.data[CONF_WEBHOOK_ID] = hass.components.webhook.async_generate_id()
entry.data[CONF_WEBHOOK_URL] = hass.components.webhook.async_generate_url(
entry.data[CONF_WEBHOOK_ID]
)
_LOGGER.info("Registering new webhook at: %s", entry.data[CONF_WEBHOOK_URL])
hass.config_entries.async_update_entry(entry, data={**entry.data})
await hass.async_add_executor_job(
session.update_webhook,
entry.data[CONF_WEBHOOK_URL],
entry.data[CONF_WEBHOOK_ID],
["*"],
)
hass.components.webhook.async_register(
DOMAIN, "Point", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
session = hass.data[DOMAIN].pop(entry.entry_id)
await hass.async_add_executor_job(session.remove_webhook)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
for component in ("binary_sensor", "sensor"):
await hass.config_entries.async_forward_entry_unload(entry, component)
return True
async def handle_webhook(hass, webhook_id, request):
"""Handle webhook callback."""
try:
data = await request.json()
_LOGGER.debug("Webhook %s: %s", webhook_id, data)
except ValueError:
return None
if isinstance(data, dict):
data["webhook_id"] = webhook_id
async_dispatcher_send(hass, SIGNAL_WEBHOOK, data, data.get("hook_id"))
hass.bus.async_fire(EVENT_RECEIVED, data)
class MinutPointClient:
"""Get the latest data and update the states."""
def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry, session):
"""Initialize the Minut data object."""
self._known_devices = set()
self._known_homes = set()
self._hass = hass
self._config_entry = config_entry
self._is_available = True
self._client = session
async_track_time_interval(self._hass, self.update, SCAN_INTERVAL)
async def update(self, *args):
"""Periodically poll the cloud for current state."""
await self._sync()
async def _sync(self):
"""Update local list of devices."""
if (
not await self._hass.async_add_executor_job(self._client.update)
and self._is_available
):
self._is_available = False
_LOGGER.warning("Device is unavailable")
return
async def new_device(device_id, component):
"""Load new device."""
config_entries_key = f"{component}.{DOMAIN}"
async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]:
if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]:
await self._hass.config_entries.async_forward_entry_setup(
self._config_entry, component
)
self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
async_dispatcher_send(
self._hass, POINT_DISCOVERY_NEW.format(component, DOMAIN), device_id
)
self._is_available = True
for home_id in self._client.homes:
if home_id not in self._known_homes:
await new_device(home_id, "alarm_control_panel")
self._known_homes.add(home_id)
for device in self._client.devices:
if device.device_id not in self._known_devices:
for component in ("sensor", "binary_sensor"):
await new_device(device.device_id, component)
self._known_devices.add(device.device_id)
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
def device(self, device_id):
"""Return device representation."""
return self._client.device(device_id)
def is_available(self, device_id):
"""Return device availability."""
return device_id in self._client.device_ids
def remove_webhook(self):
"""Remove the session webhook."""
return self._client.remove_webhook()
@property
def homes(self):
"""Return known homes."""
return self._client.homes
def alarm_disarm(self, home_id):
"""Send alarm disarm command."""
return self._client.alarm_disarm(home_id)
def alarm_arm(self, home_id):
"""Send alarm arm command."""
return self._client.alarm_arm(home_id)
class MinutPointEntity(Entity):
"""Base Entity used by the sensors."""
def __init__(self, point_client, device_id, device_class):
"""Initialize the entity."""
self._async_unsub_dispatcher_connect = None
self._client = point_client
self._id = device_id
self._name = self.device.name
self._device_class = device_class
self._updated = utc_from_timestamp(0)
self._value = None
def __str__(self):
"""Return string representation of device."""
return f"MinutPoint {self.name}"
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
_LOGGER.debug("Created device %s", self)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback
)
await self._update_callback()
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
async def _update_callback(self):
"""Update the value of the sensor."""
pass
@property
def available(self):
"""Return true if device is not offline."""
return self._client.is_available(self.device_id)
@property
def device(self):
"""Return the representation of the device."""
return self._client.device(self.device_id)
@property
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def device_id(self):
"""Return the id of the device."""
return self._id
@property
def device_state_attributes(self):
"""Return status of device."""
attrs = self.device.device_status
attrs["last_heard_from"] = as_local(self.last_update).strftime(
"%Y-%m-%d %H:%M:%S"
)
return attrs
@property
def device_info(self):
"""Return a device description for device registry."""
device = self.device.device
return {
"connections": {("mac", device["device_mac"])},
"identifieres": device["device_id"],
"manufacturer": "Minut",
"model": "Point v{}".format(device["hardware_version"]),
"name": device["description"],
"sw_version": device["firmware"]["installed"],
"via_device": (DOMAIN, device["home"]),
}
@property
def name(self):
"""Return the display name of this device."""
return "{} {}".format(self._name, self.device_class.capitalize())
@property
def is_updated(self):
"""Return true if sensor have been updated."""
return self.last_update > self._updated
@property
def last_update(self):
"""Return the last_update time for the device."""
last_update = parse_datetime(self.device.last_update)
return last_update
@property
def should_poll(self):
"""No polling needed for point."""
return False
@property
def unique_id(self):
"""Return the unique id of the sensor."""
return f"point.{self._id}-{self.device_class}"
@property
def value(self):
"""Return the sensor value."""
return self._value