Add support for Keg and Airlock to Plaato using polling API (#34760)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Johan Nenzén
2021-02-01 18:12:56 +01:00
committed by GitHub
parent 83a75b02ea
commit 285bd3aa91
14 changed files with 1019 additions and 197 deletions

View File

@ -702,7 +702,11 @@ omit =
homeassistant/components/ping/device_tracker.py
homeassistant/components/pioneer/media_player.py
homeassistant/components/pjlink/media_player.py
homeassistant/components/plaato/*
homeassistant/components/plaato/__init__.py
homeassistant/components/plaato/binary_sensor.py
homeassistant/components/plaato/const.py
homeassistant/components/plaato/entity.py
homeassistant/components/plaato/sensor.py
homeassistant/components/plex/media_player.py
homeassistant/components/plum_lightpad/light.py
homeassistant/components/pocketcasts/sensor.py

View File

@ -1,11 +1,34 @@
"""Support for Plaato Airlock."""
"""Support for Plaato devices."""
import asyncio
from datetime import timedelta
import logging
from aiohttp import web
from pyplaato.models.airlock import PlaatoAirlock
from pyplaato.plaato import (
ATTR_ABV,
ATTR_BATCH_VOLUME,
ATTR_BPM,
ATTR_BUBBLES,
ATTR_CO2_VOLUME,
ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_OG,
ATTR_SG,
ATTR_TEMP,
ATTR_TEMP_UNIT,
ATTR_VOLUME_UNIT,
Plaato,
PlaatoDeviceType,
)
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_WEBHOOK_ID,
HTTP_OK,
TEMP_CELSIUS,
@ -13,31 +36,33 @@ from homeassistant.const import (
VOLUME_GALLONS,
VOLUME_LITERS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
from .const import (
CONF_DEVICE_NAME,
CONF_DEVICE_TYPE,
CONF_USE_WEBHOOK,
COORDINATOR,
DEFAULT_SCAN_INTERVAL,
DEVICE,
DEVICE_ID,
DEVICE_NAME,
DEVICE_TYPE,
DOMAIN,
PLATFORMS,
SENSOR_DATA,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["webhook"]
PLAATO_DEVICE_SENSORS = "sensors"
PLAATO_DEVICE_ATTRS = "attrs"
ATTR_DEVICE_ID = "device_id"
ATTR_DEVICE_NAME = "device_name"
ATTR_TEMP_UNIT = "temp_unit"
ATTR_VOLUME_UNIT = "volume_unit"
ATTR_BPM = "bpm"
ATTR_TEMP = "temp"
ATTR_SG = "sg"
ATTR_OG = "og"
ATTR_BUBBLES = "bubbles"
ATTR_ABV = "abv"
ATTR_CO2_VOLUME = "co2_volume"
ATTR_BATCH_VOLUME = "batch_volume"
SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR}"
@ -60,31 +85,124 @@ WEBHOOK_SCHEMA = vol.Schema(
)
async def async_setup(hass, hass_config):
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Plaato component."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass, entry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Configure based on config entry."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
use_webhook = entry.data[CONF_USE_WEBHOOK]
if use_webhook:
async_setup_webhook(hass, entry)
else:
await async_setup_coordinator(hass, entry)
for platform in PLATFORMS:
if entry.options.get(platform, True):
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@callback
def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry):
"""Init webhook based on config entry."""
webhook_id = entry.data[CONF_WEBHOOK_ID]
hass.components.webhook.async_register(DOMAIN, "Plaato", webhook_id, handle_webhook)
device_name = entry.data[CONF_DEVICE_NAME]
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, SENSOR))
_set_entry_data(entry, hass)
return True
hass.components.webhook.async_register(
DOMAIN, f"{DOMAIN}.{device_name}", webhook_id, handle_webhook
)
async def async_unload_entry(hass, entry):
async def async_setup_coordinator(hass: HomeAssistant, entry: ConfigEntry):
"""Init auth token based on config entry."""
auth_token = entry.data[CONF_TOKEN]
device_type = entry.data[CONF_DEVICE_TYPE]
if entry.options.get(CONF_SCAN_INTERVAL):
update_interval = timedelta(minutes=entry.options[CONF_SCAN_INTERVAL])
else:
update_interval = timedelta(minutes=DEFAULT_SCAN_INTERVAL)
coordinator = PlaatoCoordinator(hass, auth_token, device_type, update_interval)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
_set_entry_data(entry, hass, coordinator, auth_token)
for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
def _set_entry_data(entry, hass, coordinator=None, device_id=None):
device = {
DEVICE_NAME: entry.data[CONF_DEVICE_NAME],
DEVICE_TYPE: entry.data[CONF_DEVICE_TYPE],
DEVICE_ID: device_id,
}
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
DEVICE: device,
SENSOR_DATA: None,
UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener),
}
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
hass.data[SENSOR_DATA_KEY]()
use_webhook = entry.data[CONF_USE_WEBHOOK]
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
await hass.config_entries.async_forward_entry_unload(entry, SENSOR)
return True
if use_webhook:
return await async_unload_webhook(hass, entry)
return await async_unload_coordinator(hass, entry)
async def async_unload_webhook(hass: HomeAssistant, entry: ConfigEntry):
"""Unload webhook based entry."""
if entry.data[CONF_WEBHOOK_ID] is not None:
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return await async_unload_platforms(hass, entry, PLATFORMS)
async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry):
"""Unload auth token based entry."""
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
return await async_unload_platforms(hass, entry, coordinator.platforms)
async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms):
"""Unload platforms."""
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in platforms
]
)
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)
return unloaded
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def handle_webhook(hass, webhook_id, request):
@ -96,31 +214,9 @@ async def handle_webhook(hass, webhook_id, request):
return
device_id = _device_id(data)
sensor_data = PlaatoAirlock.from_web_hook(data)
attrs = {
ATTR_DEVICE_NAME: data.get(ATTR_DEVICE_NAME),
ATTR_DEVICE_ID: data.get(ATTR_DEVICE_ID),
ATTR_TEMP_UNIT: data.get(ATTR_TEMP_UNIT),
ATTR_VOLUME_UNIT: data.get(ATTR_VOLUME_UNIT),
}
sensors = {
ATTR_TEMP: data.get(ATTR_TEMP),
ATTR_BPM: data.get(ATTR_BPM),
ATTR_SG: data.get(ATTR_SG),
ATTR_OG: data.get(ATTR_OG),
ATTR_ABV: data.get(ATTR_ABV),
ATTR_CO2_VOLUME: data.get(ATTR_CO2_VOLUME),
ATTR_BATCH_VOLUME: data.get(ATTR_BATCH_VOLUME),
ATTR_BUBBLES: data.get(ATTR_BUBBLES),
}
hass.data[DOMAIN][device_id] = {
PLAATO_DEVICE_ATTRS: attrs,
PLAATO_DEVICE_SENSORS: sensors,
}
async_dispatcher_send(hass, SENSOR_UPDATE, device_id)
async_dispatcher_send(hass, SENSOR_UPDATE, *(device_id, sensor_data))
return web.Response(text=f"Saving status for {device_id}", status=HTTP_OK)
@ -128,3 +224,35 @@ async def handle_webhook(hass, webhook_id, request):
def _device_id(data):
"""Return name of device sensor."""
return f"{data.get(ATTR_DEVICE_NAME)}_{data.get(ATTR_DEVICE_ID)}"
class PlaatoCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass,
auth_token,
device_type: PlaatoDeviceType,
update_interval: timedelta,
):
"""Initialize."""
self.api = Plaato(auth_token=auth_token)
self.hass = hass
self.device_type = device_type
self.platforms = []
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=update_interval,
)
async def _async_update_data(self):
"""Update data via library."""
data = await self.api.get_data(
session=aiohttp_client.async_get_clientsession(self.hass),
device_type=self.device_type,
)
return data

View File

@ -0,0 +1,56 @@
"""Support for Plaato Airlock sensors."""
import logging
from pyplaato.plaato import PlaatoKeg
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OPENING,
DEVICE_CLASS_PROBLEM,
BinarySensorEntity,
)
from .const import CONF_USE_WEBHOOK, COORDINATOR, DOMAIN
from .entity import PlaatoEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plaato from a config entry."""
if config_entry.data[CONF_USE_WEBHOOK]:
return False
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
async_add_entities(
PlaatoBinarySensor(
hass.data[DOMAIN][config_entry.entry_id],
sensor_type,
coordinator,
)
for sensor_type in coordinator.data.binary_sensors.keys()
)
return True
class PlaatoBinarySensor(PlaatoEntity, BinarySensorEntity):
"""Representation of a Binary Sensor."""
@property
def is_on(self):
"""Return true if the binary sensor is on."""
if self._coordinator is not None:
return self._coordinator.data.binary_sensors.get(self._sensor_type)
return False
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
if self._coordinator is None:
return None
if self._sensor_type is PlaatoKeg.Pins.LEAK_DETECTION:
return DEVICE_CLASS_PROBLEM
if self._sensor_type is PlaatoKeg.Pins.POURING:
return DEVICE_CLASS_OPENING

View File

@ -1,10 +1,223 @@
"""Config flow for GPSLogger."""
from homeassistant.helpers import config_entry_flow
"""Config flow for Plaato."""
import logging
from .const import DOMAIN
from pyplaato.plaato import PlaatoDeviceType
import voluptuous as vol
config_entry_flow.register_webhook_flow(
DOMAIN,
"Webhook",
{"docs_url": "https://www.home-assistant.io/integrations/plaato/"},
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .const import (
CONF_CLOUDHOOK,
CONF_DEVICE_NAME,
CONF_DEVICE_TYPE,
CONF_USE_WEBHOOK,
DEFAULT_SCAN_INTERVAL,
DOCS_URL,
PLACEHOLDER_DEVICE_NAME,
PLACEHOLDER_DEVICE_TYPE,
PLACEHOLDER_DOCS_URL,
PLACEHOLDER_WEBHOOK_URL,
)
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__package__)
class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handles a Plaato config flow."""
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize."""
self._init_info = {}
async def async_step_user(self, user_input=None):
"""Handle user step."""
if user_input is not None:
self._init_info[CONF_DEVICE_TYPE] = PlaatoDeviceType(
user_input[CONF_DEVICE_TYPE]
)
self._init_info[CONF_DEVICE_NAME] = user_input[CONF_DEVICE_NAME]
return await self.async_step_api_method()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(
CONF_DEVICE_NAME,
default=self._init_info.get(CONF_DEVICE_NAME, None),
): str,
vol.Required(
CONF_DEVICE_TYPE,
default=self._init_info.get(CONF_DEVICE_TYPE, None),
): vol.In(list(PlaatoDeviceType)),
}
),
)
async def async_step_api_method(self, user_input=None):
"""Handle device type step."""
device_type = self._init_info[CONF_DEVICE_TYPE]
if user_input is not None:
token = user_input.get(CONF_TOKEN, None)
use_webhook = user_input.get(CONF_USE_WEBHOOK, False)
if not token and not use_webhook:
errors = {"base": PlaatoConfigFlow._get_error(device_type)}
return await self._show_api_method_form(device_type, errors)
self._init_info[CONF_USE_WEBHOOK] = use_webhook
self._init_info[CONF_TOKEN] = token
return await self.async_step_webhook()
return await self._show_api_method_form(device_type)
async def async_step_webhook(self, user_input=None):
"""Validate config step."""
use_webhook = self._init_info[CONF_USE_WEBHOOK]
if use_webhook and user_input is None:
webhook_id, webhook_url, cloudhook = await self._get_webhook_id()
self._init_info[CONF_WEBHOOK_ID] = webhook_id
self._init_info[CONF_CLOUDHOOK] = cloudhook
return self.async_show_form(
step_id="webhook",
description_placeholders={
PLACEHOLDER_WEBHOOK_URL: webhook_url,
PLACEHOLDER_DOCS_URL: DOCS_URL,
},
)
return await self._async_create_entry()
async def _async_create_entry(self):
"""Create the entry step."""
webhook_id = self._init_info.get(CONF_WEBHOOK_ID, None)
auth_token = self._init_info[CONF_TOKEN]
device_name = self._init_info[CONF_DEVICE_NAME]
device_type = self._init_info[CONF_DEVICE_TYPE]
unique_id = auth_token if auth_token else webhook_id
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=device_type.name,
data=self._init_info,
description_placeholders={
PLACEHOLDER_DEVICE_TYPE: device_type.name,
PLACEHOLDER_DEVICE_NAME: device_name,
},
)
async def _show_api_method_form(
self, device_type: PlaatoDeviceType, errors: dict = None
):
data_scheme = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str})
if device_type == PlaatoDeviceType.Airlock:
data_scheme = data_scheme.extend(
{vol.Optional(CONF_USE_WEBHOOK, default=False): bool}
)
return self.async_show_form(
step_id="api_method",
data_schema=data_scheme,
errors=errors,
description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name},
)
async def _get_webhook_id(self):
"""Generate webhook ID."""
webhook_id = self.hass.components.webhook.async_generate_id()
if self.hass.components.cloud.async_active_subscription():
webhook_url = await self.hass.components.cloud.async_create_cloudhook(
webhook_id
)
cloudhook = True
else:
webhook_url = self.hass.components.webhook.async_generate_url(webhook_id)
cloudhook = False
return webhook_id, webhook_url, cloudhook
@staticmethod
def _get_error(device_type: PlaatoDeviceType):
if device_type == PlaatoDeviceType.Airlock:
return "no_api_method"
return "no_auth_token"
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return PlaatoOptionsFlowHandler(config_entry)
class PlaatoOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Plaato options."""
def __init__(self, config_entry: ConfigEntry):
"""Initialize domain options flow."""
super().__init__()
self._config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
use_webhook = self._config_entry.data.get(CONF_USE_WEBHOOK, False)
if use_webhook:
return await self.async_step_webhook()
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(
CONF_SCAN_INTERVAL,
default=self._config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): cv.positive_int
}
),
)
async def async_step_webhook(self, user_input=None):
"""Manage the options for webhook device."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
webhook_id = self._config_entry.data.get(CONF_WEBHOOK_ID, None)
webhook_url = (
""
if webhook_id is None
else self.hass.components.webhook.async_generate_url(webhook_id)
)
return self.async_show_form(
step_id="webhook",
description_placeholders={PLACEHOLDER_WEBHOOK_URL: webhook_url},
)

View File

@ -1,3 +1,27 @@
"""Const for GPSLogger."""
"""Const for Plaato."""
from datetime import timedelta
DOMAIN = "plaato"
PLAATO_DEVICE_SENSORS = "sensors"
PLAATO_DEVICE_ATTRS = "attrs"
SENSOR_SIGNAL = f"{DOMAIN}_%s_%s"
CONF_USE_WEBHOOK = "use_webhook"
CONF_DEVICE_TYPE = "device_type"
CONF_DEVICE_NAME = "device_name"
CONF_CLOUDHOOK = "cloudhook"
PLACEHOLDER_WEBHOOK_URL = "webhook_url"
PLACEHOLDER_DOCS_URL = "docs_url"
PLACEHOLDER_DEVICE_TYPE = "device_type"
PLACEHOLDER_DEVICE_NAME = "device_name"
DOCS_URL = "https://www.home-assistant.io/integrations/plaato/"
PLATFORMS = ["sensor", "binary_sensor"]
SENSOR_DATA = "sensor_data"
COORDINATOR = "coordinator"
DEVICE = "device"
DEVICE_NAME = "device_name"
DEVICE_TYPE = "device_type"
DEVICE_ID = "device_id"
UNDO_UPDATE_LISTENER = "undo_update_listener"
DEFAULT_SCAN_INTERVAL = 5
MIN_UPDATE_INTERVAL = timedelta(minutes=1)

View File

@ -0,0 +1,103 @@
"""PlaatoEntity class."""
from pyplaato.models.device import PlaatoDevice
from homeassistant.helpers import entity
from .const import (
DEVICE,
DEVICE_ID,
DEVICE_NAME,
DEVICE_TYPE,
DOMAIN,
SENSOR_DATA,
SENSOR_SIGNAL,
)
class PlaatoEntity(entity.Entity):
"""Representation of a Plaato Entity."""
def __init__(self, data, sensor_type, coordinator=None):
"""Initialize the sensor."""
self._coordinator = coordinator
self._entry_data = data
self._sensor_type = sensor_type
self._device_id = data[DEVICE][DEVICE_ID]
self._device_type = data[DEVICE][DEVICE_TYPE]
self._device_name = data[DEVICE][DEVICE_NAME]
self._state = 0
@property
def _attributes(self) -> dict:
return PlaatoEntity._to_snake_case(self._sensor_data.attributes)
@property
def _sensor_name(self) -> str:
return self._sensor_data.get_sensor_name(self._sensor_type)
@property
def _sensor_data(self) -> PlaatoDevice:
if self._coordinator:
return self._coordinator.data
return self._entry_data[SENSOR_DATA]
@property
def name(self):
"""Return the name of the sensor."""
return f"{DOMAIN} {self._device_type} {self._device_name} {self._sensor_name}".title()
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return f"{self._device_id}_{self._sensor_type}"
@property
def device_info(self):
"""Get device info."""
device_info = {
"identifiers": {(DOMAIN, self._device_id)},
"name": self._device_name,
"manufacturer": "Plaato",
"model": self._device_type,
}
if self._sensor_data.firmware_version != "":
device_info["sw_version"] = self._sensor_data.firmware_version
return device_info
@property
def device_state_attributes(self):
"""Return the state attributes of the monitored installation."""
if self._attributes is not None:
return self._attributes
@property
def available(self):
"""Return if sensor is available."""
if self._coordinator is not None:
return self._coordinator.last_update_success
return True
@property
def should_poll(self):
"""Return the polling state."""
return False
async def async_added_to_hass(self):
"""When entity is added to hass."""
if self._coordinator is not None:
self.async_on_remove(
self._coordinator.async_add_listener(self.async_write_ha_state)
)
else:
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
SENSOR_SIGNAL % (self._device_id, self._sensor_type),
self.async_write_ha_state,
)
)
@staticmethod
def _to_snake_case(dictionary: dict):
return {k.lower().replace(" ", "_"): v for k, v in dictionary.items()}

View File

@ -1,8 +1,10 @@
{
"domain": "plaato",
"name": "Plaato Airlock",
"name": "Plaato",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plaato",
"dependencies": ["webhook"],
"codeowners": ["@JohNan"]
"after_dependencies": ["cloud"],
"codeowners": ["@JohNan"],
"requirements": ["pyplaato==0.0.15"]
}

View File

@ -1,28 +1,29 @@
"""Support for Plaato Airlock sensors."""
import logging
from typing import Optional
from homeassistant.const import PERCENTAGE
from pyplaato.models.device import PlaatoDevice
from pyplaato.plaato import PlaatoKeg
from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from . import (
ATTR_ABV,
ATTR_BATCH_VOLUME,
ATTR_BPM,
ATTR_CO2_VOLUME,
ATTR_TEMP,
ATTR_TEMP_UNIT,
ATTR_VOLUME_UNIT,
DOMAIN as PLAATO_DOMAIN,
PLAATO_DEVICE_ATTRS,
PLAATO_DEVICE_SENSORS,
SENSOR_DATA_KEY,
SENSOR_UPDATE,
from . import ATTR_TEMP, SENSOR_UPDATE
from ...core import callback
from .const import (
CONF_USE_WEBHOOK,
COORDINATOR,
DEVICE,
DEVICE_ID,
DOMAIN,
SENSOR_DATA,
SENSOR_SIGNAL,
)
from .entity import PlaatoEntity
_LOGGER = logging.getLogger(__name__)
@ -31,134 +32,58 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""Set up the Plaato sensor."""
async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Plaato from a config entry."""
devices = {}
entry_data = hass.data[DOMAIN][entry.entry_id]
def get_device(device_id):
"""Get a device."""
return hass.data[PLAATO_DOMAIN].get(device_id, False)
def get_device_sensors(device_id):
"""Get device sensors."""
return hass.data[PLAATO_DOMAIN].get(device_id).get(PLAATO_DEVICE_SENSORS)
async def _update_sensor(device_id):
@callback
async def _async_update_from_webhook(device_id, sensor_data: PlaatoDevice):
"""Update/Create the sensors."""
if device_id not in devices and get_device(device_id):
entities = []
sensors = get_device_sensors(device_id)
entry_data[SENSOR_DATA] = sensor_data
for sensor_type in sensors:
entities.append(PlaatoSensor(device_id, sensor_type))
devices[device_id] = entities
async_add_entities(entities, True)
if device_id != entry_data[DEVICE][DEVICE_ID]:
entry_data[DEVICE][DEVICE_ID] = device_id
async_add_entities(
[
PlaatoSensor(entry_data, sensor_type)
for sensor_type in sensor_data.sensors
]
)
else:
for entity in devices[device_id]:
async_dispatcher_send(hass, f"{PLAATO_DOMAIN}_{entity.unique_id}")
for sensor_type in sensor_data.sensors:
async_dispatcher_send(hass, SENSOR_SIGNAL % (device_id, sensor_type))
hass.data[SENSOR_DATA_KEY] = async_dispatcher_connect(
hass, SENSOR_UPDATE, _update_sensor
)
if entry.data[CONF_USE_WEBHOOK]:
async_dispatcher_connect(hass, SENSOR_UPDATE, _async_update_from_webhook)
else:
coordinator = entry_data[COORDINATOR]
async_add_entities(
PlaatoSensor(entry_data, sensor_type, coordinator)
for sensor_type in coordinator.data.sensors.keys()
)
return True
class PlaatoSensor(Entity):
"""Representation of a Sensor."""
def __init__(self, device_id, sensor_type):
"""Initialize the sensor."""
self._device_id = device_id
self._type = sensor_type
self._state = 0
self._name = f"{device_id} {sensor_type}"
self._attributes = None
class PlaatoSensor(PlaatoEntity):
"""Representation of a Plaato Sensor."""
@property
def name(self):
"""Return the name of the sensor."""
return f"{PLAATO_DOMAIN} {self._name}"
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return f"{self._device_id}_{self._type}"
@property
def device_info(self):
"""Get device info."""
return {
"identifiers": {(PLAATO_DOMAIN, self._device_id)},
"name": self._device_id,
"manufacturer": "Plaato",
"model": "Airlock",
}
def get_sensors(self):
"""Get device sensors."""
return (
self.hass.data[PLAATO_DOMAIN]
.get(self._device_id)
.get(PLAATO_DEVICE_SENSORS, False)
)
def get_sensors_unit_of_measurement(self, sensor_type):
"""Get unit of measurement for sensor of type."""
return (
self.hass.data[PLAATO_DOMAIN]
.get(self._device_id)
.get(PLAATO_DEVICE_ATTRS, [])
.get(sensor_type, "")
)
def device_class(self) -> Optional[str]:
"""Return the class of this device, from component DEVICE_CLASSES."""
if self._coordinator is not None:
if self._sensor_type == PlaatoKeg.Pins.TEMPERATURE:
return DEVICE_CLASS_TEMPERATURE
if self._sensor_type == ATTR_TEMP:
return DEVICE_CLASS_TEMPERATURE
return None
@property
def state(self):
"""Return the state of the sensor."""
sensors = self.get_sensors()
if sensors is False:
_LOGGER.debug("Device with name %s has no sensors", self.name)
return 0
if self._type == ATTR_ABV:
return round(sensors.get(self._type), 2)
if self._type == ATTR_TEMP:
return round(sensors.get(self._type), 1)
if self._type == ATTR_CO2_VOLUME:
return round(sensors.get(self._type), 2)
return sensors.get(self._type)
@property
def device_state_attributes(self):
"""Return the state attributes of the monitored installation."""
if self._attributes is not None:
return self._attributes
return self._sensor_data.sensors.get(self._sensor_type)
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
if self._type == ATTR_TEMP:
return self.get_sensors_unit_of_measurement(ATTR_TEMP_UNIT)
if self._type == ATTR_BATCH_VOLUME or self._type == ATTR_CO2_VOLUME:
return self.get_sensors_unit_of_measurement(ATTR_VOLUME_UNIT)
if self._type == ATTR_BPM:
return "bpm"
if self._type == ATTR_ABV:
return PERCENTAGE
return ""
@property
def should_poll(self):
"""Return the polling state."""
return False
async def async_added_to_hass(self):
"""Register callbacks."""
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_write_ha_state
)
)
return self._sensor_data.get_unit_of_measurement(self._sensor_type)

View File

@ -2,16 +2,53 @@
"config": {
"step": {
"user": {
"title": "Set up the Plaato Webhook",
"description": "[%key:common::config_flow::description::confirm_setup%]"
"title": "Set up the Plaato devices",
"description": "[%key:common::config_flow::description::confirm_setup%]",
"data": {
"device_name": "Name your device",
"device_type": "Type of Plaato device"
}
},
"api_method": {
"title": "Select API method",
"description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank",
"data": {
"use_webhook": "Use webhook",
"token": "Paste Auth Token here"
}
},
"webhook": {
"title": "Webhook to use",
"description": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
}
},
"error": {
"invalid_webhook_device": "You have selected a device that doesn't not support sending data to a webhook. It is only available for the Airlock",
"no_auth_token": "You need to add an auth token",
"no_api_method": "You need to add an auth token or select webhook"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
"default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!"
}
},
"options": {
"step": {
"webhook": {
"title": "Options for Plaato Airlock",
"description": "Webhook info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n"
},
"user": {
"title": "Options for Plaato",
"description": "Set the update interval (minutes)",
"data": {
"update_interval": "Update interval (minutes)"
}
}
}
}
}

View File

@ -1,16 +1,53 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured",
"single_instance_allowed": "Already configured. Only a single configuration possible.",
"webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
"default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!"
},
"error": {
"invalid_webhook_device": "You have selected a device that doesn't not support sending data to a webhook. It is only available for the Airlock",
"no_api_method": "You need to add an auth token or select webhook",
"no_auth_token": "You need to add an auth token"
},
"step": {
"api_method": {
"data": {
"token": "Paste Auth Token here",
"use_webhook": "Use webhook"
},
"description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank",
"title": "Select API method"
},
"user": {
"data": {
"device_name": "Name your device",
"device_type": "Type of Plaato device"
},
"description": "Do you want to start set up?",
"title": "Set up the Plaato Webhook"
"title": "Set up the Plaato devices"
},
"webhook": {
"description": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
"title": "Webhook to use"
}
}
},
"options": {
"step": {
"user": {
"data": {
"update_interval": "Update interval (minutes)"
},
"description": "Set the update interval (minutes)",
"title": "Options for Plaato"
},
"webhook": {
"description": "Webhook info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n",
"title": "Options for Plaato Airlock"
}
}
}

View File

@ -1615,6 +1615,9 @@ pypck==0.7.9
# homeassistant.components.pjlink
pypjlink2==1.2.1
# homeassistant.components.plaato
pyplaato==0.0.15
# homeassistant.components.point
pypoint==2.0.0

View File

@ -842,6 +842,9 @@ pyowm==3.1.1
# homeassistant.components.onewire
pyownet==0.10.0.post1
# homeassistant.components.plaato
pyplaato==0.0.15
# homeassistant.components.point
pypoint==2.0.0

View File

@ -0,0 +1 @@
"""Tests for the Plaato integration."""

View File

@ -0,0 +1,286 @@
"""Test the Plaato config flow."""
from unittest.mock import patch
from pyplaato.models.device import PlaatoDeviceType
import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.plaato.const import (
CONF_DEVICE_NAME,
CONF_DEVICE_TYPE,
CONF_USE_WEBHOOK,
DOMAIN,
)
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from tests.common import MockConfigEntry
BASE_URL = "http://example.com"
WEBHOOK_ID = "webhook_id"
UNIQUE_ID = "plaato_unique_id"
@pytest.fixture(name="webhook_id")
def mock_webhook_id():
"""Mock webhook_id."""
with patch(
"homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID
), patch(
"homeassistant.components.webhook.async_generate_url", return_value="hook_id"
):
yield
async def test_show_config_form(hass):
"""Test show configuration form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
async def test_show_config_form_device_type_airlock(hass):
"""Test show configuration form."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
CONF_DEVICE_NAME: "device_name",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
assert result["data_schema"].schema.get(CONF_TOKEN) == str
assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool
async def test_show_config_form_device_type_keg(hass):
"""Test show configuration form."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
assert result["data_schema"].schema.get(CONF_TOKEN) == str
assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None
async def test_show_config_form_validate_webhook(hass, webhook_id):
"""Test show configuration form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
CONF_DEVICE_NAME: "device_name",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
async def return_async_value(val):
return val
hass.config.components.add("cloud")
with patch(
"homeassistant.components.cloud.async_active_subscription", return_value=True
), patch(
"homeassistant.components.cloud.async_create_cloudhook",
return_value=return_async_value("https://hooks.nabu.casa/ABCD"),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_TOKEN: "",
CONF_USE_WEBHOOK: True,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "webhook"
async def test_show_config_form_validate_token(hass):
"""Test show configuration form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE_TYPE: PlaatoDeviceType.Keg,
CONF_DEVICE_NAME: "device_name",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
with patch("homeassistant.components.plaato.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_TOKEN: "valid_token"}
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == PlaatoDeviceType.Keg.name
assert result["data"] == {
CONF_USE_WEBHOOK: False,
CONF_TOKEN: "valid_token",
CONF_DEVICE_TYPE: PlaatoDeviceType.Keg,
CONF_DEVICE_NAME: "device_name",
}
async def test_show_config_form_no_cloud_webhook(hass, webhook_id):
"""Test show configuration form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
CONF_DEVICE_NAME: "device_name",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_USE_WEBHOOK: True,
CONF_TOKEN: "",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "webhook"
assert result["errors"] is None
async def test_show_config_form_api_method_no_auth_token(hass, webhook_id):
"""Test show configuration form."""
# Using Keg
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE_TYPE: PlaatoDeviceType.Keg,
CONF_DEVICE_NAME: "device_name",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_TOKEN: ""}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
assert len(result["errors"]) == 1
assert result["errors"]["base"] == "no_auth_token"
# Using Airlock
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
CONF_DEVICE_NAME: "device_name",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_TOKEN: ""}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_method"
assert len(result["errors"]) == 1
assert result["errors"]["base"] == "no_api_method"
async def test_options(hass):
"""Test updating options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title="NAME",
data={},
options={CONF_SCAN_INTERVAL: 5},
)
config_entry.add_to_hass(hass)
with patch("homeassistant.components.plaato.async_setup_entry", return_value=True):
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_SCAN_INTERVAL: 10},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_SCAN_INTERVAL] == 10
async def test_options_webhook(hass, webhook_id):
"""Test updating options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title="NAME",
data={CONF_USE_WEBHOOK: True, CONF_WEBHOOK_ID: None},
options={CONF_SCAN_INTERVAL: 5},
)
config_entry.add_to_hass(hass)
with patch("homeassistant.components.plaato.async_setup_entry", return_value=True):
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "webhook"
assert result["description_placeholders"] == {"webhook_url": ""}
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_WEBHOOK_ID: WEBHOOK_ID},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID