Merge branch 'dev' into esphome-config-flow-try-both-keys

This commit is contained in:
J. Nick Koston
2026-08-03 13:02:10 -05:00
committed by GitHub
395 changed files with 8623 additions and 1608 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"entries": {
"github/gh-aw-actions/setup-cli@v0.83.4": {
"repo": "github/gh-aw-actions/setup-cli",
"version": "v0.83.4",
"sha": "e89c65e17eb281bbd5ff2ff9e9199a03e96654c7"
},
"github/gh-aw-actions/setup@v0.83.4": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.83.4",
"sha": "e89c65e17eb281bbd5ff2ff9e9199a03e96654c7"
}
}
}
+1
View File
@@ -182,6 +182,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before. Comments in tests that explain why a function call or assertion is made are ok.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
- When catching exceptions, try-clauses should be as small as possible, i.e. avoid wrapping large blocks of code in a try-clause, and avoid catching exceptions from functions that are not expected to raise them.
- Sensitive service actions, i.e. those that can change configuration or have security implications, should require an admin user. Register them with the `async_register_admin_service` service helper, which checks this for you.
## AI policy
+3
View File
@@ -0,0 +1,3 @@
{
"maintenance": false
}
+3 -3
View File
@@ -342,13 +342,13 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -521,7 +521,7 @@ jobs:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -10,6 +10,8 @@ name: Check requirements (deterministic)
on:
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- master
paths:
- "requirements*.txt"
- "**/requirements*.txt"
File diff suppressed because one or more lines are too long
+5
View File
@@ -3,6 +3,11 @@ on:
workflow_run:
workflows: ["Check requirements (deterministic)"]
types: [completed]
# On workflow_run the actor is the PR author, so the default role gate
# (admin/maintainer/write) skips every PR from an outside contributor — which is
# exactly who this review is for. The job is read-only and its single safe-output
# is a sanitized comment on the PR recorded in the trusted upstream artifact.
roles: all
permissions:
contents: read
actions: read
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
# - Issues
# - No issues marked as no-stale or help-wanted
- name: 60 days stale PRs policy and 90 days stale issue policy
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
repo-token: ${{ steps.token.outputs.token }}
remove-stale-when-updated: true
+1
View File
@@ -45,6 +45,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before. Comments in tests that explain why a function call or assertion is made are ok.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
- When catching exceptions, try-clauses should be as small as possible, i.e. avoid wrapping large blocks of code in a try-clause, and avoid catching exceptions from functions that are not expected to raise them.
- Sensitive service actions, i.e. those that can change configuration or have security implications, should require an admin user. Register them with the `async_register_admin_service` service helper, which checks this for you.
## AI policy
Generated
+4 -4
View File
@@ -215,8 +215,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/azure_service_bus/ @hfurubotten
/homeassistant/components/azure_storage/ @zweckj
/tests/components/azure_storage/ @zweckj
/homeassistant/components/backblaze_b2/ @hugo-vrijswijk @ElCruncharino
/tests/components/backblaze_b2/ @hugo-vrijswijk @ElCruncharino
/homeassistant/components/backblaze_b2/ @ElCruncharino
/tests/components/backblaze_b2/ @ElCruncharino
/homeassistant/components/backup/ @home-assistant/core
/tests/components/backup/ @home-assistant/core
/homeassistant/components/baf/ @bdraco @jfroy
@@ -1139,8 +1139,8 @@ CLAUDE.md @home-assistant/core
/tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/microbees/ @microBeesTech
/tests/components/microbees/ @microBeesTech
/homeassistant/components/midea/ @chemelli74 @rokam @wuwentao
/tests/components/midea/ @chemelli74 @rokam @wuwentao
/homeassistant/components/midea/ @chemelli74 @rokam @wuwentao @caibinqing
/tests/components/midea/ @chemelli74 @rokam @wuwentao @caibinqing
/homeassistant/components/miele/ @astrandb
/tests/components/miele/ @astrandb
/homeassistant/components/mikrotik/ @engrbm87 @chemelli74
-4
View File
@@ -152,10 +152,6 @@ class AuthManagerFlowManager(
cast(Mapping[str, str], result["data"]),
)
if flow.context.get("credential_only"):
result["result"] = credentials
return result
# multi-factor module cannot enabled for new credential
# which has not linked to a user yet
if auth_provider.support_mfa and not credentials.is_new:
-1
View File
@@ -26,7 +26,6 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
class AuthFlowContext(FlowContext, total=False):
"""Typed context dict for auth flow."""
credential_only: bool
ip_address: IPv4Address | IPv6Address
redirect_uri: str
@@ -240,10 +240,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
(DOMAIN, serial_num), self.config_entry.entry_id
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
device_registry.async_remove_device(device.id)
async def _async_remove_routine_stale(
self,
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.1"]
"requirements": ["anova-wifi==1.0.1"]
}
+24 -2
View File
@@ -6,12 +6,21 @@ import anthemav
from anthemav.device_error import DeviceError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import ANTHEMAV_UPDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS
from .const import ANTHEMAV_UPDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN, MANUFACTURER
type AnthemavConfigEntry = ConfigEntry[anthemav.Connection]
@@ -43,6 +52,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthemavConfigEntry) ->
entry.runtime_data = avr
# Register the zone 1 receiver first so higher zones can link to it as a
# via device when their entities are created.
mac_address = entry.data[CONF_MAC]
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, mac_address)},
connections={(CONNECTION_NETWORK_MAC, mac_address)},
name=entry.title,
manufacturer=MANUFACTURER,
model=entry.data[CONF_MODEL],
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@callback
@@ -13,6 +13,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.const import CONF_MAC, CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -40,7 +41,13 @@ async def async_setup_entry(
async_add_entities(
AnthemAVR(
avr.protocol, name, mac_address, model, zone_number, config_entry.entry_id
hass,
avr.protocol,
name,
mac_address,
model,
zone_number,
config_entry.entry_id,
)
for zone_number in avr.protocol.zones
)
@@ -64,6 +71,7 @@ class AnthemAVR(MediaPlayerEntity):
def __init__(
self,
hass: HomeAssistant,
avr: AVR,
name: str,
mac_address: str,
@@ -85,7 +93,11 @@ class AnthemAVR(MediaPlayerEntity):
name=f"Zone {zone_number}",
manufacturer=MANUFACTURER,
model=model,
via_device=(DOMAIN, mac_address),
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, mac_address),
config_entry_id=entry_id,
),
)
else:
# Zone 1 is the physical receiver that owns the network MAC; higher
@@ -131,10 +131,7 @@ class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
self.config_entry.entry_id,
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
device_registry.async_remove_device(device.id)
self.previous_devices = current_devices
device_data = {}
@@ -10,6 +10,7 @@ from arcam.fmj.client import Client
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .const import DEFAULT_SCAN_INTERVAL
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator, ArcamFmjRuntimeData
@@ -29,6 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) ->
coordinator = ArcamFmjCoordinator(hass, entry, client, zone)
coordinators[zone] = coordinator
# Register the zone 1 device before forwarding platforms so the other zones'
# entities can link to it via via_device_id.
device_registry = dr.async_get(hass)
zone1_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
**coordinators[1].device_info,
)
for zone, coordinator in coordinators.items():
if zone != 1:
coordinator.device_info["via_device_id"] = zone1_device.id
entry.runtime_data = ArcamFmjRuntimeData(client, coordinators)
entry.async_create_background_task(
@@ -70,9 +70,6 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
)
self.zone_unique_id = f"{unique_id}-{zone}"
if zone != 1:
self.device_info["via_device"] = (DOMAIN, unique_id)
@override
async def _async_update_data(self) -> None:
"""Fetch data for manual refresh."""
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.10.0"]
"requirements": ["hassil==3.11.0"]
}
+4 -7
View File
@@ -22,8 +22,7 @@ Pass in parameter 'client_id' and 'redirect_url' validate by indieauth.
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id.
And optional parameter 'type' has to set as 'link_user' if login flow used for
link credential to exist user. Default 'type' is 'authorize'.
The default 'type' is 'authorize'.
{
"client_id": "https://hassbian.local:8123/",
@@ -54,9 +53,6 @@ Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
have type FlowResultType.CREATE_ENTRY and "result" key will contain
an authorization code.
The authorization code associated with an authorized user by default, it will
associate with an credential if "type" set to "link_user" in
"/auth/login_flow"
{
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
@@ -351,7 +347,9 @@ class LoginFlowIndexView(LoginFlowBaseView):
[vol.Any(str, None)], vol.Length(2, 2), vol.Coerce(tuple)
),
vol.Required("redirect_uri"): str,
vol.Optional("type", default="authorize"): str,
vol.Optional(
"type", default="authorize"
): str, # not used, kept for backwards compatibility
}
)
)
@@ -371,7 +369,6 @@ class LoginFlowIndexView(LoginFlowBaseView):
handler,
context=AuthFlowContext(
ip_address=ip_address(request.remote), # type: ignore[arg-type]
credential_only=data.get("type") == "link_user",
redirect_uri=redirect_uri,
),
)
@@ -1,7 +1,7 @@
{
"domain": "backblaze_b2",
"name": "Backblaze B2",
"codeowners": ["@hugo-vrijswijk", "@ElCruncharino"],
"codeowners": ["@ElCruncharino"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/backblaze_b2",
"integration_type": "service",
+7 -1
View File
@@ -9,6 +9,7 @@ from pyblu import Player
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -118,8 +119,13 @@ class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
# The leader (default-port) device is a separate config entry that may
# not be loaded yet; link to it only if it already exists.
if leader_devices := dr.async_get(coordinator.hass).async_get_devices(
identifiers={(DOMAIN, format_mac(sync_status.mac))}
):
self._attr_device_info["via_device_id"] = leader_devices[0].id
@override
async def async_press(self) -> None:
@@ -19,7 +19,11 @@ from homeassistant.components.media_player import (
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -131,8 +135,13 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
# The leader (default-port) device is a separate config entry that may
# not be loaded yet; link to it only if it already exists.
if leader_devices := dr.async_get(coordinator.hass).async_get_devices(
identifiers={(DOMAIN, format_mac(sync_status.mac))}
):
self._attr_device_info["via_device_id"] = leader_devices[0].id
@override
async def async_added_to_hass(self) -> None:
@@ -26,9 +26,11 @@ async def async_setup_entry(
async_add_entities(
AreaAlarmControlPanel(
hass,
panel,
area_id,
config_entry.unique_id or config_entry.entry_id,
config_entry.entry_id,
)
for area_id in panel.areas
)
@@ -48,9 +50,18 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
_attr_code_arm_required = False
_attr_name = None
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
area_id: int,
unique_id: str,
config_entry_id: str,
) -> None:
"""Initialise a Bosch Alarm control panel entity."""
super().__init__(panel, area_id, unique_id, True, False, True)
super().__init__(
hass, panel, area_id, unique_id, config_entry_id, True, False, True
)
self._attr_unique_id = self._area_unique_id
@property
@@ -116,16 +116,17 @@ async def async_setup_entry(
) -> None:
"""Set up binary sensors for alarm points and the connection status."""
panel = config_entry.runtime_data
unique_id = config_entry.unique_id or config_entry.entry_id
entities: list[BinarySensorEntity] = [
PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id)
PointSensor(hass, panel, point_id, unique_id, config_entry.entry_id)
for point_id in panel.points
]
entities.extend(
PanelFaultsSensor(
panel,
config_entry.unique_id or config_entry.entry_id,
unique_id,
fault_type,
)
for fault_type in FAULT_TYPES
@@ -133,14 +134,14 @@ async def async_setup_entry(
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "away"
hass, panel, area_id, unique_id, config_entry.entry_id, "away"
)
for area_id in panel.areas
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "home"
hass, panel, area_id, unique_id, config_entry.entry_id, "home"
)
for area_id in panel.areas
)
@@ -182,10 +183,18 @@ class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self, panel: Panel, area_id: int, unique_id: str, arm_type: str
self,
hass: HomeAssistant,
panel: Panel,
area_id: int,
unique_id: str,
config_entry_id: str,
arm_type: str,
) -> None:
"""Set up a binary sensor for arming status in a bosch panel."""
super().__init__(panel, area_id, unique_id, False, False, True)
super().__init__(
hass, panel, area_id, unique_id, config_entry_id, False, False, True
)
self.panel = panel
self._arm_type = arm_type
self._attr_translation_key = f"area_ready_to_arm_{arm_type}"
@@ -207,9 +216,16 @@ class PointSensor(BoschAlarmPointEntity, BinarySensorEntity):
_attr_name = None
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
point_id: int,
unique_id: str,
config_entry_id: str,
) -> None:
"""Set up a binary sensor entity for a point in a bosch alarm panel."""
super().__init__(panel, point_id, unique_id)
super().__init__(hass, panel, point_id, unique_id, config_entry_id)
self._attr_unique_id = self._point_unique_id
@property
+40 -7
View File
@@ -5,6 +5,8 @@ from typing import override
from bosch_alarm_mode2 import Panel
from homeassistant.components.sensor import Entity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN
@@ -56,9 +58,11 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
area_id: int,
unique_id: str,
config_entry_id: str,
observe_alarms: bool,
observe_ready: bool,
observe_status: bool,
@@ -75,7 +79,9 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
identifiers={(DOMAIN, self._area_unique_id)},
name=self._area.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, unique_id), config_entry_id=config_entry_id
),
)
@override
@@ -104,7 +110,14 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
class BoschAlarmPointEntity(BoschAlarmEntity):
"""A base entity for point related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
point_id: int,
unique_id: str,
config_entry_id: str,
) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._point_id = point_id
@@ -114,7 +127,9 @@ class BoschAlarmPointEntity(BoschAlarmEntity):
identifiers={(DOMAIN, self._point_unique_id)},
name=self._point.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, unique_id), config_entry_id=config_entry_id
),
)
@override
@@ -133,7 +148,14 @@ class BoschAlarmPointEntity(BoschAlarmEntity):
class BoschAlarmDoorEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, door_id: int, unique_id: str) -> None:
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
door_id: int,
unique_id: str,
config_entry_id: str,
) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._door_id = door_id
@@ -143,7 +165,9 @@ class BoschAlarmDoorEntity(BoschAlarmEntity):
identifiers={(DOMAIN, self._door_unique_id)},
name=self._door.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, unique_id), config_entry_id=config_entry_id
),
)
@override
@@ -162,7 +186,14 @@ class BoschAlarmDoorEntity(BoschAlarmEntity):
class BoschAlarmOutputEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
output_id: int,
unique_id: str,
config_entry_id: str,
) -> None:
"""Set up a output related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._output_id = output_id
@@ -172,7 +203,9 @@ class BoschAlarmOutputEntity(BoschAlarmEntity):
identifiers={(DOMAIN, self._output_unique_id)},
name=self._output.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, unique_id), config_entry_id=config_entry_id
),
)
@override
@@ -82,7 +82,9 @@ async def async_setup_entry(
unique_id = config_entry.unique_id or config_entry.entry_id
async_add_entities(
BoschAreaSensor(panel, area_id, unique_id, template)
BoschAreaSensor(
hass, panel, area_id, unique_id, config_entry.entry_id, template
)
for area_id in panel.areas
for template in SENSOR_TYPES
)
@@ -98,16 +100,20 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
area_id: int,
unique_id: str,
config_entry_id: str,
entity_description: BoschAlarmSensorEntityDescription,
) -> None:
"""Set up an area sensor entity for a bosch alarm panel."""
super().__init__(
hass,
panel,
area_id,
unique_id,
config_entry_id,
entity_description.observe_alarms,
entity_description.observe_ready,
entity_description.observe_status,
+17 -7
View File
@@ -59,18 +59,19 @@ async def async_setup_entry(
"""Set up switch entities for outputs."""
panel = config_entry.runtime_data
unique_id = config_entry.unique_id or config_entry.entry_id
entities: list[SwitchEntity] = [
PanelOutputEntity(
panel, output_id, config_entry.unique_id or config_entry.entry_id
)
PanelOutputEntity(hass, panel, output_id, unique_id, config_entry.entry_id)
for output_id in panel.outputs
]
entities.extend(
PanelDoorEntity(
hass,
panel,
door_id,
config_entry.unique_id or config_entry.entry_id,
unique_id,
config_entry.entry_id,
entity_description,
)
for door_id in panel.doors
@@ -90,13 +91,15 @@ class PanelDoorEntity(BoschAlarmDoorEntity, SwitchEntity):
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
door_id: int,
unique_id: str,
config_entry_id: str,
entity_description: BoschAlarmSwitchEntityDescription,
) -> None:
"""Set up a switch entity for a door on a bosch alarm panel."""
super().__init__(panel, door_id, unique_id)
super().__init__(hass, panel, door_id, unique_id, config_entry_id)
self.entity_description = entity_description
self._attr_unique_id = f"{self._door_unique_id}_{entity_description.key}"
@@ -134,9 +137,16 @@ class PanelOutputEntity(BoschAlarmOutputEntity, SwitchEntity):
_attr_name = None
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
def __init__(
self,
hass: HomeAssistant,
panel: Panel,
output_id: int,
unique_id: str,
config_entry_id: str,
) -> None:
"""Set up an output entity for a bosch alarm panel."""
super().__init__(panel, output_id, unique_id)
super().__init__(hass, panel, output_id, unique_id, config_entry_id)
self._attr_unique_id = self._output_unique_id
@property
@@ -37,6 +37,7 @@ async def async_setup_entry(
entities: list[BinarySensorEntity] = [
ShutterContactSensor(
hass=hass,
device=binary_sensor,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -49,6 +50,7 @@ async def async_setup_entry(
entities.extend(
BatterySensor(
hass=hass,
device=binary_sensor,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -75,9 +77,11 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity):
_attr_name = None
_device: SHCShutterContact
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
def __init__(
self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str
) -> None:
"""Initialize an SHC shutter contact sensor."""
super().__init__(device, parent_id, entry_id)
super().__init__(hass, device, parent_id, entry_id)
switcher: dict[str | None, BinarySensorDeviceClass] = {
"ENTRANCE_DOOR": BinarySensorDeviceClass.DOOR,
"REGULAR_WINDOW": BinarySensorDeviceClass.WINDOW,
@@ -101,9 +105,11 @@ class BatterySensor(SHCEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.BATTERY
_device: SHCBatteryDevice
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
def __init__(
self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str
) -> None:
"""Initialize an SHC battery reporting sensor."""
super().__init__(device, parent_id, entry_id)
super().__init__(hass, device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_battery"
@property
@@ -33,6 +33,7 @@ async def async_setup_entry(
async_add_entities(
ShutterControlCover(
hass=hass,
device=cover,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
+15 -37
View File
@@ -2,7 +2,7 @@
from typing import override
from boschshcpy import SHCDevice, SHCIntrusionSystem
from boschshcpy import SHCDevice
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
@@ -21,7 +21,7 @@ async def async_remove_devices(
(DOMAIN, entity.device_id), entry_id
)
if device is not None:
dev_registry.async_update_device(device.id, remove_config_entry_id=entry_id)
dev_registry.async_remove_device(device.id)
class SHCBaseEntity(Entity):
@@ -30,9 +30,7 @@ class SHCBaseEntity(Entity):
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self, device: SHCDevice | SHCIntrusionSystem, parent_id: str, entry_id: str
) -> None:
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize the generic SHC device."""
self._device = device
self._entry_id = entry_id
@@ -67,16 +65,25 @@ class SHCEntity(SHCBaseEntity):
_device: SHCDevice
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
def __init__(
self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str
) -> None:
"""Initialize generic SHC device."""
self._attr_unique_id = device.serial
self._attr_device_info = DeviceInfo(
device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
manufacturer=device.manufacturer,
model=device.device_model,
name=device.name,
via_device=(DOMAIN, device.root_device_id),
)
# boschshcpy may render the hub identifier (shc_info.unique_id) and a
# device's root_device_id differently, so the lookup can miss; link only
# when it resolves instead of raising out of setup.
if hub := dr.async_get(hass).async_get_device_by_identifier(
(DOMAIN, device.root_device_id), entry_id
):
device_info["via_device_id"] = hub.id
self._attr_device_info = device_info
super().__init__(device=device, parent_id=parent_id, entry_id=entry_id)
@override
@@ -102,32 +109,3 @@ class SHCEntity(SHCBaseEntity):
def available(self) -> bool:
"""Return false if status is unavailable."""
return self._device.status == "AVAILABLE"
class SHCDomainEntity(SHCBaseEntity):
"""Representation of a SHC domain service entity."""
_device: SHCIntrusionSystem
def __init__(
self, domain: SHCIntrusionSystem, parent_id: str, entry_id: str
) -> None:
"""Initialize the generic SHC device."""
self._attr_unique_id = domain.id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, domain.id)},
manufacturer=domain.manufacturer,
model=domain.device_model,
name=domain.name,
via_device=(
DOMAIN,
parent_id,
),
)
super().__init__(device=domain, parent_id=parent_id, entry_id=entry_id)
@property
@override
def available(self) -> bool:
"""Return false if status is unavailable."""
return self._device.system_availability
+7 -1
View File
@@ -212,6 +212,7 @@ async def async_setup_entry(
entities: list[SensorEntity] = [
SHCSensor(
hass,
device,
description,
shc_info.unique_id,
@@ -226,6 +227,7 @@ async def async_setup_entry(
entities.extend(
SHCSensor(
hass,
device,
description,
shc_info.unique_id,
@@ -240,6 +242,7 @@ async def async_setup_entry(
entities.extend(
SHCSensor(
hass,
device,
description,
shc_info.unique_id,
@@ -263,6 +266,7 @@ async def async_setup_entry(
]
entities.extend(
SHCSensor(
hass,
device,
description,
shc_info.unique_id,
@@ -274,6 +278,7 @@ async def async_setup_entry(
entities.extend(
SHCSensor(
hass,
device,
description,
shc_info.unique_id,
@@ -297,13 +302,14 @@ class SHCSensor[_DeviceT: SHCDevice](SHCEntity, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
device: _DeviceT,
entity_description: SHCSensorEntityDescription[_DeviceT],
parent_id: str,
entry_id: str,
) -> None:
"""Initialize sensor."""
super().__init__(device, parent_id, entry_id)
super().__init__(hass, device, parent_id, entry_id)
self._device: _DeviceT = device
self.entity_description = entity_description
self._attr_unique_id = f"{device.serial}_{entity_description.key}"
+12 -3
View File
@@ -89,6 +89,7 @@ async def async_setup_entry(
entities: list[SwitchEntity] = [
SHCSwitch(
hass=hass,
device=switch,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -99,6 +100,7 @@ async def async_setup_entry(
entities.extend(
SHCRoutingSwitch(
hass=hass,
device=switch,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -108,6 +110,7 @@ async def async_setup_entry(
entities.extend(
SHCSwitch(
hass=hass,
device=switch,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -118,6 +121,7 @@ async def async_setup_entry(
entities.extend(
SHCSwitch(
hass=hass,
device=switch,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -128,6 +132,7 @@ async def async_setup_entry(
entities.extend(
SHCSwitch(
hass=hass,
device=switch,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -138,6 +143,7 @@ async def async_setup_entry(
entities.extend(
SHCSwitch(
hass=hass,
device=switch,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
@@ -156,13 +162,14 @@ class SHCSwitch(SHCEntity, SwitchEntity):
def __init__(
self,
hass: HomeAssistant,
device: SHCDevice,
parent_id: str,
entry_id: str,
description: SHCSwitchEntityDescription,
) -> None:
"""Initialize a SHC switch."""
super().__init__(device, parent_id, entry_id)
super().__init__(hass, device, parent_id, entry_id)
self.entity_description = description
@property
@@ -202,9 +209,11 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity):
_attr_entity_category = EntityCategory.CONFIG
_device: SHCSmartPlug
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
def __init__(
self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str
) -> None:
"""Initialize an SHC routing switch."""
super().__init__(device, parent_id, entry_id)
super().__init__(hass, device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_routing"
@property
+1 -4
View File
@@ -38,7 +38,7 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
BruntDevice(coordinator, serial, thing, entry.entry_id)
BruntDevice(coordinator, serial, thing)
for serial, thing in coordinator.data.items()
)
@@ -64,20 +64,17 @@ class BruntDevice(CoordinatorEntity[BruntCoordinator], CoverEntity):
coordinator: BruntCoordinator,
serial: str | None,
thing: Thing,
entry_id: str,
) -> None:
"""Init the Brunt device."""
super().__init__(coordinator)
self._attr_unique_id = serial
self._thing = thing
self._entry_id = entry_id
self._remove_update_listener = None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)}, # type: ignore[arg-type]
name=self._thing.name,
via_device=(DOMAIN, self._entry_id),
manufacturer="Brunt",
sw_version=self._thing.fw_version,
model=self._thing.model,
@@ -153,10 +153,7 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
(DOMAIN, identifier), self.config_entry.entry_id
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
device_registry.async_remove_device(device.id)
class ComelitSerialBridge(ComelitBaseCoordinator[T]):
+5 -12
View File
@@ -68,13 +68,13 @@ async def cleanup_stale_entity(
identifiers.append(f"{config_entry.entry_id}-{device.type}-{device.index}")
if len(identifiers) > 0:
_async_remove_state_config_entry_from_devices(hass, identifiers, config_entry)
_async_remove_stale_devices(hass, identifiers, config_entry)
def _async_remove_state_config_entry_from_devices(
def _async_remove_stale_devices(
hass: HomeAssistant, identifiers: list[str], config_entry: ConfigEntry
) -> None:
"""Remove config entry from device."""
"""Remove stale devices."""
device_registry = dr.async_get(hass)
for identifier in identifiers:
@@ -82,15 +82,8 @@ def _async_remove_state_config_entry_from_devices(
(DOMAIN, identifier), config_entry.entry_id
)
if device:
LOGGER.info(
"Removing config entry %s from device %s",
config_entry.title,
device.name,
)
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=config_entry.entry_id,
)
LOGGER.info("Removing device %s", device.name)
device_registry.async_remove_device(device.id)
def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.10.0", "home-assistant-intents==2026.7.30"]
"requirements": ["hassil==3.11.0", "home-assistant-intents==2026.7.30"]
}
@@ -646,13 +646,13 @@ class ScannerEntity(
@override
def entity_registry_enabled_default(self) -> bool:
"""Return if entity is enabled by default."""
# If mac_address is None, we can never find a device entry.
# If mac_address is None, we can never find a matching device.
return (
# Do not disable if we won't activate our attach to device logic
# Do not disable if we won't activate our own device registration logic
self.mac_address is None
or self.device_info is not None
# Disable if we automatically attach but there is no device
or self.find_device_entry() is not None
# Disable if the tracked device is not known to any integration
or self._async_mac_address_registered()
)
@callback
@@ -681,12 +681,14 @@ class ScannerEntity(
)
@callback
def find_device_entry(self) -> dr.DeviceEntry | None:
"""Return device entry."""
def _async_mac_address_registered(self) -> bool:
"""Return if a device with the entity's MAC address is registered."""
assert self.mac_address is not None
return dr.async_get(self.hass).async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}
return bool(
dr.async_get(self.hass).async_get_devices(
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}
)
)
@override
@@ -697,7 +699,7 @@ class ScannerEntity(
not self.registry_entry
or not self.platform.config_entry
or not self.mac_address
or (device_entry := self.find_device_entry()) is None
or not self._async_mac_address_registered()
# Entities should not have a device info. We opt them out
# of this logic if they do.
or self.device_info
@@ -707,27 +709,18 @@ class ScannerEntity(
await super().async_internal_added_to_hass()
return
dev_reg = dr.async_get(self.hass)
# find_device_entry may return a synthesized pre-migration composite whose id is
# not a real device and can't be assigned to an entity; resolve it to the split
# owned by this config entry so we attach to a concrete device.
if device_entry.id not in dev_reg.devices:
device_entry = next(
(
split
for split in dev_reg.async_get_devices_for_composite_device_id(
device_entry.id
)
if split.config_entry_id == self.platform.config_entry.entry_id
),
None,
)
# Register our own device now that the tracked device is known to another
# integration; matching the split of a pre-migration composite device prunes
# the identifiers and connections copied from the composite.
device_entry = dr.async_get(self.hass).async_get_or_create(
config_entry_id=self.platform.config_entry.entry_id,
config_subentry_id=self.registry_entry.config_subentry_id,
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)},
default_name=self.hostname or self.mac_address,
)
# Attach entry to device
if (
device_entry is not None
and self.registry_entry.device_id != device_entry.id
):
# Link the entity's registry entry to the device
if self.registry_entry.device_id != device_entry.id:
self.registry_entry = er.async_get(self.hass).async_update_entity(
self.entity_id, device_id=device_entry.id
)
+1 -4
View File
@@ -262,10 +262,7 @@ async def async_setup_entry(
(DOMAIN, f"{mac}_{node_id}"), entry.entry_id
)
if device:
device_reg.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
)
device_reg.async_remove_device(device.id)
known_nodes.difference_update(stale_node_ids)
known_box_sensors.difference_update(
{
@@ -57,16 +57,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
serial_number=envoy.serial_number,
)
envoy_data = coordinator.envoy.data
# register the ACB aggregate device before the individual batteries reference
# it as via_device, so they nest under it in the device hierarchy
if (envoy_data := coordinator.envoy.data) and envoy_data.acb_inventory:
if envoy_data and envoy_data.acb_inventory:
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, f"{envoy.serial_number}_acb")},
manufacturer="Enphase",
model="ACB",
name=f"ACB {envoy.serial_number}",
via_device=(DOMAIN, envoy.serial_number),
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, envoy.serial_number), config_entry_id=entry.entry_id
),
)
# register the Enpower device before the dry contact relays reference it as
# via_device, so they nest under it in the device hierarchy
if envoy_data and (enpower := envoy_data.enpower):
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, enpower.serial_number)},
manufacturer="Enphase",
model="Enpower",
name=f"Enpower {enpower.serial_number}",
sw_version=str(enpower.firmware_version),
serial_number=enpower.serial_number,
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, envoy.serial_number), config_entry_id=entry.entry_id
),
)
entry.runtime_data = coordinator
@@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -209,7 +210,11 @@ class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity):
model="Encharge",
name=f"Encharge {serial_number}",
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=serial_number,
)
@@ -243,7 +248,11 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity):
model="Enpower",
name=f"Enpower {enpower.serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=enpower.serial_number,
)
@@ -277,7 +286,11 @@ class EnvoyCollarBinarySensorEntity(EnvoyBaseBinarySensorEntity):
model="IQ Meter Collar",
name=f"Collar {collar_data.serial_number}",
sw_version=str(collar_data.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=collar_data.serial_number,
)
@@ -311,7 +324,11 @@ class EnvoyC6CCBinarySensorEntity(EnvoyBaseBinarySensorEntity):
model="C6 COMBINER CONTROLLER",
name=f"C6 Combiner {c6cc_data.serial_number}",
sw_version=str(c6cc_data.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=c6cc_data.serial_number,
)
@@ -8,6 +8,7 @@ from pyenphase import EnvoyData
from pyenphase.exceptions import EnvoyError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -85,7 +86,11 @@ class EnvoyACBAggregateEntity(EnvoyBaseEntity):
manufacturer="Enphase",
model="ACB",
name=f"ACB {self.envoy_serial_num}",
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@@ -117,6 +122,10 @@ class EnvoyACBBatteryEntity(EnvoyBaseEntity):
manufacturer="Enphase",
model="AC Battery",
name=f"AC Battery {serial_number}",
via_device=(DOMAIN, f"{self.envoy_serial_num}_acb"),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, f"{self.envoy_serial_num}_acb"),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=serial_number,
)
@@ -16,6 +16,7 @@ from homeassistant.components.number import (
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -120,7 +121,11 @@ class EnvoyRelayNumberEntity(EnvoyBaseEntity, NumberEntity):
model="Dry contact relay",
name=self.data.dry_contact_settings[relay_id].load_name,
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, serial_number),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, serial_number),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@property
@@ -164,7 +169,11 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
model="Enpower",
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=self._serial_number,
)
else:
@@ -12,6 +12,7 @@ from pyenphase.models.tariff import EnvoyStorageMode, EnvoyStorageSettings
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -187,7 +188,11 @@ class EnvoyRelaySelectEntity(EnvoyBaseEntity, SelectEntity):
model="Dry contact relay",
name=self.relay.load_name,
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, serial_number),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, serial_number),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@property
@@ -232,7 +237,11 @@ class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
model="Enpower",
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=self._serial_number,
)
else:
@@ -50,6 +50,7 @@ from homeassistant.const import (
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -1347,7 +1348,11 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
name=f"Inverter {serial_number}",
manufacturer="Enphase",
model="Inverter",
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=serial_number,
)
@@ -1392,7 +1397,11 @@ class EnvoyEnchargeEntity(EnvoySensorBaseEntity):
model="Encharge",
name=f"Encharge {serial_number}",
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=serial_number,
)
@@ -1460,7 +1469,11 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
model="Enpower",
name=f"Enpower {enpower_data.serial_number}",
sw_version=str(enpower_data.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=enpower_data.serial_number,
)
@@ -1565,7 +1578,11 @@ class EnvoyCollarEntity(EnvoySensorBaseEntity):
model="IQ Meter Collar",
name=f"Collar {collar_data.serial_number}",
sw_version=str(collar_data.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=collar_data.serial_number,
)
@@ -1599,7 +1616,11 @@ class EnvoyC6CCEntity(EnvoySensorBaseEntity):
model="C6 COMBINER CONTROLLER",
name=f"C6 Combiner {c6cc_data.serial_number}",
sw_version=str(c6cc_data.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=c6cc_data.serial_number,
)
@@ -11,6 +11,7 @@ from pyenphase.models.tariff import EnvoyStorageSettings
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -135,7 +136,11 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity):
model="Enpower",
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=self._serial_number,
)
@@ -189,7 +194,11 @@ class EnvoyDryContactSwitchEntity(EnvoyBaseEntity, SwitchEntity):
model="Dry contact relay",
name=relay.load_name,
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, enpower.serial_number),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, enpower.serial_number),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@property
@@ -239,7 +248,11 @@ class EnvoyStorageSettingsSwitchEntity(EnvoyBaseEntity, SwitchEntity):
model="Enpower",
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, self.envoy_serial_num),
config_entry_id=coordinator.config_entry.entry_id,
),
serial_number=self._serial_number,
)
else:
+7 -2
View File
@@ -186,9 +186,13 @@ class FibaroController:
identifiers={(DOMAIN, main_device.fibaro_id)},
manufacturer=manufacturer,
name=main_device.name,
via_device=(DOMAIN, self.hub_serial),
)
def link_main_devices_to_hub(self, via_device_id: str) -> None:
"""Link all main devices to the hub via device."""
for device_info in self._device_infos.values():
device_info["via_device_id"] = via_device_id
def get_device_info(self, device: DeviceModel) -> DeviceInfo:
"""Get the device info by fibaro device id."""
if device.fibaro_id in self._device_infos:
@@ -321,7 +325,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bo
# register the hub device info separately as the hub has sometimes no entities
fibaro_info = controller.read_fibaro_info()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
hub_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, controller.hub_serial)},
serial_number=controller.hub_serial,
@@ -332,6 +336,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bo
configuration_url=controller.get_frontend_url(),
connections={(dr.CONNECTION_NETWORK_MAC, fibaro_info.mac_address)},
)
controller.link_main_devices_to_hub(hub_device.id)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+7 -1
View File
@@ -8,7 +8,7 @@ from freebox_api.exceptions import HttpRequestError
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.event import async_track_time_interval
from .const import DOMAIN, PLATFORMS
@@ -96,6 +96,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> b
entry.runtime_data = router
# Register the router device up front so child devices (home devices, disks)
# can deterministically resolve it as their via_device on any platform.
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id, **router.device_info
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def async_close_connection(event: Event) -> None:
+5 -1
View File
@@ -54,7 +54,11 @@ class FreeboxHomeEntity(Entity):
model=self._model,
name=node["label"].strip(),
sw_version=self._firmware,
via_device=(DOMAIN, router.mac),
via_device_id=dr.async_get_device_id_by_identifier(
router.hass,
(DOMAIN, router.mac),
config_entry_id=router.config_entry.entry_id,
),
)
async def async_update_signal(self) -> None:
@@ -108,6 +108,7 @@ class FreeboxRouter:
) -> None:
"""Initialize a Freebox router."""
self.hass = hass
self.config_entry = entry
self._host = entry.data[CONF_HOST]
self._port = entry.data[CONF_PORT]
+5 -3
View File
@@ -17,6 +17,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -246,9 +247,10 @@ class FreeboxDiskSensor(FreeboxSensor):
model=disk["model"],
name=f"Disk {disk['id']}",
sw_version=disk["firmware"],
via_device=(
DOMAIN,
router.mac,
via_device_id=dr.async_get_device_id_by_identifier(
router.hass,
(DOMAIN, router.mac),
config_entry_id=router.config_entry.entry_id,
),
)
@@ -88,10 +88,7 @@ class FreshrDevicesCoordinator(DataUpdateCoordinator[dict[str, DeviceSummary]]):
if device := device_registry.async_get_device_by_identifier(
(DOMAIN, device_id), self.config_entry.entry_id
):
device_registry.async_update_device(
device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
device_registry.async_remove_device(device.id)
return current
+1 -4
View File
@@ -101,10 +101,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
(DOMAIN, f"{self.config_entry.entry_id}-{plant_id}"),
self.config_entry.entry_id,
):
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
device_registry.async_remove_device(device.id)
_LOGGER.debug("Device removed from device registry: %s", device.id)
# add new devices
@@ -70,10 +70,7 @@ class GatusDataUpdateCoordinator(DataUpdateCoordinator[dict[str, EndpointStatus]
and identifier[1].removeprefix(f"{self._entry_id}_") in stale_keys
for identifier in device.identifiers
):
device_registry.async_update_device(
device.id,
remove_config_entry_id=self._entry_id,
)
device_registry.async_remove_device(device.id)
self._known_endpoint_keys = current_keys
return {ep.key: ep for ep in raw_endpoints}
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["google-genai==1.59.0"]
"requirements": ["google-genai==2.16.0"]
}
@@ -23,7 +23,6 @@ Error handling pattern for reauth:
raise ConfigEntryAuthFailed
- All other errors ConfigEntryError (setup) or UpdateFailed (coordinator)
"""
# pylint: disable=home-assistant-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from collections.abc import Mapping
import datetime
@@ -49,7 +48,6 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
AUTH_API_TOKEN,
AUTH_PASSWORD,
CACHED_API_KEY,
CONF_AUTH_TYPE,
CONF_PLANT_ID,
DEFAULT_PLANT_ID,
@@ -70,6 +68,11 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
# Temporary handoff of authenticated Classic API sessions from async_migrate_entry
# to async_setup_entry. Avoids a second login() during the same startup, which
# would hit Growatt's 5-minute per-endpoint rate limit. Popped after first use.
_CACHED_APIS: dict[str, growattServer.GrowattApi] = {}
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Growatt Server integration."""
@@ -196,8 +199,7 @@ async def async_migrate_entry(
)
# Cache the logged-in API instance for reuse in async_setup_entry()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][f"{CACHED_API_KEY}{config_entry.entry_id}"] = api
_CACHED_APIS[config_entry.entry_id] = api
_LOGGER.info(
"Migrated config entry to use specific plant_id '%s'",
@@ -347,9 +349,7 @@ async def async_setup_entry(
# Check if migration cached an authenticated API instance for us to reuse.
# This avoids calling login() twice (once in migration, once here) which
# would trigger rate limiting.
cached_api = hass.data.get(DOMAIN, {}).pop(
f"{CACHED_API_KEY}{config_entry.entry_id}", None
)
cached_api = _CACHED_APIS.pop(config_entry.entry_id, None)
if cached_api:
# Reuse the logged-in API instance from migration (rate limit optimization)
@@ -55,11 +55,6 @@ BATT_MODE_LOAD_FIRST = 0
BATT_MODE_BATTERY_FIRST = 1
BATT_MODE_GRID_FIRST = 2
# Internal key prefix for caching authenticated API instance
# Used to pass logged-in session from async_migrate_entry to async_setup_entry
# to avoid double login() calls that trigger API rate limiting
CACHED_API_KEY = "_cached_api_"
# Supported device types for coordinator creation
SUPPORTED_DEVICE_TYPES = ["inverter", "tlx", "storage", "mix", "min", "sph"]
@@ -115,6 +115,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: GuardianConfigEntry) ->
paired_sensor_manager=paired_sensor_manager,
)
# Register the valve controller device up front so paired sensors can resolve
# it as their via device when they are added:
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.data[CONF_UID])},
manufacturer="Elexa",
name=f"Guardian valve controller {entry.data[CONF_UID]}",
sw_version=valve_controller_coordinators[API_SYSTEM_DIAGNOSTICS].data[
"firmware"
],
)
# Set up all of the Guardian entity platforms:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+6 -1
View File
@@ -2,6 +2,7 @@
from dataclasses import dataclass
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -43,7 +44,11 @@ class PairedSensorEntity(GuardianEntity):
manufacturer="Elexa",
model=coordinator.data["codename"],
name=f"Guardian paired sensor {paired_sensor_uid}",
via_device=(DOMAIN, entry.data[CONF_UID]),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, entry.data[CONF_UID]),
config_entry_id=entry.entry_id,
),
)
self._attr_unique_id = f"{paired_sensor_uid}_{description.key}"
+37 -1
View File
@@ -1,8 +1,10 @@
"""The habitica integration."""
from typing import TYPE_CHECKING
from uuid import UUID
from habiticalib import Habitica
from yarl import URL
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
@@ -16,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import CONF_API_USER, DOMAIN, X_CLIENT
from .const import CONF_API_USER, DOMAIN, MANUFACTURER, NAME, X_CLIENT
from .coordinator import (
HabiticaConfigEntry,
HabiticaDataUpdateCoordinator,
@@ -71,6 +73,23 @@ async def async_setup_entry(
config_entry.runtime_data = coordinator
if TYPE_CHECKING:
assert config_entry.unique_id
# Register the user device so children (party device) can resolve it
# deterministically as their via_device parent before platforms are set up.
device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
entry_type=dr.DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
model=NAME,
name=coordinator.data.user.profile.name,
configuration_url=(
URL(config_entry.data[CONF_URL]) / "profile" / config_entry.unique_id
),
identifiers={(DOMAIN, config_entry.unique_id)},
)
party = coordinator.data.user.party.id
hass.data.setdefault(HABITICA_KEY, {})
@@ -81,6 +100,23 @@ async def async_setup_entry(
hass.data[HABITICA_KEY][party] = party_coordinator
party_added_by_this_entry = party
if party is not None:
# Register the party device so party member devices can resolve it
# deterministically as their via_device parent.
device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
entry_type=dr.DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
model=NAME,
name=hass.data[HABITICA_KEY][party].data.party.summary,
identifiers={(DOMAIN, f"{config_entry.unique_id}_{party!s}")},
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, config_entry.unique_id),
config_entry_id=config_entry.entry_id,
),
)
@callback
def _party_update_listener() -> None:
"""On party change, unload coordinator, remove device and reload."""
+14 -9
View File
@@ -8,6 +8,7 @@ from yarl import URL
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_URL
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -57,14 +58,14 @@ class HabiticaBase(CoordinatorEntity[HabiticaDataUpdateCoordinator]):
)
if subentry:
self._attr_device_info.update(
DeviceInfo(
via_device=(
(
DOMAIN,
f"{coordinator.config_entry.unique_id}_{self.user.party.id}",
)
)
self._attr_device_info["via_device_id"] = (
dr.async_get_device_id_by_identifier(
coordinator.hass,
(
DOMAIN,
f"{coordinator.config_entry.unique_id}_{self.user.party.id}",
),
config_entry_id=coordinator.config_entry.entry_id,
)
)
@@ -138,6 +139,10 @@ class HabiticaPartyBase(CoordinatorEntity[HabiticaPartyCoordinator]):
model=NAME,
name=coordinator.data.party.summary,
identifiers={(DOMAIN, unique_id)},
via_device=(DOMAIN, config_entry.unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, config_entry.unique_id),
config_entry_id=config_entry.entry_id,
),
)
self.content = content
@@ -388,7 +388,8 @@ def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
def next_datetime(simple_time: time) -> datetime:
"""Take a time like 08:00:00 and combine it with the current date."""
combined = datetime.combine(dt_util.start_of_local_day(), simple_time)
if combined < datetime.now(): # pylint: disable=home-assistant-enforce-naive-now
start_of_day = dt_util.start_of_local_day()
combined = datetime.combine(start_of_day, simple_time, start_of_day.tzinfo)
if combined < dt_util.now():
combined = combined + timedelta(days=1)
return combined
@@ -267,6 +267,7 @@ async def async_setup_entry(
seen_channels.add(channel)
entities.append(
HikvisionBinarySensor(
hass=hass,
entry=entry,
description=BINARY_SENSOR_DESCRIPTIONS[sensor_type],
sensor_type=sensor_type,
@@ -284,13 +285,14 @@ class HikvisionBinarySensor(HikvisionEntity, BinarySensorEntity):
def __init__(
self,
hass: HomeAssistant,
entry: HikvisionConfigEntry,
description: BinarySensorEntityDescription,
sensor_type: str,
channel: int,
) -> None:
"""Initialize the binary sensor."""
super().__init__(entry, channel)
super().__init__(hass, entry, channel)
self.entity_description = description
self._sensor_type = sensor_type
+4 -2
View File
@@ -27,7 +27,7 @@ async def async_setup_entry(
if data.channels:
# NVR with video channels from get_video_channels()
async_add_entities(
HikvisionCamera(entry, channel)
HikvisionCamera(hass, entry, channel)
for channel in data.channels.values()
if channel.enabled
)
@@ -36,6 +36,7 @@ async def async_setup_entry(
async_add_entities(
[
HikvisionCamera(
hass,
entry,
VideoChannel(id=1, name=data.device_name, enabled=True),
)
@@ -51,11 +52,12 @@ class HikvisionCamera(HikvisionEntity, Camera):
def __init__(
self,
hass: HomeAssistant,
entry: HikvisionConfigEntry,
channel: VideoChannel,
) -> None:
"""Initialize the camera."""
super().__init__(entry, channel.id)
super().__init__(hass, entry, channel.id)
self._video_channel = channel
# Build unique ID (unique per platform per integration)
+7 -3
View File
@@ -1,5 +1,7 @@
"""Base entity for Hikvision integration."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
@@ -14,6 +16,7 @@ class HikvisionEntity(Entity):
def __init__(
self,
hass: HomeAssistant,
entry: HikvisionConfigEntry,
channel: int,
) -> None:
@@ -23,12 +26,13 @@ class HikvisionEntity(Entity):
self._camera = self._data.camera
self._channel = channel
# Device info for device registry
if self._data.device_type == "NVR":
# NVR channels get their own device linked to the NVR via via_device
# NVR channels get their own device linked to the NVR via via_device_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
via_device=(DOMAIN, self._data.device_id),
via_device_id=dr.async_get_device_id_by_identifier(
hass, (DOMAIN, self._data.device_id), config_entry_id=entry.entry_id
),
translation_key="nvr_channel",
translation_placeholders={
"device_name": self._data.device_name,
@@ -79,7 +79,7 @@ async def async_setup_entry(
devices = hive.session.deviceList.get("binary_sensor")
sensors.extend(
HiveBinarySensorEntity(hive, dev, description)
HiveBinarySensorEntity(hass, entry, hive, dev, description)
for dev in devices
for description in BINARY_SENSOR_TYPES
if dev["hiveType"] == description.key
@@ -87,7 +87,7 @@ async def async_setup_entry(
devices = hive.session.deviceList.get("sensor")
sensors.extend(
HiveSensorEntity(hive, dev, description)
HiveSensorEntity(hass, entry, hive, dev, description)
for dev in devices
for description in SENSOR_TYPES
if dev["hiveType"] == description.key
@@ -101,12 +101,14 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
entity_description: BinarySensorEntityDescription,
) -> None:
"""Initialise hive binary sensor."""
super().__init__(hive, hive_device)
super().__init__(hass, entry, hive, hive_device)
self.entity_description = entity_description
async def async_update(self) -> None:
@@ -131,12 +133,14 @@ class HiveSensorEntity(HiveEntity, BinarySensorEntity):
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
entity_description: BinarySensorEntityDescription,
) -> None:
"""Initialise hive sensor."""
super().__init__(hive, hive_device)
super().__init__(hass, entry, hive, hive_device)
self.entity_description = entity_description
async def async_update(self) -> None:
+11 -3
View File
@@ -61,7 +61,9 @@ async def async_setup_entry(
hive = entry.runtime_data
devices = hive.session.deviceList.get("climate")
if devices:
async_add_entities((HiveClimateEntity(hive, dev) for dev in devices), True)
async_add_entities(
(HiveClimateEntity(hass, entry, hive, dev) for dev in devices), True
)
platform = entity_platform.async_get_current_platform()
@@ -97,9 +99,15 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
| ClimateEntityFeature.TURN_ON
)
def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None:
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
) -> None:
"""Initialize the Climate device."""
super().__init__(hive, hive_device)
super().__init__(hass, entry, hive, hive_device)
self.thermostat_node_id = hive_device["device_id"]
self._attr_temperature_unit = TEMP_UNIT[hive_device["temperatureunit"]]
+18 -3
View File
@@ -1,20 +1,31 @@
"""Support for the Hive devices and services."""
from typing import Any, override
from typing import TYPE_CHECKING, Any, override
from apyhiveapi import Hive
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
if TYPE_CHECKING:
from . import HiveConfigEntry
class HiveEntity(Entity):
"""Initiate Hive Base Class."""
def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None:
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
) -> None:
"""Initialize the instance."""
self.hive = hive
self.device = hive_device
@@ -26,7 +37,11 @@ class HiveEntity(Entity):
manufacturer=self.device["deviceData"]["manufacturer"],
name=self.device["device_name"],
sw_version=self.device["deviceData"]["version"],
via_device=(DOMAIN, self.device["parentDevice"]),
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, self.device["parentDevice"]),
config_entry_id=entry.entry_id,
),
)
self.attributes: dict[str, Any] = {}
+11 -3
View File
@@ -35,7 +35,9 @@ async def async_setup_entry(
devices = hive.session.deviceList.get("light")
if not devices:
return
async_add_entities((HiveDeviceLight(hive, dev) for dev in devices), True)
async_add_entities(
(HiveDeviceLight(hass, entry, hive, dev) for dev in devices), True
)
class HiveDeviceLight(HiveEntity, LightEntity):
@@ -44,9 +46,15 @@ class HiveDeviceLight(HiveEntity, LightEntity):
_attr_min_color_temp_kelvin = 2700 # 370 Mireds
_attr_max_color_temp_kelvin = 6500 # 153 Mireds
def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None:
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
) -> None:
"""Initialise hive light."""
super().__init__(hive, hive_device)
super().__init__(hass, entry, hive, hive_device)
if self.device["hiveType"] == "warmwhitelight":
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
self._attr_color_mode = ColorMode.BRIGHTNESS
+4 -2
View File
@@ -99,7 +99,7 @@ async def async_setup_entry(
return
async_add_entities(
(
HiveSensorEntity(hive, dev, description)
HiveSensorEntity(hass, entry, hive, dev, description)
for dev in devices
for description in SENSOR_TYPES
if dev["hiveType"] == description.key
@@ -115,12 +115,14 @@ class HiveSensorEntity(HiveEntity, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
entity_description: HiveSensorEntityDescription,
) -> None:
"""Initialise hive sensor."""
super().__init__(hive, hive_device)
super().__init__(hass, entry, hive, hive_device)
self.entity_description = entity_description
async def async_update(self) -> None:
+4 -2
View File
@@ -41,7 +41,7 @@ async def async_setup_entry(
return
async_add_entities(
(
HiveSwitch(hive, dev, description)
HiveSwitch(hass, entry, hive, dev, description)
for dev in devices
for description in SWITCH_TYPES
if dev["hiveType"] == description.key
@@ -55,12 +55,14 @@ class HiveSwitch(HiveEntity, SwitchEntity):
def __init__(
self,
hass: HomeAssistant,
entry: HiveConfigEntry,
hive: Hive,
hive_device: dict[str, Any],
entity_description: SwitchEntityDescription,
) -> None:
"""Initialise hive switch."""
super().__init__(hive, hive_device)
super().__init__(hass, entry, hive, hive_device)
self.entity_description = entity_description
@refresh_system
@@ -52,7 +52,9 @@ async def async_setup_entry(
hive = entry.runtime_data
devices = hive.session.deviceList.get("water_heater")
if devices:
async_add_entities((HiveWaterHeater(hive, dev) for dev in devices), True)
async_add_entities(
(HiveWaterHeater(hass, entry, hive, dev) for dev in devices), True
)
platform = entity_platform.async_get_current_platform()
@@ -68,14 +68,14 @@ class OAuth2FlowHandler(
) -> ConfigFlowResult:
"""Handle a DHCP discovery."""
device_registry = dr.async_get(self.hass)
if device_entry := device_registry.async_get_device(
for device in device_registry.async_get_devices(
identifiers={
(DOMAIN, discovery_info.hostname),
(DOMAIN, discovery_info.hostname.split("-")[-1]),
}
):
device_registry.async_update_device(
device_entry.id,
device.id,
new_connections={
(dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress)
},
@@ -368,10 +368,7 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
(DOMAIN, self.data.info.ha_id), self._config_entry.entry_id
)
if device:
self.device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self._config_entry.entry_id,
)
self.device_registry.async_remove_device(device.id)
for (
listener,
context,
+1 -4
View File
@@ -117,10 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
)
if device:
_LOGGER.info("Removing device %s", device.name)
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=entry.entry_id,
)
device_registry.async_remove_device(device.id)
homee.add_nodes_listener(_remove_node_callback)
@@ -61,13 +61,16 @@ def get_supported_features(
async def add_alarm_control_panel_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee alarm control panel entities."""
async_add_entities(
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
HomeeAlarmPanel(
hass, attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type]
)
for node in nodes
for attribute in node.attributes
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
@@ -82,7 +85,7 @@ async def async_setup_entry(
"""Add the homee platform for the alarm control panel component."""
await setup_homee_platform(
add_alarm_control_panel_entities, async_add_entities, config_entry
hass, add_alarm_control_panel_entities, async_add_entities, config_entry
)
@@ -93,12 +96,13 @@ class HomeeAlarmPanel(HomeeEntity, AlarmControlPanelEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeAlarmControlPanelEntityDescription,
) -> None:
"""Initialize a Homee alarm control panel entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
self._attr_code_arm_required = description.code_arm_required
self._attr_supported_features = get_supported_features(description.state_list)
@@ -156,6 +156,7 @@ BINARY_SENSOR_DESCRIPTIONS: dict[AttributeType, BinarySensorEntityDescription] =
async def add_binary_sensor_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
@@ -163,7 +164,7 @@ async def add_binary_sensor_entities(
"""Add homee binary sensor entities."""
async_add_entities(
HomeeBinarySensor(
attribute, config_entry, BINARY_SENSOR_DESCRIPTIONS[attribute.type]
hass, attribute, config_entry, BINARY_SENSOR_DESCRIPTIONS[attribute.type]
)
for node in nodes
for attribute in node.attributes
@@ -179,7 +180,7 @@ async def async_setup_entry(
"""Add the homee platform for the binary sensor component."""
await setup_homee_platform(
add_binary_sensor_entities, async_add_entities, config_entry
hass, add_binary_sensor_entities, async_add_entities, config_entry
)
@@ -188,12 +189,13 @@ class HomeeBinarySensor(HomeeEntity, BinarySensorEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize a Homee binary sensor entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
+7 -3
View File
@@ -43,13 +43,14 @@ BUTTON_DESCRIPTIONS: dict[AttributeType, ButtonEntityDescription] = {
async def add_button_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee button entities."""
async_add_entities(
HomeeButton(attribute, config_entry, BUTTON_DESCRIPTIONS[attribute.type])
HomeeButton(hass, attribute, config_entry, BUTTON_DESCRIPTIONS[attribute.type])
for node in nodes
for attribute in node.attributes
if attribute.type in BUTTON_DESCRIPTIONS and attribute.editable
@@ -63,7 +64,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the button component."""
await setup_homee_platform(add_button_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_button_entities, async_add_entities, config_entry
)
class HomeeButton(HomeeEntity, ButtonEntity):
@@ -71,12 +74,13 @@ class HomeeButton(HomeeEntity, ButtonEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: ButtonEntityDescription,
) -> None:
"""Initialize a Homee button entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
if attribute.instance == 0:
if attribute.type == AttributeType.IMPULSE:
+9 -4
View File
@@ -33,13 +33,14 @@ ROOM_THERMOSTATS = {
async def add_climate_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee climate entities."""
async_add_entities(
HomeeClimate(node, config_entry)
HomeeClimate(hass, node, config_entry)
for node in nodes
if node.profile in CLIMATE_PROFILES
)
@@ -52,7 +53,9 @@ async def async_setup_entry(
) -> None:
"""Add the Homee platform for the climate component."""
await setup_homee_platform(add_climate_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_climate_entities, async_add_entities, config_entry
)
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
@@ -61,9 +64,11 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
_attr_name = None
_attr_translation_key = DOMAIN
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, node: HomeeNode, entry: HomeeConfigEntry
) -> None:
"""Initialize a Homee climate entity."""
super().__init__(node, entry)
super().__init__(hass, node, entry)
(
self._attr_supported_features,
+9 -4
View File
@@ -101,6 +101,7 @@ def get_device_class(node: HomeeNode) -> CoverDeviceClass | None:
async def add_cover_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
@@ -122,7 +123,7 @@ async def add_cover_entities(
node.name,
)
async_add_entities(HomeeCover(cover, config_entry) for cover in entities)
async_add_entities(HomeeCover(hass, cover, config_entry) for cover in entities)
async def async_setup_entry(
@@ -132,7 +133,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the cover integration."""
await setup_homee_platform(add_cover_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_cover_entities, async_add_entities, config_entry
)
def is_cover_node(node: HomeeNode) -> bool:
@@ -151,9 +154,11 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
_attr_name = None
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, node: HomeeNode, entry: HomeeConfigEntry
) -> None:
"""Initialize a homee cover entity."""
super().__init__(node, entry)
super().__init__(hass, node, entry)
self._open_close_attribute = get_open_close_attribute(node)
self._attr_supported_features = get_cover_features(
node, self._open_close_attribute
+22 -5
View File
@@ -7,7 +7,9 @@ from pyHomee.const import AttributeType, NodeProfile, NodeState
from pyHomee.model import HomeeAttribute, HomeeNode
from websockets.exceptions import ConnectionClosed
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
@@ -26,7 +28,9 @@ class HomeeEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, attribute: HomeeAttribute, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, attribute: HomeeAttribute, entry: HomeeConfigEntry
) -> None:
"""Initialize the wrapper using a HomeeAttribute and target entity."""
self._attribute = attribute
self._attr_unique_id = (
@@ -43,11 +47,18 @@ class HomeeEntity(Entity):
else:
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
(
DOMAIN,
f"{entry.runtime_data.settings.uid}-{attribute.node_id}",
)
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, entry.runtime_data.settings.uid),
config_entry_id=entry.entry_id,
),
)
if attribute.name:
self._attr_name = attribute.name
@@ -106,7 +117,9 @@ class HomeeNodeEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, node: HomeeNode, entry: HomeeConfigEntry
) -> None:
"""Initialize the wrapper using a HomeeNode and target entity."""
self._node = node
self._attr_unique_id = f"{entry.unique_id}-{node.id}"
@@ -123,7 +136,11 @@ class HomeeNodeEntity(Entity):
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
sw_version=self._get_software_version(),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, entry.runtime_data.settings.uid),
config_entry_id=entry.entry_id,
),
)
self._host_connected = entry.runtime_data.connected
+7 -3
View File
@@ -54,13 +54,14 @@ EVENT_DESCRIPTIONS: dict[AttributeType, EventEntityDescription] = {
async def add_event_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee event entities."""
async_add_entities(
HomeeEvent(attribute, config_entry, EVENT_DESCRIPTIONS[attribute.type])
HomeeEvent(hass, attribute, config_entry, EVENT_DESCRIPTIONS[attribute.type])
for node in nodes
for attribute in node.attributes
if attribute.type in EVENT_DESCRIPTIONS
@@ -76,7 +77,9 @@ async def async_setup_entry(
) -> None:
"""Add event entities for homee."""
await setup_homee_platform(add_event_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_event_entities, async_add_entities, config_entry
)
class HomeeEvent(HomeeEntity, EventEntity):
@@ -84,12 +87,13 @@ class HomeeEvent(HomeeEntity, EventEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: EventEntityDescription,
) -> None:
"""Initialize the homee event entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
if attribute.instance > 0:
+7 -4
View File
@@ -25,13 +25,14 @@ PARALLEL_UPDATES = 0
async def add_fan_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee fan entities."""
async_add_entities(
HomeeFan(node, config_entry)
HomeeFan(hass, node, config_entry)
for node in nodes
if node.profile == NodeProfile.VENTILATION_CONTROL
)
@@ -44,7 +45,7 @@ async def async_setup_entry(
) -> None:
"""Set up the Homee fan platform."""
await setup_homee_platform(add_fan_entities, async_add_entities, config_entry)
await setup_homee_platform(hass, add_fan_entities, async_add_entities, config_entry)
class HomeeFan(HomeeNodeEntity, FanEntity):
@@ -56,9 +57,11 @@ class HomeeFan(HomeeNodeEntity, FanEntity):
speed_range = (1, 8)
_attr_speed_count = int_states_in_range(speed_range)
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, node: HomeeNode, entry: HomeeConfigEntry
) -> None:
"""Initialize a Homee fan entity."""
super().__init__(node, entry)
super().__init__(hass, node, entry)
self._speed_attribute: HomeeAttribute = cast(
HomeeAttribute, node.get_attribute_by_type(AttributeType.VENTILATION_LEVEL)
)
+10 -3
View File
@@ -7,6 +7,7 @@ from typing import Any
from pyHomee.model import HomeeNode
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
@@ -15,8 +16,14 @@ _LOGGER = logging.getLogger(__name__)
async def setup_homee_platform(
hass: HomeAssistant,
add_platform_entities: Callable[
[HomeeConfigEntry, AddConfigEntryEntitiesCallback, list[HomeeNode]],
[
HomeAssistant,
HomeeConfigEntry,
AddConfigEntryEntitiesCallback,
list[HomeeNode],
],
Coroutine[Any, Any, None],
],
async_add_entities: AddConfigEntryEntitiesCallback,
@@ -24,13 +31,13 @@ async def setup_homee_platform(
) -> None:
"""Set up a homee platform."""
await add_platform_entities(
config_entry, async_add_entities, config_entry.runtime_data.nodes
hass, config_entry, async_add_entities, config_entry.runtime_data.nodes
)
async def add_device(node: HomeeNode, add: bool) -> None:
"""Dynamically add entities."""
if add:
await add_platform_entities(config_entry, async_add_entities, [node])
await add_platform_entities(hass, config_entry, async_add_entities, [node])
config_entry.async_on_unload(
config_entry.runtime_data.add_nodes_listener(add_device)
+7 -3
View File
@@ -87,13 +87,14 @@ def decimal_to_rgb_list(color: float) -> list[int]:
async def add_light_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee light entities."""
async_add_entities(
HomeeLight(node, light, config_entry)
HomeeLight(hass, node, light, config_entry)
for node in nodes
for light in get_light_attribute_sets(node)
if is_light_node(node)
@@ -107,7 +108,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the light entity."""
await setup_homee_platform(add_light_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_light_entities, async_add_entities, config_entry
)
class HomeeLight(HomeeNodeEntity, LightEntity):
@@ -115,12 +118,13 @@ class HomeeLight(HomeeNodeEntity, LightEntity):
def __init__(
self,
hass: HomeAssistant,
node: HomeeNode,
light: dict[AttributeType, HomeeAttribute],
entry: HomeeConfigEntry,
) -> None:
"""Initialize a Homee light."""
super().__init__(node, entry)
super().__init__(hass, node, entry)
self._on_off_attr: HomeeAttribute = light[AttributeType.ON_OFF]
self._dimmer_attr: HomeeAttribute | None = light.get(
+9 -4
View File
@@ -35,13 +35,14 @@ def _determine_lock_state_open(attribute: HomeeAttribute) -> float | None:
async def add_lock_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee lock entities."""
async_add_entities(
HomeeLock(attribute, config_entry)
HomeeLock(hass, attribute, config_entry)
for node in nodes
for attribute in node.attributes
if (attribute.type == AttributeType.LOCK_STATE and attribute.editable)
@@ -55,7 +56,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the lock component."""
await setup_homee_platform(add_lock_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_lock_entities, async_add_entities, config_entry
)
class HomeeLock(HomeeEntity, LockEntity):
@@ -63,9 +66,11 @@ class HomeeLock(HomeeEntity, LockEntity):
_attr_name = None
def __init__(self, attribute: HomeeAttribute, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, attribute: HomeeAttribute, entry: HomeeConfigEntry
) -> None:
"""Initialize the homee lock."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self._lock_state_open = _determine_lock_state_open(attribute)
if self._lock_state_open is not None:
self._attr_supported_features = LockEntityFeature.OPEN
+7 -3
View File
@@ -139,13 +139,14 @@ NUMBER_DESCRIPTIONS = {
async def add_number_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee number entities."""
async_add_entities(
HomeeNumber(attribute, config_entry, NUMBER_DESCRIPTIONS[attribute.type])
HomeeNumber(hass, attribute, config_entry, NUMBER_DESCRIPTIONS[attribute.type])
for node in nodes
for attribute in node.attributes
if attribute.type in NUMBER_DESCRIPTIONS and attribute.data != "fixed_value"
@@ -159,7 +160,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the number component."""
await setup_homee_platform(add_number_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_number_entities, async_add_entities, config_entry
)
class HomeeNumber(HomeeEntity, NumberEntity):
@@ -169,12 +172,13 @@ class HomeeNumber(HomeeEntity, NumberEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeNumberEntityDescription,
) -> None:
"""Initialize a Homee number entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
self._attr_native_unit_of_measurement = (
+7 -3
View File
@@ -31,13 +31,14 @@ SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
async def add_select_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee select entities."""
async_add_entities(
HomeeSelect(attribute, config_entry, SELECT_DESCRIPTIONS[attribute.type])
HomeeSelect(hass, attribute, config_entry, SELECT_DESCRIPTIONS[attribute.type])
for node in nodes
for attribute in node.attributes
if attribute.type in SELECT_DESCRIPTIONS and attribute.editable
@@ -51,7 +52,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the select component."""
await setup_homee_platform(add_select_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_select_entities, async_add_entities, config_entry
)
class HomeeSelect(HomeeEntity, SelectEntity):
@@ -59,12 +62,13 @@ class HomeeSelect(HomeeEntity, SelectEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: SelectEntityDescription,
) -> None:
"""Initialize a Homee select entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
assert description.options is not None
self._attr_options = description.options
+10 -5
View File
@@ -290,6 +290,7 @@ async def async_setup_entry(
"""Add the homee platform for the sensor components."""
async def add_sensor_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
@@ -300,14 +301,14 @@ async def async_setup_entry(
for node in nodes:
# Node properties that are sensors.
entities.extend(
HomeeNodeSensor(node, config_entry, description)
HomeeNodeSensor(hass, node, config_entry, description)
for description in NODE_SENSOR_DESCRIPTIONS
)
# Node attributes that are sensors.
entities.extend(
HomeeSensor(
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
hass, attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
)
for attribute in node.attributes
if attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable
@@ -316,7 +317,9 @@ async def async_setup_entry(
if entities:
async_add_entities(entities)
await setup_homee_platform(add_sensor_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_sensor_entities, async_add_entities, config_entry
)
class HomeeSensor(HomeeEntity, SensorEntity):
@@ -326,12 +329,13 @@ class HomeeSensor(HomeeEntity, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeSensorEntityDescription,
) -> None:
"""Initialize a homee sensor entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
if attribute.instance > 0:
@@ -363,12 +367,13 @@ class HomeeNodeSensor(HomeeNodeEntity, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
node: HomeeNode,
entry: HomeeConfigEntry,
description: HomeeNodeSensorEntityDescription,
) -> None:
"""Initialize a homee node sensor entity."""
super().__init__(node, entry)
super().__init__(hass, node, entry)
self.entity_description = description
self._node = node
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
+5 -2
View File
@@ -17,13 +17,14 @@ PARALLEL_UPDATES = 0
async def add_siren_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee siren entities."""
async_add_entities(
HomeeSiren(attribute, config_entry)
HomeeSiren(hass, attribute, config_entry)
for node in nodes
for attribute in node.attributes
if attribute.type == AttributeType.SIREN
@@ -37,7 +38,9 @@ async def async_setup_entry(
) -> None:
"""Add siren entities for homee."""
await setup_homee_platform(add_siren_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_siren_entities, async_add_entities, config_entry
)
class HomeeSiren(HomeeEntity, SirenEntity):
+23 -11
View File
@@ -16,6 +16,7 @@ from homeassistant.components.switch import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -70,13 +71,14 @@ SWITCH_DESCRIPTIONS: dict[AttributeType, HomeeSwitchEntityDescription] = {
async def add_switch_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee switch entities."""
async_add_entities(
HomeeSwitch(attribute, config_entry, SWITCH_DESCRIPTIONS[attribute.type])
HomeeSwitch(hass, attribute, config_entry, SWITCH_DESCRIPTIONS[attribute.type])
for node in nodes
for attribute in node.attributes
if (attribute.type in SWITCH_DESCRIPTIONS and attribute.editable)
@@ -97,9 +99,11 @@ async def async_setup_entry(
) -> None:
"""Set up the switch platform for the Homee component."""
await setup_homee_platform(add_switch_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_switch_entities, async_add_entities, config_entry
)
async_add_entities(
HomeegramSwitch(homeegram, config_entry)
HomeegramSwitch(hass, homeegram, config_entry)
for homeegram in config_entry.runtime_data.homeegrams
)
@@ -111,12 +115,13 @@ class HomeeSwitch(HomeeEntity, SwitchEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeSwitchEntityDescription,
) -> None:
"""Initialize a Homee switch entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
if attribute.instance == 0:
if attribute.type == AttributeType.ON_OFF:
@@ -156,17 +161,13 @@ class HomeegramSwitch(SwitchEntity):
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, homeegram: HomeeGram, entry: HomeeConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, homeegram: HomeeGram, entry: HomeeConfigEntry
) -> None:
"""Initialize a homee Homeegram switch entity."""
self._homeegram = homeegram
self._entry = entry
self._attr_unique_id = f"{entry.unique_id}-hg-{homeegram.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.unique_id}-homeegrams")},
name="Homeegrams",
model="Homeegram Switches",
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._attr_translation_key = "homeegram"
self._host_connected = entry.runtime_data.connected
self._attr_name = homeegram.name
@@ -175,6 +176,17 @@ class HomeegramSwitch(SwitchEntity):
homeegram
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.unique_id}-homeegrams")},
name="Homeegrams",
model="Homeegram Switches",
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, entry.runtime_data.settings.uid),
config_entry_id=entry.entry_id,
),
)
@override
async def async_added_to_hass(self) -> None:
"""Add the Homeegram entity to home assistant."""
+7 -3
View File
@@ -29,13 +29,14 @@ VALVE_DESCRIPTIONS = {
async def add_valve_entities(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
nodes: list[HomeeNode],
) -> None:
"""Add homee valve entities."""
async_add_entities(
HomeeValve(attribute, config_entry, VALVE_DESCRIPTIONS[attribute.type])
HomeeValve(hass, attribute, config_entry, VALVE_DESCRIPTIONS[attribute.type])
for node in nodes
for attribute in node.attributes
if attribute.type in VALVE_DESCRIPTIONS
@@ -49,7 +50,9 @@ async def async_setup_entry(
) -> None:
"""Add the homee platform for the valve component."""
await setup_homee_platform(add_valve_entities, async_add_entities, config_entry)
await setup_homee_platform(
hass, add_valve_entities, async_add_entities, config_entry
)
class HomeeValve(HomeeEntity, ValveEntity):
@@ -59,12 +62,13 @@ class HomeeValve(HomeeEntity, ValveEntity):
def __init__(
self,
hass: HomeAssistant,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: ValveEntityDescription,
) -> None:
"""Initialize a Homee valve entity."""
super().__init__(attribute, entry)
super().__init__(hass, attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
@@ -7,11 +7,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -95,19 +91,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
EVENT_HOMEASSISTANT_STOP, hap.shutdown
)
# Register hap as device in registry.
device_registry = dr.async_get(hass)
home = hap.home
hapname = home.label if home.label != entry.unique_id else f"Home-{home.label}"
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, home.id)},
manufacturer="eQ-3",
# Add the name from config entry.
name=hapname,
)
return True
@@ -1,7 +1,7 @@
"""Support for HomematicIP Cloud alarm control panel."""
import logging
from typing import override
from typing import TYPE_CHECKING, override
from homematicip.functionalHomes import SecurityAndAlarmHome
@@ -11,6 +11,7 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -51,12 +52,18 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity):
@override
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
if TYPE_CHECKING:
assert self.platform.config_entry is not None
return DeviceInfo(
identifiers={(DOMAIN, f"ACP {self._home.id}")},
manufacturer="eQ-3",
model=CONST_ALARM_CONTROL_PANEL_NAME,
name=self.name,
via_device=(DOMAIN, self._home.id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self._home.id),
config_entry_id=self.platform.config_entry.entry_id,
),
)
@property
@@ -1,6 +1,6 @@
"""Support for HomematicIP Cloud climate devices."""
from typing import Any, override
from typing import TYPE_CHECKING, Any, override
from homematicip.base.enums import AbsenceType
from homematicip.device import (
@@ -24,6 +24,7 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -104,12 +105,18 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
@override
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
if TYPE_CHECKING:
assert self.platform.config_entry is not None
return DeviceInfo(
identifiers={(DOMAIN, self._device.id)},
manufacturer="eQ-3",
model=self._device.modelType,
name=self._device.label,
via_device=(DOMAIN, self._device.homeId),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self._device.homeId),
config_entry_id=self.platform.config_entry.entry_id,
),
)
@property
@@ -2,7 +2,7 @@
import contextlib
import logging
from typing import Any, override
from typing import TYPE_CHECKING, Any, override
from homematicip.base.functionalChannels import FunctionalChannel
from homematicip.device import Device
@@ -145,6 +145,8 @@ class HomematicipGenericEntity(Entity):
if device_name and home_name:
device_name = f"{home_name} {device_name}"
if TYPE_CHECKING:
assert self.platform.config_entry is not None
return DeviceInfo(
identifiers={
# Serial numbers of Homematic IP device
@@ -155,7 +157,11 @@ class HomematicipGenericEntity(Entity):
name=device_name,
sw_version=self._device.firmwareVersion,
# Link to the homematic ip access point.
via_device=(DOMAIN, home_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, home_id),
config_entry_id=self.platform.config_entry.entry_id,
),
)
return None
@@ -19,9 +19,17 @@ import homeassistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS
from .const import (
DOMAIN,
HMIPC_AUTHTOKEN,
HMIPC_HAPID,
HMIPC_NAME,
HMIPC_PIN,
PLATFORMS,
)
from .errors import HmipcConnectionError
_LOGGER = logging.getLogger(__name__)
@@ -138,12 +146,32 @@ class HomematicipHAP:
"Connected to HomematicIP with HAP %s", self.config_entry.unique_id
)
# Register the access point as a device before forwarding platforms so
# child devices can resolve it as their via_device.
self._async_register_access_point()
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS
)
return True
@callback
def _async_register_access_point(self) -> None:
"""Register the access point as a device in the device registry."""
home = self.home
hapname = (
home.label
if home.label != self.config_entry.unique_id
else f"Home-{home.label}"
)
dr.async_get(self.hass).async_get_or_create(
config_entry_id=self.config_entry.entry_id,
identifiers={(DOMAIN, home.id)},
manufacturer="eQ-3",
name=hapname,
)
@callback
def async_update(self, *args, **kwargs) -> None:
"""Async update the home device.
@@ -17,6 +17,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from .const import DOMAIN, PLATFORMS
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
from .entity import create_main_device_info
async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -> bool:
@@ -60,6 +61,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
):
hass.config_entries.flow.async_abort(progress_flow["flow_id"])
# Register the main device up front so external sub-device sensors can
# resolve it as their via_device parent regardless of entity add order.
if coordinator.data.device.serial is not None:
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
**create_main_device_info(coordinator),
)
# Finalize
entry.async_on_unload(coordinator.api.close)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+18 -13
View File
@@ -8,6 +8,23 @@ from .const import DOMAIN
from .coordinator import HWEnergyDeviceUpdateCoordinator
def create_main_device_info(
coordinator: HWEnergyDeviceUpdateCoordinator,
) -> DeviceInfo:
"""Return the device info for the main HomeWizard device."""
device_info = DeviceInfo(
manufacturer="HomeWizard",
sw_version=coordinator.data.device.firmware_version,
model_id=coordinator.data.device.product_type,
model=coordinator.data.device.model_name,
)
if (serial_number := coordinator.data.device.serial) is not None:
device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, serial_number)}
device_info[ATTR_IDENTIFIERS] = {(DOMAIN, serial_number)}
device_info[ATTR_SERIAL_NUMBER] = serial_number
return device_info
class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]):
"""Defines a HomeWizard entity."""
@@ -16,16 +33,4 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]):
def __init__(self, coordinator: HWEnergyDeviceUpdateCoordinator) -> None:
"""Initialize the HomeWizard entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
manufacturer="HomeWizard",
sw_version=coordinator.data.device.firmware_version,
model_id=coordinator.data.device.product_type,
model=coordinator.data.device.model_name,
)
if (serial_number := coordinator.data.device.serial) is not None:
self._attr_device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, serial_number)
}
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, serial_number)}
self._attr_device_info[ATTR_SERIAL_NUMBER] = serial_number
self._attr_device_info = create_main_device_info(coordinator)

Some files were not shown because too many files have changed in this diff Show More