From 266b3bc7144e1054c2f97d33e77915d073488954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Tue, 18 Jun 2019 05:23:59 +0200 Subject: [PATCH] Adds integration for Plaato Airlock (#23727) * Adds integration for Plaato Airlock * Updates codeowners and coveragerc * Fixes lint errors * Fixers lint check error * Removed sv translation file * Adds en translation file * Sets config flow to true in manifest * Moves config flow and domain to seperate files * Fixes lint errors * Runs hassfest to regenerate config_flows.py * Adds should poll property and fixes for loop * Only log a warning when webhook data was broken * Fixes static test failure * Moves state update from async_update to state prop * Unsubscribes the dispatch signal listener * Update sensor.py --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/plaato/.translations/en.json | 18 +++ homeassistant/components/plaato/__init__.py | 126 ++++++++++++++++ .../components/plaato/config_flow.py | 11 ++ homeassistant/components/plaato/const.py | 3 + homeassistant/components/plaato/manifest.json | 9 ++ homeassistant/components/plaato/sensor.py | 139 ++++++++++++++++++ homeassistant/components/plaato/strings.json | 18 +++ homeassistant/generated/config_flows.py | 1 + 10 files changed, 327 insertions(+) create mode 100644 homeassistant/components/plaato/.translations/en.json create mode 100644 homeassistant/components/plaato/__init__.py create mode 100644 homeassistant/components/plaato/config_flow.py create mode 100644 homeassistant/components/plaato/const.py create mode 100644 homeassistant/components/plaato/manifest.json create mode 100644 homeassistant/components/plaato/sensor.py create mode 100644 homeassistant/components/plaato/strings.json diff --git a/.coveragerc b/.coveragerc index 7e618b9d4b3..c8213378e91 100644 --- a/.coveragerc +++ b/.coveragerc @@ -451,6 +451,7 @@ omit = homeassistant/components/ping/device_tracker.py homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py + homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plum_lightpad/* diff --git a/CODEOWNERS b/CODEOWNERS index a6b1b44e34c..fb6b204253b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -191,6 +191,7 @@ homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff +homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 diff --git a/homeassistant/components/plaato/.translations/en.json b/homeassistant/components/plaato/.translations/en.json new file mode 100644 index 00000000000..6d3aa2c59c4 --- /dev/null +++ b/homeassistant/components/plaato/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Plaato Airlock.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "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." + }, + "step": { + "user": { + "description": "Are you sure you want to set up the Plaato Airlock?", + "title": "Set up the Plaato Webhook" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py new file mode 100644 index 00000000000..9857ef47b1c --- /dev/null +++ b/homeassistant/components/plaato/__init__.py @@ -0,0 +1,126 @@ +"""Support for Plaato Airlock.""" +import logging + +from aiohttp import web +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.const import ( + CONF_WEBHOOK_ID, HTTP_OK, + TEMP_CELSIUS, TEMP_FAHRENHEIT, VOLUME_GALLONS, VOLUME_LITERS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from .const import DOMAIN + +_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 = '{}_sensor_update'.format(DOMAIN) +SENSOR_DATA_KEY = '{}.{}'.format(DOMAIN, SENSOR) + +WEBHOOK_SCHEMA = vol.Schema({ + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_DEVICE_ID): cv.positive_int, + vol.Required(ATTR_TEMP_UNIT): vol.Any(TEMP_CELSIUS, TEMP_FAHRENHEIT), + vol.Required(ATTR_VOLUME_UNIT): vol.Any(VOLUME_LITERS, VOLUME_GALLONS), + vol.Required(ATTR_BPM): cv.positive_int, + vol.Required(ATTR_TEMP): vol.Coerce(float), + vol.Required(ATTR_SG): vol.Coerce(float), + vol.Required(ATTR_OG): vol.Coerce(float), + vol.Required(ATTR_ABV): vol.Coerce(float), + vol.Required(ATTR_CO2_VOLUME): vol.Coerce(float), + vol.Required(ATTR_BATCH_VOLUME): vol.Coerce(float), + vol.Required(ATTR_BUBBLES): cv.positive_int, +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, hass_config): + """Set up the Plaato component.""" + return True + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + webhook_id = entry.data[CONF_WEBHOOK_ID] + hass.components.webhook.async_register( + DOMAIN, 'Plaato', webhook_id, handle_webhook) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SENSOR) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + hass.data[SENSOR_DATA_KEY]() + + await hass.config_entries.async_forward_entry_unload(entry, SENSOR) + return True + + +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook from Plaato.""" + try: + data = WEBHOOK_SCHEMA(await request.json()) + except vol.MultipleInvalid as error: + _LOGGER.warning("An error occurred when parsing webhook data <%s>", + error) + return + + device_id = _device_id(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) + + return web.Response( + text="Saving status for {}".format(device_id), status=HTTP_OK) + + +def _device_id(data): + """Return name of device sensor.""" + return "{}_{}".format(data.get(ATTR_DEVICE_NAME), data.get(ATTR_DEVICE_ID)) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py new file mode 100644 index 00000000000..c3f9279df05 --- /dev/null +++ b/homeassistant/components/plaato/config_flow.py @@ -0,0 +1,11 @@ +"""Config flow for GPSLogger.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/plaato/' + } +) diff --git a/homeassistant/components/plaato/const.py b/homeassistant/components/plaato/const.py new file mode 100644 index 00000000000..f683ddb664c --- /dev/null +++ b/homeassistant/components/plaato/const.py @@ -0,0 +1,3 @@ +"""Const for GPSLogger.""" + +DOMAIN = 'plaato' diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json new file mode 100644 index 00000000000..cd6111ba9da --- /dev/null +++ b/homeassistant/components/plaato/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "plaato", + "name": "Plaato Airlock", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/plaato", + "dependencies": ["webhook"], + "codeowners": ["@JohNan"], + "requirements": [] +} \ No newline at end of file diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py new file mode 100644 index 00000000000..6352c837121 --- /dev/null +++ b/homeassistant/components/plaato/sensor.py @@ -0,0 +1,139 @@ +"""Support for Plaato Airlock sensors.""" + +import logging + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +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) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Plaato sensor.""" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plaato from a config entry.""" + devices = {} + + 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): + """Update/Create the sensors.""" + if device_id not in devices and get_device(device_id): + entities = [] + sensors = get_device_sensors(device_id) + + for sensor_type in sensors: + entities.append(PlaatoSensor(device_id, sensor_type)) + + devices[device_id] = entities + + async_add_entities(entities, True) + else: + for entity in devices[device_id]: + entity.async_schedule_update_ha_state() + + hass.data[SENSOR_DATA_KEY] = async_dispatcher_connect( + hass, SENSOR_UPDATE, _update_sensor + ) + + 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 = "{} {}".format(device_id, sensor_type) + self._attributes = None + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format(PLAATO_DOMAIN, self._name) + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return "{}_{}".format(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, '') + + @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 + + @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 '%' + + return '' + + @property + def should_poll(self): + """Return the polling state.""" + return False diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json new file mode 100644 index 00000000000..ee99da0c8b1 --- /dev/null +++ b/homeassistant/components/plaato/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Plaato Airlock", + "step": { + "user": { + "title": "Set up the Plaato Webhook", + "description": "Are you sure you want to set up the Plaato Airlock?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Plaato Airlock." + }, + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 296c620cd7d..4c7d77e0dab 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ FLOWS = [ "nest", "openuv", "owntracks", + "plaato", "point", "ps4", "rainmachine",