Skip to content

Commit

Permalink
Merge pull request #117 from ThomasLomas/config-entry
Browse files Browse the repository at this point in the history
- Fixes errors in logs when reloading, changing options, migrating cameras
- Fixes deprecated method warning
- Uses runtime data to auto clean up
  • Loading branch information
ThomasLomas authored Jan 4, 2025
2 parents 88089d9 + d012709 commit 69fd5ca
Show file tree
Hide file tree
Showing 19 changed files with 111 additions and 137 deletions.
54 changes: 31 additions & 23 deletions custom_components/starling_home_hub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@

from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntry

from custom_components.starling_home_hub.api import StarlingHomeHubApiClient
from custom_components.starling_home_hub.const import CONF_ENABLE_RTSP_STREAM, CONF_ENABLE_WEBRTC_STREAM, CONF_RTSP_PASSWORD, CONF_RTSP_USERNAME, DOMAIN, LOGGER, PLATFORMS
from custom_components.starling_home_hub.coordinator import StarlingHomeHubDataUpdateCoordinator
from custom_components.starling_home_hub.coordinator import StarlingHomeHubDataUpdateCoordinator, StarlingHomeHubConfigEntry


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry) -> bool:
"""Set up this integration using UI."""

LOGGER.info("Setting up Starling Home Hub integration")

client = StarlingHomeHubApiClient(
url=entry.data[CONF_URL],
api_key=entry.data[CONF_API_KEY],
Expand All @@ -30,9 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = coordinator = StarlingHomeHubDataUpdateCoordinator(
hass=hass,
client=client,
config_entry=entry,
)

# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
await coordinator.async_config_entry_first_refresh()

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand All @@ -41,41 +42,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unloaded
async def async_reload_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry) -> None:
"""Reload config entry."""

LOGGER.info("Reloading Starling Home Hub integration")

async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry) -> bool:
"""Unload a config entry."""

LOGGER.info("Unloading Starling Home Hub integration")

unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
hass: HomeAssistant, config_entry: StarlingHomeHubConfigEntry, device_entry: DeviceEntry
) -> bool:
"""Remove a config entry from a device."""
LOGGER.info(f"Removing device {device_entry.serial_number}")
LOGGER.info(f"Removing device {
device_entry.serial_number} from config entry")

coordinator: StarlingHomeHubDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
starling_device_id = list(device_entry.identifiers)[0][1]

if starling_device_id in coordinator.data.devices:
if starling_device_id in config_entry.runtime_data.devices:
LOGGER.warning(f"Not removing device {
starling_device_id} - still in use")
return False

return True


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_migrate_entry(hass: HomeAssistant, config_entry: StarlingHomeHubConfigEntry) -> bool:
"""Migrate old entry."""

LOGGER.debug("Migrating configuration from version %s.%s",
config_entry.version, config_entry.minor_version)
LOGGER.info("Migrating configuration from version %s.%s",
config_entry.version, config_entry.minor_version)

if config_entry.version > 2:
# This means the user has downgraded from a future version
Expand All @@ -94,7 +102,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
hass.config_entries.async_update_entry(
config_entry, data=new_data, minor_version=0, version=2)

LOGGER.debug("Migration to configuration version %s.%s successful",
config_entry.version, config_entry.minor_version)
LOGGER.info("Migration to configuration version %s.%s successful",
config_entry.version, config_entry.minor_version)

return True
10 changes: 4 additions & 6 deletions custom_components/starling_home_hub/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@

from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.starling_home_hub.const import DOMAIN
from custom_components.starling_home_hub.entities.binary_sensor import StarlingHomeHubBinarySensorEntity
from custom_components.starling_home_hub.integrations import DEVICE_CATEGORIES_TO_PLATFORMS
from custom_components.starling_home_hub.models.coordinator import CoordinatorData
from custom_components.starling_home_hub.coordinator import StarlingHomeHubConfigEntry


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Set up the binary sensor platform."""

coordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[StarlingHomeHubBinarySensorEntity] = []
data: CoordinatorData = coordinator.data

for device in data.devices.items():
for device in entry.runtime_data.devices.items():
if device[1].properties["category"] in DEVICE_CATEGORIES_TO_PLATFORMS:
platforms = DEVICE_CATEGORIES_TO_PLATFORMS[device[1].properties["category"]]
if Platform.BINARY_SENSOR in platforms:
Expand All @@ -43,4 +41,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
)
)

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)
12 changes: 5 additions & 7 deletions custom_components/starling_home_hub/camera.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
"""Camera platform for Starling Home Hub."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.starling_home_hub.const import DOMAIN
from custom_components.starling_home_hub.integrations.camera_rtsp import StarlingHomeHubRTSPCamera
from custom_components.starling_home_hub.integrations.camera_webrtc import StarlingHomeHubWebRTCCamera
from custom_components.starling_home_hub.models.coordinator import CoordinatorData
from custom_components.starling_home_hub.coordinator import StarlingHomeHubDataUpdateCoordinator, StarlingHomeHubConfigEntry


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Set up the camera platform."""

coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: StarlingHomeHubDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[StarlingHomeHubWebRTCCamera |
StarlingHomeHubRTSPCamera] = []
data: CoordinatorData = coordinator.data

enable_rtsp_stream = entry.data.get("enable_rtsp_stream", False)
enable_webrtc_stream = entry.data.get("enable_webrtc_stream", False)

for device in filter(lambda device: device[1].properties["category"] == "cam", data.devices.items()):
for device in filter(lambda device: device[1].properties["category"] == "cam", entry.runtime_data.devices.items()):
if enable_webrtc_stream and "supportsWebRtcStreaming" in device[1].properties and device[1].properties["supportsWebRtcStreaming"]:
entities.append(
StarlingHomeHubWebRTCCamera(
Expand All @@ -39,4 +37,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
)
)

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)
12 changes: 5 additions & 7 deletions custom_components/starling_home_hub/climate.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
"""Support for Starling Home Hub thermostats."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.starling_home_hub.const import DOMAIN
from custom_components.starling_home_hub.entities.thermostat import StarlingHomeHubThermostatEntity
from custom_components.starling_home_hub.models.coordinator import CoordinatorData
from custom_components.starling_home_hub.coordinator import StarlingHomeHubDataUpdateCoordinator, StarlingHomeHubConfigEntry


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Set up the climate / thermostat platform."""

coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: StarlingHomeHubDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[StarlingHomeHubThermostatEntity] = []
data: CoordinatorData = coordinator.data

for device in filter(lambda device: device[1].properties["type"] == "thermostat", data.devices.items()):
for device in filter(lambda device: device[1].properties["type"] == "thermostat", entry.runtime_data.devices.items()):
entities.append(
StarlingHomeHubThermostatEntity(
device_id=device[0],
coordinator=coordinator
)
)

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)
8 changes: 4 additions & 4 deletions custom_components/starling_home_hub/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlow, FlowResult
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_create_clientsession
Expand All @@ -14,7 +14,7 @@
CONF_RTSP_USERNAME, DOMAIN, LOGGER)


class StarlingHomeHubFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class StarlingHomeHubFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Staring Home Hub."""

VERSION = 2
Expand Down Expand Up @@ -72,7 +72,7 @@ async def validate_credentials(self, user_input: dict, errors: dict[str, str]) -
async def async_step_user(
self,
user_input: dict | None = None,
) -> config_entries.FlowResult:
) -> FlowResult:
"""Handle a flow initialized by the user."""

errors = {}
Expand All @@ -91,7 +91,7 @@ async def async_step_user(
errors=errors,
)

async def async_step_reconfigure(self, user_input: dict | None = None) -> config_entries.FlowResult:
async def async_step_reconfigure(self, user_input: dict | None = None) -> FlowResult:
"""Handle reconfiguration of existing entry."""

reconfigure_entry = self._get_reconfigure_entry()
Expand Down
2 changes: 1 addition & 1 deletion custom_components/starling_home_hub/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

NAME = "Starling Home Hub Integration"
DOMAIN = "starling_home_hub"
VERSION = "1.4.0"
VERSION = "1.5.0"
ATTRIBUTION = "Based on the Starling Home Hub Developer Connect API"

CONF_ENABLE_RTSP_STREAM = "enable_rtsp_stream"
Expand Down
19 changes: 11 additions & 8 deletions custom_components/starling_home_hub/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,27 @@
from custom_components.starling_home_hub.models.api.stream import StartStream, StreamStatus
from custom_components.starling_home_hub.models.coordinator import CoordinatorData

type StarlingHomeHubConfigEntry = ConfigEntry[CoordinatorData]


class StarlingHomeHubDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""

config_entry: ConfigEntry
data: CoordinatorData
device_snapshots_cache: dict[str, bytes] = {}
config_entry: StarlingHomeHubConfigEntry

def __init__(
self,
hass: HomeAssistant,
client: StarlingHomeHubApiClient,
config_entry: StarlingHomeHubConfigEntry
) -> None:
"""Initialize."""
super().__init__(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=10),
config_entry=config_entry,
)
self.client = client

Expand All @@ -59,13 +61,13 @@ async def update_device(self, device_id: str, update: dict) -> DeviceUpdate:
async def get_snapshot(self, device_id: str) -> bytes:
"""Get a snapshot. Caches for 10 seconds."""

if device_id in self.device_snapshots_cache and self.device_snapshots_cache[device_id]["expires"] > self.hass.loop.time():
if device_id in self.config_entry.runtime_data.device_snapshots_cache and self.config_entry.runtime_data.device_snapshots_cache[device_id]["expires"] > self.hass.loop.time():
LOGGER.debug(f"Using cached snapshot for device {device_id}")
return self.device_snapshots_cache[device_id]["snapshot"]
return self.config_entry.runtime_data.device_snapshots_cache[device_id]["snapshot"]

LOGGER.debug(f"Fetching new snapshot for device {device_id}")
new_snapshot = await self.client.async_get_camera_snapshot(device_id=device_id)
self.device_snapshots_cache[device_id] = {
self.config_entry.runtime_data.device_snapshots_cache[device_id] = {
"snapshot": new_snapshot,
"expires": self.hass.loop.time() + 10,
}
Expand All @@ -83,9 +85,10 @@ async def fetch_data(self) -> CoordinatorData:
full_device = await self.client.async_get_device(device_id=device["id"])
full_devices[device["id"]] = full_device

self.data = CoordinatorData(devices=full_devices, status=status)
self.config_entry.runtime_data = CoordinatorData(
devices=full_devices, status=status)

return self.data
return self.config_entry.runtime_data

async def refresh_data(self) -> bool:
"""Refresh data."""
Expand Down
14 changes: 5 additions & 9 deletions custom_components/starling_home_hub/cover.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
"""Switch platform for Starling Home Hub."""

from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.starling_home_hub.const import DOMAIN
from custom_components.starling_home_hub.entities.cover import StarlingHomeHubCoverEntity
from custom_components.starling_home_hub.integrations import DEVICE_CATEGORIES_TO_PLATFORMS
from custom_components.starling_home_hub.models.coordinator import CoordinatorData
from custom_components.starling_home_hub.coordinator import StarlingHomeHubDataUpdateCoordinator, StarlingHomeHubConfigEntry


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Set up the cover platform."""

coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: StarlingHomeHubDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[StarlingHomeHubCoverEntity] = []
data: CoordinatorData = coordinator.data

for device in data.devices.items():
for device in entry.runtime_data.devices.items():
if device[1].properties["category"] in DEVICE_CATEGORIES_TO_PLATFORMS:
platforms = DEVICE_CATEGORIES_TO_PLATFORMS[device[1].properties["category"]]
if Platform.COVER in platforms:
Expand All @@ -34,4 +30,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
)
)

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)
2 changes: 1 addition & 1 deletion custom_components/starling_home_hub/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ def _handle_coordinator_update(self) -> None:

def get_device(self) -> Device:
"""Get the actual device data from coordinator."""
return self.coordinator.data.devices.get(self.device_id)
return self.coordinator.config_entry.runtime_data.devices.get(self.device_id)
12 changes: 5 additions & 7 deletions custom_components/starling_home_hub/fan.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
"""Fan entity for Starling Home Hub."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.starling_home_hub.const import DOMAIN
from custom_components.starling_home_hub.entities.fan import StarlingHomeHubFanEntity
from custom_components.starling_home_hub.models.coordinator import CoordinatorData
from custom_components.starling_home_hub.coordinator import StarlingHomeHubDataUpdateCoordinator, StarlingHomeHubConfigEntry


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: StarlingHomeHubConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Set up the fan platform."""

coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: StarlingHomeHubDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[StarlingHomeHubFanEntity] = []
data: CoordinatorData = coordinator.data

for device in filter(lambda device: device[1].properties["category"] == "fan", data.devices.items()):
for device in filter(lambda device: device[1].properties["category"] == "fan", entry.runtime_data.devices.items()):
entities.append(
StarlingHomeHubFanEntity(
device_id=device[0],
coordinator=coordinator
)
)

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)
Loading

0 comments on commit 69fd5ca

Please sign in to comment.