Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,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
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
230 changes: 230 additions & 0 deletions homeassistant/components/wiffi/__init__.py
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(
Comment thread
mampfes marked this conversation as resolved.
Outdated
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 changes: 53 additions & 0 deletions homeassistant/components/wiffi/binary_sensor.py
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()
57 changes: 57 additions & 0 deletions homeassistant/components/wiffi/config_flow.py
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 {}
)
12 changes: 12 additions & 0 deletions homeassistant/components/wiffi/const.py
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"
11 changes: 11 additions & 0 deletions homeassistant/components/wiffi/manifest.json
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"
]
}
Loading