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 5 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 @@ -1217,6 +1217,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
63 changes: 63 additions & 0 deletions homeassistant/components/senziio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Senziio integration."""

from __future__ import annotations

import logging

from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL, CONF_UNIQUE_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo

from .const import DOMAIN, MANUFACTURER
from .device import SenziioDevice

_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[CONF_UNIQUE_ID]
device_model = entry.data[CONF_MODEL]
device = SenziioDevice(device_id, device_model, hass)

registry_info = DeviceInfo(
fzuccolo marked this conversation as resolved.
Show resolved Hide resolved
identifiers={(DOMAIN, device_id)},
name=entry.title,
manufacturer=MANUFACTURER,
model=device_model,
sw_version=entry.data["fw-version"],
serial_number=entry.data["serial-number"],
connections={(dr.CONNECTION_NETWORK_MAC, entry.data["mac-address"])},
)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
**registry_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
188 changes: 188 additions & 0 deletions homeassistant/components/senziio/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""Config flows for Senziio integration."""

from __future__ import annotations

import logging
from typing import Any

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 .const import DOMAIN, MANUFACTURER
from .device import SenziioDevice
from .exceptions import CannotConnect, MQTTNotEnabled, RepeatedTitle

_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 MQTTNotEnabled:
errors["base"] = "mqtt_not_enabled"
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 MQTTNotEnabled:
errors["base"] = "mqtt_not_enabled"
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 = SenziioDevice(device_id, device_model, 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())
4 changes: 4 additions & 0 deletions homeassistant/components/senziio/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Constants for the Senziio integration."""

DOMAIN = "senziio"
MANUFACTURER = "Senziio"
fzuccolo marked this conversation as resolved.
Show resolved Hide resolved
46 changes: 46 additions & 0 deletions homeassistant/components/senziio/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Support for Senziio devices."""

from collections.abc import Callable
import logging

from senziio import Senziio, SenziioMQTT

from homeassistant.components.mqtt import async_publish, async_subscribe
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from .exceptions import MQTTNotEnabled

_LOGGER = logging.getLogger(__name__)


class SenziioDevice(Senziio):
fzuccolo marked this conversation as resolved.
Show resolved Hide resolved
"""Senziio device interaction."""

def __init__(self, device_id: str, device_model: str, hass: HomeAssistant) -> None:
"""Initialize Senziio instance."""
super().__init__(device_id, device_model, mqtt=SenziioHAMQTT(hass))


class SenziioHAMQTT(SenziioMQTT):
fzuccolo marked this conversation as resolved.
Show resolved Hide resolved
"""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 MQTTNotEnabled 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 MQTTNotEnabled from error
15 changes: 15 additions & 0 deletions homeassistant/components/senziio/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Errors for Senziio integration."""

from homeassistant.exceptions import HomeAssistantError


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


class RepeatedTitle(HomeAssistantError):
fzuccolo marked this conversation as resolved.
Show resolved Hide resolved
"""Error to indicate that chosen device name is not unique."""


class MQTTNotEnabled(HomeAssistantError):
"""Error to indicate that required MQTT integration is not enabled."""
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*"
}
]
}