-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add wiffi integration #30784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add wiffi integration #30784
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
758998f
Add integration for wiffi devices
mampfes 3605bf9
Fix pylint warning
mampfes 5b606b8
Use WIFFI / STALL WIFFI instead of wiffi to be consistent with stall.biz
mampfes 31e63e6
Don't update disabled entities.
mampfes b5b88bf
fix complains
mampfes 46bd841
incorporate various suggestions from code review
mampfes f415226
fix remaining comments from Martin
mampfes 376d955
fix comments
mampfes 1933fb0
add tests for config flow
mampfes efb0b3e
fix comments
mampfes 036fc54
add missing requirements for tests
mampfes 6893234
fix pylint warnings
mampfes 9627746
fix comments
mampfes 47c9db0
fix comments
mampfes 52d52b2
rebase and adapt to latest dev branch
mampfes 90f8c34
Update homeassistant/components/wiffi/config_flow.py
mampfes cdc6809
Update homeassistant/components/wiffi/config_flow.py
mampfes 4fec772
fix missing import
mampfes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 {} | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ] | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.