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 configflow for the SPC integration #135894

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
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
116 changes: 75 additions & 41 deletions homeassistant/components/spc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,115 @@
"""Support for Vanderbilt (formerly Siemens) SPC alarm systems."""

from __future__ import annotations

import logging
from typing import cast

from aiohttp import ClientError
from pyspcwebgw import SpcWebGateway
from pyspcwebgw.area import Area
from pyspcwebgw.zone import Zone
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType

_LOGGER = logging.getLogger(__name__)

CONF_WS_URL = "ws_url"
CONF_API_URL = "api_url"

DOMAIN = "spc"
DATA_API = "spc_api"
from .const import (
CONF_API_URL,
CONF_WS_URL,
DATA_SCHEMA,
DOMAIN,
SIGNAL_UPDATE_ALARM,
SIGNAL_UPDATE_SENSOR,
)

SIGNAL_UPDATE_ALARM = "spc_update_alarm_{}"
SIGNAL_UPDATE_SENSOR = "spc_update_sensor_{}"
_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_WS_URL): cv.string,
vol.Required(CONF_API_URL): cv.string,
}
)
DOMAIN: DATA_SCHEMA,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly recommend to keep the data schema here as that way we don't touch it and it will be removed in a while anyway

},
extra=vol.ALLOW_EXTRA,
)

PLATFORMS = (Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR)


class SPCConfigEntry(ConfigEntry):
"""Handle SPC config entry."""

runtime_data: SpcWebGateway
Comment on lines +43 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class SPCConfigEntry(ConfigEntry):
"""Handle SPC config entry."""
runtime_data: SpcWebGateway
type SPCConfigEntry = ConfigEntry[SpcWebGateway]



async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the SPC component."""
if DOMAIN in config:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)

async def async_update_callback(spc_object):
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SPCConfigEntry) -> bool:

"""Set up SPC from a config entry."""

async def async_update_callback(spc_object: Area | Zone) -> None:
"""Process updates from SPC system."""
if isinstance(spc_object, Area):
async_dispatcher_send(hass, SIGNAL_UPDATE_ALARM.format(spc_object.id))
elif isinstance(spc_object, Zone):
async_dispatcher_send(hass, SIGNAL_UPDATE_SENSOR.format(spc_object.id))
else:
_LOGGER.warning("Received invalid update object type: %s", type(spc_object))

Check warning on line 71 in homeassistant/components/spc/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/spc/__init__.py#L71

Added line #L71 was not covered by tests

session = aiohttp_client.async_get_clientsession(hass)
entry = cast(SPCConfigEntry, entry)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
entry = cast(SPCConfigEntry, entry)


try:
spc = SpcWebGateway(
loop=hass.loop,
session=session,
api_url=entry.data[CONF_API_URL],
ws_url=entry.data[CONF_WS_URL],
async_callback=async_update_callback,
)

spc = SpcWebGateway(
loop=hass.loop,
session=session,
api_url=config[DOMAIN].get(CONF_API_URL),
ws_url=config[DOMAIN].get(CONF_WS_URL),
async_callback=async_update_callback,
if not await spc.async_load_parameters():
raise ConfigEntryNotReady("Cannot connect to SPC controller")
except (ClientError, ConnectionError, TimeoutError) as err:
raise ConfigEntryNotReady("Cannot connect to SPC controller") from err

entry.runtime_data = spc

device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, spc.info["sn"])},
manufacturer="Vanderbilt",
name=spc.info["sn"],
model=spc.info["type"],
sw_version=spc.info["version"],
configuration_url=f"http://{spc.ethernet['ip_address']}/",
)

hass.data[DATA_API] = spc
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

if not await spc.async_load_parameters():
_LOGGER.error("Failed to load area/zone information from SPC")
return False
spc.start()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this?


# add sensor devices for each zone (typically motion/fire/door sensors)
hass.async_create_task(
discovery.async_load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config)
)
return True

# create a separate alarm panel for each area
hass.async_create_task(
discovery.async_load_platform(
hass, Platform.ALARM_CONTROL_PANEL, DOMAIN, {}, config
)
)

# start listening for incoming events over websocket
spc.start()
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
entry = cast(SPCConfigEntry, entry)
Comment on lines +110 to +112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
entry = cast(SPCConfigEntry, entry)
async def async_unload_entry(hass: HomeAssistant, entry: SPCConfigEntry) -> bool:
"""Unload a config entry."""

entry.runtime_data.stop()

return True
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
21 changes: 9 additions & 12 deletions homeassistant/components/spc/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from typing import cast

from pyspcwebgw import SpcWebGateway
from pyspcwebgw.area import Area
from pyspcwebgw.const import AreaMode
Expand All @@ -14,14 +16,12 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import DATA_API, SIGNAL_UPDATE_ALARM
from . import SIGNAL_UPDATE_ALARM, ConfigEntry, SPCConfigEntry


def _get_alarm_state(area: Area) -> AlarmControlPanelState | None:
"""Get the alarm state."""

if area.verified_alarm:
return AlarmControlPanelState.TRIGGERED

Expand All @@ -34,16 +34,12 @@ def _get_alarm_state(area: Area) -> AlarmControlPanelState | None:
return mode_to_state.get(area.mode)


async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant, entry: SPCConfigEntry, async_add_entities: AddEntitiesCallback

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase your branch as we now have a different type for the async_add_entities

) -> None:
"""Set up the SPC alarm control panel platform."""
if discovery_info is None:
return
api: SpcWebGateway = hass.data[DATA_API]
"""Set up the SPC alarm control panel from a config entry."""
entry = cast(SPCConfigEntry, entry)
api = entry.runtime_data
async_add_entities([SpcAlarm(area=area, api=api) for area in api.areas.values()])


Expand All @@ -63,6 +59,7 @@ def __init__(self, area: Area, api: SpcWebGateway) -> None:
self._area = area
self._api = api
self._attr_name = area.name
self._attr_unique_id = area.id

async def async_added_to_hass(self) -> None:
"""Call for adding new entities."""
Expand Down
29 changes: 12 additions & 17 deletions homeassistant/components/spc/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import annotations

from pyspcwebgw import SpcWebGateway
from typing import cast

from pyspcwebgw.const import ZoneInput, ZoneType
from pyspcwebgw.zone import Zone

Expand All @@ -13,9 +14,8 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import DATA_API, SIGNAL_UPDATE_SENSOR
from . import SIGNAL_UPDATE_SENSOR, ConfigEntry, SPCConfigEntry


def _get_device_class(zone_type: ZoneType) -> BinarySensorDeviceClass | None:
Expand All @@ -27,22 +27,16 @@ def _get_device_class(zone_type: ZoneType) -> BinarySensorDeviceClass | None:
}.get(zone_type)


async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the SPC binary sensor."""
if discovery_info is None:
return
api: SpcWebGateway = hass.data[DATA_API]
"""Set up the SPC binary sensors from a config entry."""
entry = cast(SPCConfigEntry, entry)
api = entry.runtime_data
async_add_entities(
[
SpcBinarySensor(zone)
for zone in api.zones.values()
if _get_device_class(zone.type)
]
SpcBinarySensor(zone)
for zone in api.zones.values()
if _get_device_class(zone.type)
)


Expand All @@ -56,6 +50,7 @@ def __init__(self, zone: Zone) -> None:
self._zone = zone
self._attr_name = zone.name
self._attr_device_class = _get_device_class(zone.type)
self._attr_unique_id = zone.id

async def async_added_to_hass(self) -> None:
"""Call for adding new entities."""
Expand Down
103 changes: 103 additions & 0 deletions homeassistant/components/spc/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Config flow for SPC integration."""

from __future__ import annotations

from typing import Any

from aiohttp import ClientError
from pyspcwebgw import SpcWebGateway

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client

from . import CONF_API_URL, CONF_WS_URL, DATA_SCHEMA, DOMAIN


async def validate_connection(
hass: HomeAssistant, api_url: str, ws_url: str
) -> tuple[str | None, dict[str, Any] | None]:
"""Test if we can connect to the SPC controller.

Returns a tuple of (error, device_info).
"""
session = aiohttp_client.async_get_clientsession(hass)

try:
spc = SpcWebGateway(
loop=hass.loop,
session=session,
api_url=api_url,
ws_url=ws_url,
async_callback=None, # No callback needed for validation
)
if not await spc.async_load_parameters():
return "cannot_connect", None
return None, spc.info # noqa: TRY300
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's fix this TRY300

except (ClientError, TimeoutError, Exception): # pylint: disable=broad-except
return "cannot_connect", None


class SpcConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SPC."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}

if user_input is not None:
self._async_abort_entries_match(
{
CONF_API_URL: user_input[CONF_API_URL],
CONF_WS_URL: user_input[CONF_WS_URL],
}
)

error, device_info = await validate_connection(
self.hass,
user_input[CONF_API_URL],
user_input[CONF_WS_URL],
)
if error is None and device_info is not None:
return self.async_create_entry(
title=f"{device_info['type']} - {device_info['sn']}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is sn a serial number? Should we set that as unique id for the config entry?

data=user_input,
)

if error:
errors["base"] = error

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

async def async_step_import(
self, import_config: dict[str, Any]
) -> ConfigFlowResult:
"""Import a config entry from configuration.yaml."""
self._async_abort_entries_match(
{
CONF_API_URL: import_config[CONF_API_URL],
CONF_WS_URL: import_config[CONF_WS_URL],
}
)

error, device_info = await validate_connection(
self.hass,
import_config[CONF_API_URL],
import_config[CONF_WS_URL],
)

if error is None and device_info is not None:
return self.async_create_entry(
title=f"{device_info['type']} - {device_info['sn']}",
data=import_config,
)

return self.async_abort(reason=error or "cannot_connect")
Loading