Add wiffi integration (#30784)
* Add integration for wiffi devices wiffi devices are DIY board manufactured by stall.biz. Several devices are available, e.g. a weather station (weatherman), an indoor environmental sensor (wiffi-wz) and some more. This intgration has been developed using a weatherman device, but should also work for other devices from stall.biz. * Fix pylint warning * Use WIFFI / STALL WIFFI instead of wiffi to be consistent with stall.biz * Don't update disabled entities. * fix complains - move wiffi specific code to pypi - remove yaml configuration code * incorporate various suggestions from code review * fix remaining comments from Martin * fix comments * add tests for config flow * fix comments * add missing requirements for tests * fix pylint warnings * fix comments * fix comments remove debug log rename .translations to translations * rebase and adapt to latest dev branch * Update homeassistant/components/wiffi/config_flow.py Co-Authored-By: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/wiffi/config_flow.py Co-Authored-By: Martin Hjelmare <marhje52@gmail.com> * fix missing import Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
6464c94990
commit
ee96ff2846
16 changed files with 647 additions and 0 deletions
|
@ -849,6 +849,7 @@ omit =
|
||||||
homeassistant/components/webostv/*
|
homeassistant/components/webostv/*
|
||||||
homeassistant/components/wemo/*
|
homeassistant/components/wemo/*
|
||||||
homeassistant/components/whois/sensor.py
|
homeassistant/components/whois/sensor.py
|
||||||
|
homeassistant/components/wiffi/*
|
||||||
homeassistant/components/wink/*
|
homeassistant/components/wink/*
|
||||||
homeassistant/components/wirelesstag/*
|
homeassistant/components/wirelesstag/*
|
||||||
homeassistant/components/worldtidesinfo/sensor.py
|
homeassistant/components/worldtidesinfo/sensor.py
|
||||||
|
|
|
@ -449,6 +449,7 @@ homeassistant/components/weather/* @fabaff
|
||||||
homeassistant/components/webostv/* @bendavid
|
homeassistant/components/webostv/* @bendavid
|
||||||
homeassistant/components/websocket_api/* @home-assistant/core
|
homeassistant/components/websocket_api/* @home-assistant/core
|
||||||
homeassistant/components/wemo/* @sqldiablo
|
homeassistant/components/wemo/* @sqldiablo
|
||||||
|
homeassistant/components/wiffi/* @mampfes
|
||||||
homeassistant/components/withings/* @vangorra
|
homeassistant/components/withings/* @vangorra
|
||||||
homeassistant/components/wled/* @frenck
|
homeassistant/components/wled/* @frenck
|
||||||
homeassistant/components/workday/* @fabaff
|
homeassistant/components/workday/* @fabaff
|
||||||
|
|
230
homeassistant/components/wiffi/__init__.py
Normal file
230
homeassistant/components/wiffi/__init__.py
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
"""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
|
||||||
|
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,
|
||||||
|
DOMAIN,
|
||||||
|
UPDATE_ENTITY_SIGNAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
PLATFORMS = ["sensor", "binary_sensor"]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the wiffi component. config contains data from configuration.yaml."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
# 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 component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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, component)
|
||||||
|
for component 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):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
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=3)
|
||||||
|
|
||||||
|
@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()
|
53
homeassistant/components/wiffi/binary_sensor.py
Normal file
53
homeassistant/components/wiffi/binary_sensor.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"""Binary sensor platform support for wiffi devices."""
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from . import WiffiEntity
|
||||||
|
from .const import CREATE_ENTITY_SIGNAL
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up platform for a new integration.
|
||||||
|
|
||||||
|
Called by the HA framework after async_forward_entry_setup has been called
|
||||||
|
during initialization of a new integration (= wiffi).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _create_entity(device, metric):
|
||||||
|
"""Create platform specific entities."""
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
if metric.is_bool:
|
||||||
|
entities.append(BoolEntity(device, metric))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity)
|
||||||
|
|
||||||
|
|
||||||
|
class BoolEntity(WiffiEntity, BinarySensorEntity):
|
||||||
|
"""Entity for wiffi metrics which have a boolean value."""
|
||||||
|
|
||||||
|
def __init__(self, device, metric):
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(device, metric)
|
||||||
|
self._value = metric.value
|
||||||
|
self.reset_expiration_date()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_value_callback(self, device, metric):
|
||||||
|
"""Update the value of the entity.
|
||||||
|
|
||||||
|
Called if a new message has been received from the wiffi device.
|
||||||
|
"""
|
||||||
|
self.reset_expiration_date()
|
||||||
|
self._value = metric.value
|
||||||
|
self.async_write_ha_state()
|
57
homeassistant/components/wiffi/config_flow.py
Normal file
57
homeassistant/components/wiffi/config_flow.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
"""Config flow for wiffi component.
|
||||||
|
|
||||||
|
Used by UI to setup a wiffi integration.
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from wiffi import WiffiTcpServer
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_PORT
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
|
class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Wiffi server setup config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the start of the config flow.
|
||||||
|
|
||||||
|
Called after wiffi integration has been selected in the 'add integration
|
||||||
|
UI'. The user_input is set to None in this case. We will open a config
|
||||||
|
flow form then.
|
||||||
|
This function is also called if the form has been submitted. user_input
|
||||||
|
contains a dict with the user entered values then.
|
||||||
|
"""
|
||||||
|
if user_input is None:
|
||||||
|
return self._async_show_form()
|
||||||
|
|
||||||
|
# received input from form or configuration.yaml
|
||||||
|
|
||||||
|
try:
|
||||||
|
# try to start server to check whether port is in use
|
||||||
|
server = WiffiTcpServer(user_input[CONF_PORT])
|
||||||
|
await server.start_server()
|
||||||
|
await server.close_server()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Port {user_input[CONF_PORT]}", data=user_input
|
||||||
|
)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno == errno.EADDRINUSE:
|
||||||
|
return self.async_abort(reason="addr_in_use")
|
||||||
|
return self.async_abort(reason="start_server_failed")
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_show_form(self, errors=None):
|
||||||
|
"""Show the config flow form to the user."""
|
||||||
|
data_schema = {vol.Required(CONF_PORT, default=DEFAULT_PORT): int}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {}
|
||||||
|
)
|
12
homeassistant/components/wiffi/const.py
Normal file
12
homeassistant/components/wiffi/const.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""Constants for the wiffi component."""
|
||||||
|
|
||||||
|
# Component domain, used to store component data in hass data.
|
||||||
|
DOMAIN = "wiffi"
|
||||||
|
|
||||||
|
# Default port for TCP server
|
||||||
|
DEFAULT_PORT = 8189
|
||||||
|
|
||||||
|
# Signal name to send create/update to platform (sensor/binary_sensor)
|
||||||
|
CREATE_ENTITY_SIGNAL = "wiffi_create_entity_signal"
|
||||||
|
UPDATE_ENTITY_SIGNAL = "wiffi_update_entity_signal"
|
||||||
|
CHECK_ENTITIES_SIGNAL = "wiffi_check_entities_signal"
|
11
homeassistant/components/wiffi/manifest.json
Normal file
11
homeassistant/components/wiffi/manifest.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"domain": "wiffi",
|
||||||
|
"name": "Wiffi",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/wiffi",
|
||||||
|
"requirements": ["wiffi==1.0.0"],
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": [
|
||||||
|
"@mampfes"
|
||||||
|
]
|
||||||
|
}
|
125
homeassistant/components/wiffi/sensor.py
Normal file
125
homeassistant/components/wiffi/sensor.py
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
"""Sensor platform support for wiffi devices."""
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_ILLUMINANCE,
|
||||||
|
DEVICE_CLASS_PRESSURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
)
|
||||||
|
from homeassistant.const import DEGREE, PRESSURE_MBAR, TEMP_CELSIUS
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from . import WiffiEntity
|
||||||
|
from .const import CREATE_ENTITY_SIGNAL
|
||||||
|
from .wiffi_strings import (
|
||||||
|
WIFFI_UOM_DEGREE,
|
||||||
|
WIFFI_UOM_LUX,
|
||||||
|
WIFFI_UOM_MILLI_BAR,
|
||||||
|
WIFFI_UOM_PERCENT,
|
||||||
|
WIFFI_UOM_TEMP_CELSIUS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# map to determine HA device class from wiffi's unit of measurement
|
||||||
|
UOM_TO_DEVICE_CLASS_MAP = {
|
||||||
|
WIFFI_UOM_TEMP_CELSIUS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
WIFFI_UOM_PERCENT: DEVICE_CLASS_HUMIDITY,
|
||||||
|
WIFFI_UOM_MILLI_BAR: DEVICE_CLASS_PRESSURE,
|
||||||
|
WIFFI_UOM_LUX: DEVICE_CLASS_ILLUMINANCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
# map to convert wiffi unit of measurements to common HA uom's
|
||||||
|
UOM_MAP = {
|
||||||
|
WIFFI_UOM_DEGREE: DEGREE,
|
||||||
|
WIFFI_UOM_TEMP_CELSIUS: TEMP_CELSIUS,
|
||||||
|
WIFFI_UOM_MILLI_BAR: PRESSURE_MBAR,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up platform for a new integration.
|
||||||
|
|
||||||
|
Called by the HA framework after async_forward_entry_setup has been called
|
||||||
|
during initialization of a new integration (= wiffi).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _create_entity(device, metric):
|
||||||
|
"""Create platform specific entities."""
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
if metric.is_number:
|
||||||
|
entities.append(NumberEntity(device, metric))
|
||||||
|
elif metric.is_string:
|
||||||
|
entities.append(StringEntity(device, metric))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity)
|
||||||
|
|
||||||
|
|
||||||
|
class NumberEntity(WiffiEntity):
|
||||||
|
"""Entity for wiffi metrics which have a number value."""
|
||||||
|
|
||||||
|
def __init__(self, device, metric):
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(device, metric)
|
||||||
|
self._device_class = UOM_TO_DEVICE_CLASS_MAP.get(metric.unit_of_measurement)
|
||||||
|
self._unit_of_measurement = UOM_MAP.get(
|
||||||
|
metric.unit_of_measurement, metric.unit_of_measurement
|
||||||
|
)
|
||||||
|
self._value = metric.value
|
||||||
|
self.reset_expiration_date()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the automatically determined device class."""
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement of this entity."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the value of the entity."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_value_callback(self, device, metric):
|
||||||
|
"""Update the value of the entity.
|
||||||
|
|
||||||
|
Called if a new message has been received from the wiffi device.
|
||||||
|
"""
|
||||||
|
self.reset_expiration_date()
|
||||||
|
self._unit_of_measurement = UOM_MAP.get(
|
||||||
|
metric.unit_of_measurement, metric.unit_of_measurement
|
||||||
|
)
|
||||||
|
self._value = metric.value
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class StringEntity(WiffiEntity):
|
||||||
|
"""Entity for wiffi metrics which have a string value."""
|
||||||
|
|
||||||
|
def __init__(self, device, metric):
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(device, metric)
|
||||||
|
self._value = metric.value
|
||||||
|
self.reset_expiration_date()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the value of the entity."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_value_callback(self, device, metric):
|
||||||
|
"""Update the value of the entity.
|
||||||
|
|
||||||
|
Called if a new message has been received from the wiffi device.
|
||||||
|
"""
|
||||||
|
self.reset_expiration_date()
|
||||||
|
self._value = metric.value
|
||||||
|
self.async_write_ha_state()
|
16
homeassistant/components/wiffi/strings.json
Normal file
16
homeassistant/components/wiffi/strings.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Setup TCP server for WIFFI devices",
|
||||||
|
"data": {
|
||||||
|
"port": "Server Port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"addr_in_use": "Server port already in use.",
|
||||||
|
"start_server_failed": "Start server failed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
homeassistant/components/wiffi/translations/en.json
Normal file
16
homeassistant/components/wiffi/translations/en.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"addr_in_use": "Server port already in use.",
|
||||||
|
"start_server_failed": "Start server failed."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"port": "Server Port"
|
||||||
|
},
|
||||||
|
"title": "Setup TCP server for WIFFI devices"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
homeassistant/components/wiffi/wiffi_strings.py
Normal file
8
homeassistant/components/wiffi/wiffi_strings.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
"""Definition of string used in wiffi json telegrams."""
|
||||||
|
|
||||||
|
# units of measurement
|
||||||
|
WIFFI_UOM_TEMP_CELSIUS = "gradC"
|
||||||
|
WIFFI_UOM_DEGREE = "grad"
|
||||||
|
WIFFI_UOM_PERCENT = "%"
|
||||||
|
WIFFI_UOM_MILLI_BAR = "mb"
|
||||||
|
WIFFI_UOM_LUX = "lux"
|
|
@ -154,6 +154,7 @@ FLOWS = [
|
||||||
"vilfo",
|
"vilfo",
|
||||||
"vizio",
|
"vizio",
|
||||||
"wemo",
|
"wemo",
|
||||||
|
"wiffi",
|
||||||
"withings",
|
"withings",
|
||||||
"wled",
|
"wled",
|
||||||
"wwlln",
|
"wwlln",
|
||||||
|
|
|
@ -2176,6 +2176,9 @@ webexteamssdk==1.1.1
|
||||||
# homeassistant.components.gpmdp
|
# homeassistant.components.gpmdp
|
||||||
websocket-client==0.54.0
|
websocket-client==0.54.0
|
||||||
|
|
||||||
|
# homeassistant.components.wiffi
|
||||||
|
wiffi==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.wirelesstag
|
# homeassistant.components.wirelesstag
|
||||||
wirelesstagpy==0.4.0
|
wirelesstagpy==0.4.0
|
||||||
|
|
||||||
|
|
|
@ -873,6 +873,9 @@ wakeonlan==1.1.6
|
||||||
# homeassistant.components.folder_watcher
|
# homeassistant.components.folder_watcher
|
||||||
watchdog==0.8.3
|
watchdog==0.8.3
|
||||||
|
|
||||||
|
# homeassistant.components.wiffi
|
||||||
|
wiffi==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.withings
|
# homeassistant.components.withings
|
||||||
withings-api==2.1.3
|
withings-api==2.1.3
|
||||||
|
|
||||||
|
|
1
tests/components/wiffi/__init__.py
Normal file
1
tests/components/wiffi/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the wiffi integration."""
|
109
tests/components/wiffi/test_config_flow.py
Normal file
109
tests/components/wiffi/test_config_flow.py
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
"""Test the wiffi integration config flow."""
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from asynctest import patch
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.wiffi.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_PORT
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="dummy_tcp_server")
|
||||||
|
def mock_dummy_tcp_server():
|
||||||
|
"""Mock a valid WiffiTcpServer."""
|
||||||
|
|
||||||
|
class Dummy:
|
||||||
|
async def start_server(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def close_server(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
server = Dummy()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server
|
||||||
|
):
|
||||||
|
yield server
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="addr_in_use")
|
||||||
|
def mock_addr_in_use_server():
|
||||||
|
"""Mock a WiffiTcpServer with addr_in_use."""
|
||||||
|
|
||||||
|
class Dummy:
|
||||||
|
async def start_server(self):
|
||||||
|
raise OSError(errno.EADDRINUSE, "")
|
||||||
|
|
||||||
|
async def close_server(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
server = Dummy()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server
|
||||||
|
):
|
||||||
|
yield server
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="start_server_failed")
|
||||||
|
def mock_start_server_failed():
|
||||||
|
"""Mock a WiffiTcpServer with start_server_failed."""
|
||||||
|
|
||||||
|
class Dummy:
|
||||||
|
async def start_server(self):
|
||||||
|
raise OSError(errno.EACCES, "")
|
||||||
|
|
||||||
|
async def close_server(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
server = Dummy()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server
|
||||||
|
):
|
||||||
|
yield server
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass, dummy_tcp_server):
|
||||||
|
"""Test how we get the form."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
assert result["step_id"] == config_entries.SOURCE_USER
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_PORT: 8765},
|
||||||
|
)
|
||||||
|
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_addr_in_use(hass, addr_in_use):
|
||||||
|
"""Test how we handle addr_in_use error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_PORT: 8765},
|
||||||
|
)
|
||||||
|
assert result2["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result2["reason"] == "addr_in_use"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_start_server_failed(hass, start_server_failed):
|
||||||
|
"""Test how we handle start_server_failed error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_PORT: 8765},
|
||||||
|
)
|
||||||
|
assert result2["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result2["reason"] == "start_server_failed"
|
Loading…
Add table
Add a link
Reference in a new issue