-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
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
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||||||||||||
}, | ||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
|
||||||||||||
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] | ||||||||||||
) | ||||||||||||
) | ||||||||||||
imduffy15 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
async def async_update_callback(spc_object): | ||||||||||||
return True | ||||||||||||
|
||||||||||||
|
||||||||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
"""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)) | ||||||||||||
|
||||||||||||
session = aiohttp_client.async_get_clientsession(hass) | ||||||||||||
entry = cast(SPCConfigEntry, entry) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
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() | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
entry.runtime_data.stop() | ||||||||||||
|
||||||||||||
return True | ||||||||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 | ||||||
|
@@ -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 | ||||||
|
||||||
|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
) -> 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()]) | ||||||
|
||||||
|
||||||
|
@@ -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 | ||||||
imduffy15 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
async def async_added_to_hass(self) -> None: | ||||||
"""Call for adding new entities.""" | ||||||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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']}", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is |
||
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") |
There was a problem hiding this comment.
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