Skip to content
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

Add Senziio core integration #114650

Open
wants to merge 35 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
585619d
Implement Senziio core integration
fzuccolo Apr 2, 2024
26a792d
Remove hardware version from device info
fzuccolo Apr 2, 2024
23d361f
Remove binary sensor platform.
fzuccolo Apr 2, 2024
31d34fe
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 2, 2024
2c5329a
Remove binary_sensor patform icons
fzuccolo Apr 2, 2024
25d2c7a
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 10, 2024
2ff071b
Implement base entity for setting device info
fzuccolo Apr 10, 2024
7d5ae05
Merge branch 'senziio-integration' of github.com:senziio-admin/home-a…
fzuccolo Apr 10, 2024
adc9d38
Define constants in relevant module
fzuccolo Apr 10, 2024
c2cab39
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 10, 2024
c3340ce
Merge branch 'senziio-integration' of github.com:senziio-admin/home-a…
fzuccolo Apr 10, 2024
f8feea1
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 10, 2024
f776a6d
Merge branch 'senziio-integration' of github.com:senziio-admin/home-a…
fzuccolo Apr 10, 2024
d9b40af
Remove unnecessary SenziioDevice wrapper
fzuccolo Apr 10, 2024
1e4849b
Improve exception handling
fzuccolo Apr 10, 2024
6decf60
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 10, 2024
c65f287
Temporarily remove humidity and pressure entties
fzuccolo Apr 10, 2024
ffac762
Merge branch 'senziio-integration' of github.com:senziio-admin/home-a…
fzuccolo Apr 10, 2024
1407ff2
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 14, 2024
9afdf7b
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 19, 2024
628942a
Update relevant data on realod
fzuccolo Apr 23, 2024
c111e4b
Merge branch 'home-assistant:dev' into senziio-integration
fzuccolo Apr 23, 2024
37a88e3
Merge branches 'senziio-integration' and 'senziio-integration' of git…
fzuccolo Apr 23, 2024
550eb77
Merge branch 'dev' into senziio-integration
fzuccolo Apr 23, 2024
1a93507
Merge branch 'dev' into senziio-integration
fzuccolo Apr 23, 2024
bf8fc87
Merge branch 'dev' into senziio-integration
fzuccolo Apr 28, 2024
134735c
Merge branch 'dev' into senziio-integration
fzuccolo May 3, 2024
d03f6ab
Merge branch 'dev' into senziio-integration
fzuccolo May 6, 2024
dc77c6c
Merge branch 'dev' into senziio-integration
senziio-admin May 7, 2024
ce5a0f2
Merge branch 'dev' into senziio-integration
fzuccolo May 9, 2024
21dc140
Merge branch 'dev' into senziio-integration
fzuccolo May 11, 2024
45cb500
Merge branch 'dev' into senziio-integration
fzuccolo May 15, 2024
f7e4a2a
Merge branch 'dev' into senziio-integration
senziio-admin May 15, 2024
7bf315a
Merge branch 'dev' into senziio-integration
fzuccolo May 21, 2024
8876641
Merge branch 'dev' into senziio-integration
fzuccolo May 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,8 @@ build.json @home-assistant/supervisor
/tests/components/sentry/ @dcramer @frenck
/homeassistant/components/senz/ @milanmeu
/tests/components/senz/ @milanmeu
/homeassistant/components/senziio/ @pmiguez @fzuccolo
/tests/components/senziio/ @pmiguez @fzuccolo
/homeassistant/components/serial/ @fabaff
/homeassistant/components/seven_segments/ @fabaff
/homeassistant/components/seventeentrack/ @shaiu
Expand Down
81 changes: 81 additions & 0 deletions homeassistant/components/senziio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Senziio integration."""

from __future__ import annotations

from collections.abc import Callable
import logging

from senziio import Senziio, SenziioMQTT

from homeassistant.components import mqtt
from homeassistant.components.mqtt import async_publish, async_subscribe
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from .entity import DOMAIN

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Senziio device from a config entry."""

# Make sure MQTT integration is enabled and the client is available.
if not await mqtt.async_wait_for_mqtt_client(hass):
_LOGGER.error("MQTT integration is not available")
return False

device_id = entry.data["serial-number"]
device_model = entry.data["model"]
device = Senziio(device_id, device_model, mqtt=SenziioHAMQTT(hass))

if info := await device.get_info():
hass.config_entries.async_update_entry(entry, data=info)

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = device

# forward setup to all platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


class SenziioHAMQTT(SenziioMQTT):
"""Senziio MQTT interface using available integration."""

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize MQTT interface for Senziio devices."""
self._hass = hass

async def publish(self, topic: str, payload: str) -> None:
"""Publish to topic with a payload."""
try:
return await async_publish(self._hass, topic, payload)
except HomeAssistantError as error:
_LOGGER.error("Could not publish to MQTT topic")
raise MQTTError from error

async def subscribe(self, topic: str, callback: Callable) -> Callable:
"""Subscribe to topic with a callback."""
try:
return await async_subscribe(self._hass, topic, callback)
except HomeAssistantError as error:
_LOGGER.error("Could not subscribe to MQTT topic")
raise MQTTError from error


class MQTTError(HomeAssistantError):
"""Error to indicate that required MQTT integration is not enabled."""
197 changes: 197 additions & 0 deletions homeassistant/components/senziio/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""Config flows for Senziio integration."""

from __future__ import annotations

import logging
from typing import Any

from senziio import Senziio
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.const import CONF_FRIENDLY_NAME, CONF_MODEL, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from . import MQTTError, SenziioHAMQTT
from .entity import DOMAIN, MANUFACTURER

_LOGGER = logging.getLogger(__name__)

_input_type = vol.All(str, vol.Strip)


class SenziioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flows for Senziio Sensor."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle Senziio user setup."""
errors: dict[str, str] = {}

device_id = ""
device_model = ""
friendly_name = self._get_friendly_name()

if user_input is not None:
device_id = user_input[CONF_UNIQUE_ID]
device_model = user_input[CONF_MODEL]
friendly_name = user_input[CONF_FRIENDLY_NAME]

await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured()

try:
data = await validate_input(self.hass, user_input)
except MQTTError:
errors["base"] = "mqtt_error"
except CannotConnect:
errors["base"] = "cannot_connect"
except RepeatedTitle:
errors["base"] = "repeated_title"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=friendly_name,
data=data,
)

step_user_data_schema = vol.Schema(
{
vol.Required(CONF_UNIQUE_ID, default=device_id): _input_type,
vol.Required(CONF_MODEL, default=device_model): _input_type,
vol.Required(CONF_FRIENDLY_NAME, default=friendly_name): _input_type,
}
)

return self.async_show_form(
step_id="user", data_schema=step_user_data_schema, errors=errors
)

async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> config_entries.ConfigFlowResult:
"""Handle Senziio device discovered via Zeroconf."""
_LOGGER.info("Discovered Senziio device via Zeroconf")

device_id = discovery_info.properties["device_id"]

await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured()

self.context[CONF_UNIQUE_ID] = device_id
self.context[CONF_MODEL] = discovery_info.properties["device_model"]

return await self.async_step_zeroconf_confirm()

async def async_step_zeroconf_confirm(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Confirm the addition of a Senziio device discovered via Zeroconf."""
errors: dict[str, str] = {}

if user_input is not None:
device_id = self.context[CONF_UNIQUE_ID]
friendly_name = user_input[CONF_FRIENDLY_NAME]
data_input = {
CONF_UNIQUE_ID: device_id,
CONF_MODEL: self.context[CONF_MODEL],
CONF_FRIENDLY_NAME: friendly_name,
}

try:
data = await validate_input(self.hass, data_input)
except MQTTError:
errors["base"] = "mqtt_error"
except CannotConnect:
errors["base"] = "cannot_connect"
except RepeatedTitle:
errors["base"] = "repeated_title"
except Exception: # pylint: disable=broad-except
_LOGGER.error("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=friendly_name,
data=data,
)

data_schema = vol.Schema(
{
vol.Required(
CONF_FRIENDLY_NAME,
default=self._get_friendly_name(),
): vol.All(str, vol.Strip),
}
)

return self.async_show_form(
step_id="zeroconf_confirm",
description_placeholders={
"device_id": self.context[CONF_UNIQUE_ID],
"device_model": self.context[CONF_MODEL],
},
data_schema=data_schema,
errors=errors,
)

def _get_friendly_name(self):
"""Get a unique friendly name to display as device title."""
used_titles = {
entry.title for entry in self._async_current_entries(include_ignore=True)
}
prefix = MANUFACTURER
if model := self.context.get(CONF_MODEL):
prefix = f"{MANUFACTURER} {model}"
number = len(used_titles) + 1
while (title := f"{prefix} {number}") in used_titles:
number += 1
return title


async def validate_input(
hass: HomeAssistant, data_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate input data."""
# check friendly name is unique
friendly_name = _sanitize(data_input[CONF_FRIENDLY_NAME])
existing_titles = {
entry.title for entry in hass.config_entries.async_entries(DOMAIN)
}
if friendly_name in existing_titles:
raise RepeatedTitle

# validate device response
device_id = _sanitize(data_input[CONF_UNIQUE_ID])
device_model = _sanitize(data_input[CONF_MODEL])
device = Senziio(device_id, device_model, mqtt=SenziioHAMQTT(hass))
device_info = await device.get_info()

if not device_info:
raise CannotConnect

return {
CONF_UNIQUE_ID: device_id,
CONF_MODEL: device_model,
CONF_FRIENDLY_NAME: friendly_name,
**device_info,
}


def _sanitize(value: str) -> str:
"""Sanitize entry value."""
return " ".join(value.split())


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect to the device."""


class RepeatedTitle(HomeAssistantError):
"""Error to indicate that chosen device name is not unique."""
30 changes: 30 additions & 0 deletions homeassistant/components/senziio/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Senziio base entity."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity

DOMAIN = "senziio"
MANUFACTURER = "Senziio"


class SenziioEntity(Entity):
"""Representation of a Senziio entity."""

def __init__(self, entry: ConfigEntry) -> None:
"""Initialize base entity."""
self.entry = entry

@property
def device_info(self) -> DeviceInfo:
"""Return device info."""
return DeviceInfo(
identifiers={(DOMAIN, self.entry.data["serial-number"])},
name=self.entry.title,
manufacturer=MANUFACTURER,
model=self.entry.data["model"],
sw_version=self.entry.data["fw-version"],
serial_number=self.entry.data["serial-number"],
connections={(dr.CONNECTION_NETWORK_MAC, self.entry.data["mac-address"])},
)
24 changes: 24 additions & 0 deletions homeassistant/components/senziio/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"entity": {
"sensor": {
"person-counter": {
"default": "mdi:human"
},
"atmospheric-pressure": {
"default": "mdi:gauge"
},
"co2": {
"default": "mdi:molecule-co2"
},
"illuminance": {
"default": "mdi:brightness-5"
},
"humidity": {
"default": "mdi:water-percent"
},
"temperature": {
"default": "mdi:thermometer"
}
}
}
}
17 changes: 17 additions & 0 deletions homeassistant/components/senziio/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"domain": "senziio",
"name": "Senziio",
"codeowners": ["@pmiguez", "@fzuccolo"],
"config_flow": true,
"dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/senziio",
"homekit": {},
"iot_class": "local_push",
"requirements": ["senziio==0.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",
"name": "senziio*"
}
]
}