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>pull/35574/head
parent
6464c94990
commit
ee96ff2846
|
@ -849,6 +849,7 @@ omit =
|
|||
homeassistant/components/webostv/*
|
||||
homeassistant/components/wemo/*
|
||||
homeassistant/components/whois/sensor.py
|
||||
homeassistant/components/wiffi/*
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/wirelesstag/*
|
||||
homeassistant/components/worldtidesinfo/sensor.py
|
||||
|
|
|
@ -449,6 +449,7 @@ homeassistant/components/weather/* @fabaff
|
|||
homeassistant/components/webostv/* @bendavid
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/withings/* @vangorra
|
||||
homeassistant/components/wled/* @frenck
|
||||
homeassistant/components/workday/* @fabaff
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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 {}
|
||||
)
|
|
@ -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"
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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()
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
"vizio",
|
||||
"wemo",
|
||||
"wiffi",
|
||||
"withings",
|
||||
"wled",
|
||||
"wwlln",
|
||||
|
|
|
@ -2176,6 +2176,9 @@ webexteamssdk==1.1.1
|
|||
# homeassistant.components.gpmdp
|
||||
websocket-client==0.54.0
|
||||
|
||||
# homeassistant.components.wiffi
|
||||
wiffi==1.0.0
|
||||
|
||||
# homeassistant.components.wirelesstag
|
||||
wirelesstagpy==0.4.0
|
||||
|
||||
|
|
|
@ -873,6 +873,9 @@ wakeonlan==1.1.6
|
|||
# homeassistant.components.folder_watcher
|
||||
watchdog==0.8.3
|
||||
|
||||
# homeassistant.components.wiffi
|
||||
wiffi==1.0.0
|
||||
|
||||
# homeassistant.components.withings
|
||||
withings-api==2.1.3
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the wiffi integration."""
|
|
@ -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…
Reference in New Issue