From 1b5af3875592bfde09a1baf488bd06d66c42f0d9 Mon Sep 17 00:00:00 2001 From: Klaus-Uwe Mitterer Date: Fri, 21 May 2021 13:38:16 +0200 Subject: [PATCH] Working Kumi Sensors integration --- .gitignore | 2 + __init__.py | 34 ++++++++ config_flow.py | 74 ++++++++++++++++ const.py | 3 + kumisensors.py | 49 +++++++++++ manifest.json | 15 ++++ sensor.py | 198 +++++++++++++++++++++++++++++++++++++++++++ strings.json | 22 +++++ translations/en.json | 19 +++++ 9 files changed, 416 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 config_flow.py create mode 100644 const.py create mode 100644 kumisensors.py create mode 100644 manifest.json create mode 100644 sensor.py create mode 100644 strings.json create mode 100644 translations/en.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0b1eed3 --- /dev/null +++ b/__init__.py @@ -0,0 +1,34 @@ +"""The Kumi Sensors integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .kumisensors import KumiSensors + +PLATFORMS = ["sensor"] + +async def async_setup(hass: HomeAssistant, base_config: dict): + """Set up the component.""" + # initially empty the settings for this component + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Kumi Sensors from a config entry.""" + hass.data[DOMAIN][entry.entry_id] = entry.data + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/config_flow.py b/config_flow.py new file mode 100644 index 0000000..77255db --- /dev/null +++ b/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for Kumi Sensors integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.const import CONF_NAME, CONF_HOST + +from .const import DOMAIN +from .kumisensors import KumiSensors + +from urllib.request import urlopen + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema({"host": str}) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + + hub = KumiSensors(data[CONF_HOST], hass) + + if not await hub.connect(): + raise CannotConnect + + # Return info that you want to store in the config entry. + return {CONF_NAME: await hub.get_identifier()} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Kumi Sensors.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + user_input[CONF_NAME] = info[CONF_NAME] + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info[CONF_NAME], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + diff --git a/const.py b/const.py new file mode 100644 index 0000000..93a2b03 --- /dev/null +++ b/const.py @@ -0,0 +1,3 @@ +"""Constants for the Kumi Sensors integration.""" + +DOMAIN = "kumisensors" diff --git a/kumisensors.py b/kumisensors.py new file mode 100644 index 0000000..163c321 --- /dev/null +++ b/kumisensors.py @@ -0,0 +1,49 @@ +import json +import logging + +from homeassistant.core import HomeAssistant + +from urllib.request import urlopen + +_LOGGER = logging.getLogger(__name__) + +class KumiSensors: + def __init__(self, host: str, hass: HomeAssistant) -> None: + """Initialize.""" + if not "://" in host: + host = "http://" + host + self.host = host + self.data = None + self.hass = hass + + def fetch_data(self): + return json.load(urlopen(self.host)) + + async def get_data(self) -> dict: + self.data = self.data or await self.hass.async_add_executor_job(self.fetch_data) + return self.data + + async def connect(self) -> bool: + """Test if we can connect to the host.""" + try: + await self.get_data() + return True + except Exception as e: # pylint: disable=broad-except + _LOGGER.error(repr(e)) + return False + + async def get_identifier(self): + data = await self.get_data() + return data["identifier"] + + async def get_temperature(self): + data = await self.get_data() + return data["temperature"] + + async def get_pressure(self): + data = await self.get_data() + return data["pressure"] + + async def get_humidity(self): + data = await self.get_data() + return data["humidity"] diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..204c586 --- /dev/null +++ b/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "kumisensors", + "name": "Kumi Sensors", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kumisensors", + "requirements": [], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@kumitterer" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/sensor.py b/sensor.py new file mode 100644 index 0000000..ae26e26 --- /dev/null +++ b/sensor.py @@ -0,0 +1,198 @@ +from datetime import timedelta +import logging +import re +from typing import Any, Callable, Dict, Optional + +from homeassistant import config_entries, core +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + PERCENTAGE, + DEVICE_CLASS_PRESSURE, + PRESSURE_HPA +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) + +from .const import DOMAIN +from .kumisensors import KumiSensors + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=5) + +async def async_setup_entry( + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities, +): + """Setup sensors from a config entry created in the integrations UI.""" + config = hass.data[DOMAIN][config_entry.entry_id] + + if config_entry.options: + config.update(config_entry.options) + + sensors = [KumiTemperatureSensor(config, hass), KumiHumiditySensor(config, hass), KumiPressureSensor(config, hass)] + async_add_entities(sensors, update_before_add=True) + + +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable, + discovery_info: Optional[DiscoveryInfoType] = None, +) -> None: + """Set up the sensor platform.""" + sensors = [KumiTemperatureSensor(config, hass), KumiHumiditySensor(config, hass), KumiPressureSensor(config, hass)] + async_add_entities(sensors, update_before_add=True) + + +class KumiTemperatureSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, config, hass): + """Initialize the sensor.""" + self._sensors = KumiSensors(config[CONF_HOST], hass) + self._name = config.get(CONF_NAME, "Sensors") + self._state = None + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._name) + }, + "name": self._name + } + + @property + def name(self): + """Return the name of the sensor.""" + return '%s Temperature' % self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def device_class(self): + return DEVICE_CLASS_TEMPERATURE + + @property + def unique_id(self): + return "sensor.kumisensors.%s.temperature" % self._name.lower() + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + self._state = await self._sensors.get_temperature() + +class KumiHumiditySensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, config, hass): + """Initialize the sensor.""" + self._sensors = KumiSensors(config[CONF_HOST], hass) + self._name = config.get(CONF_NAME, "Sensors") + self._state = None + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._name) + }, + "name": self._name + } + + @property + def name(self): + """Return the name of the sensor.""" + return '%s Humidity' % self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PERCENTAGE + + @property + def device_class(self): + return DEVICE_CLASS_HUMIDITY + + @property + def unique_id(self): + return "sensor.kumisensors.%s.humidity" % self._name.lower() + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + self._state = await self._sensors.get_humidity() + +class KumiPressureSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, config, hass): + """Initialize the sensor.""" + self._sensors = KumiSensors(config[CONF_HOST], hass) + self._name = config.get(CONF_NAME, "Sensors") + self._state = None + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._name) + }, + "name": self._name + } + + @property + def name(self): + """Return the name of the sensor.""" + return '%s Pressure' % self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PRESSURE_HPA + + @property + def device_class(self): + return DEVICE_CLASS_PRESSURE + + @property + def unique_id(self): + return "sensor.kumisensors.%s.pressure" % self._name.lower() + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + self._state = await self._sensors.get_pressure() diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..b7386d9 --- /dev/null +++ b/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Kumi Sensors", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..746bb6d --- /dev/null +++ b/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "Kumi Sensors" +}