Skip to content

Add scheduled events and scene/cover cross-references to PowerView#168474

Open
TheRealPiotrP wants to merge 2 commits intohome-assistant:devfrom
TheRealPiotrP:add-powerview-scheduled-events
Open

Add scheduled events and scene/cover cross-references to PowerView#168474
TheRealPiotrP wants to merge 2 commits intohome-assistant:devfrom
TheRealPiotrP:add-powerview-scheduled-events

Conversation

@TheRealPiotrP
Copy link
Copy Markdown

Proposed change

Adds two new features to the Hunter Douglas PowerView integration:

Scheduled event switches: Exposes each scheduled event (automation) on Gen2 and Gen3 hubs as a switch entity, allowing users to enable or disable individual scheduled events from Home Assistant. Gen2 uses the scheduledevents API endpoint; Gen3 uses the automations endpoint — both handled transparently by aiopvapi's Automations class.

Scene/cover cross-references: Cover entities gain a scene_entity_ids attribute listing the scenes they belong to, and scene entities gain shade_ids / shade_entity_ids attributes listing the covers they control. Scene entities also gain scheduled_event_ids / scheduled_event_entity_ids attributes cross-referencing their corresponding switch entities. Entity IDs are resolved dynamically from the entity registry so they survive entity renames.

Gen2 uses the SceneMembers API endpoint to build shade↔scene mappings. Gen3 embeds shadeIds directly in scene data, so no extra API call is needed.

Type of change

  • New feature (which adds functionality to an existing integration)

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Cover entities expose which scenes they belong to, and scene entities
expose which covers they control, with resolved HA entity IDs in both
directions.

Gen2 uses the SceneMembers API endpoint to build the mappings. Gen3
embeds shadeIds directly in scene data, so no additional API call is
needed.
Add a Switch platform for Gen2 and Gen3 hubs that exposes each
scheduled event (automation) as a switch entity. Switches can be
toggled to enable or disable individual scheduled events. Scene
entities gain scheduled_event_ids and scheduled_event_entity_ids
attributes that cross-reference the corresponding switch entities.

Gen2 uses the scheduledevents API endpoint; Gen3 uses the automations
endpoint. Both are handled transparently by the aiopvapi Automations
class.
@home-assistant
Copy link
Copy Markdown

Hey there @bdraco, @kingy444, @trullock, mind taking a look at this pull request as it has been labeled with an integration (hunterdouglas_powerview) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of hunterdouglas_powerview can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign hunterdouglas_powerview Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds scheduled-event (automation) switch entities and bidirectional scene↔cover↔schedule cross-reference attributes to the Hunter Douglas PowerView integration to support driving native hub scheduling from Home Assistant.

Changes:

  • Expose PowerView automations/scheduled events as switch entities (Gen2/Gen3).
  • Add cross-reference state attributes on scene and cover entities (plus scene references on schedule switches), resolving HA entity_ids via the entity registry.
  • Extend test fixtures and add new tests validating entity creation, switch toggling, and cross-reference attributes across API versions.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
homeassistant/components/hunterdouglas_powerview/init.py Fetch automations + scene members (v2), build shade↔scene and scene↔automation mappings, and include switch platform.
homeassistant/components/hunterdouglas_powerview/model.py Extend runtime data model with automation data and mapping dictionaries.
homeassistant/components/hunterdouglas_powerview/switch.py New schedule switch platform exposing enable/disable and scene linkage attributes.
homeassistant/components/hunterdouglas_powerview/scene.py Add scene attributes for shade + scheduled event cross-references, with entity-registry resolution.
homeassistant/components/hunterdouglas_powerview/cover.py Add cover attributes for scene cross-references and initialize mappings on add.
tests/components/hunterdouglas_powerview/conftest.py Patch new automation + scene member API fetches and add fixtures for new JSON payloads.
tests/components/hunterdouglas_powerview/test_schedule.py New tests for schedule switch creation, state, attributes, toggling, and scene references.
tests/components/hunterdouglas_powerview/test_scene.py New tests for scene shade/scheduled-event cross-reference attributes across API versions.
tests/components/hunterdouglas_powerview/test_cover.py New tests for cover scene cross-reference attributes across API versions.

Comment on lines +95 to +117
self._attr_extra_state_attributes = {
STATE_ATTRIBUTE_ROOM_NAME: room_name,
"scene_name": scene_name,
"scene_id": automation.scene_id,
"scene_entity_id": None,
"execution_time": automation.get_execution_time(),
"execution_days": automation.get_execution_days(),
}

async def async_added_to_hass(self) -> None:
"""Resolve scene PowerView ID to HA entity ID once registered."""
await super().async_added_to_hass()
entity_registry = er.async_get(self.hass)
serial = self._device_info.serial_number
entity_id = entity_registry.async_get_entity_id(
Platform.SCENE, DOMAIN, f"{serial}_{self._automation.scene_id}"
)
if entity_id:
self._attr_extra_state_attributes = {
**self._attr_extra_state_attributes,
"scene_entity_id": entity_id,
}
self.async_write_ha_state()
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Resolve scene_entity_id dynamically (or listen for entity-registry updates) so the attribute stays correct after renames and isn’t missed due to platform setup ordering; resolving once in async_added_to_hass can leave it permanently None or stale.

Suggested change
self._attr_extra_state_attributes = {
STATE_ATTRIBUTE_ROOM_NAME: room_name,
"scene_name": scene_name,
"scene_id": automation.scene_id,
"scene_entity_id": None,
"execution_time": automation.get_execution_time(),
"execution_days": automation.get_execution_days(),
}
async def async_added_to_hass(self) -> None:
"""Resolve scene PowerView ID to HA entity ID once registered."""
await super().async_added_to_hass()
entity_registry = er.async_get(self.hass)
serial = self._device_info.serial_number
entity_id = entity_registry.async_get_entity_id(
Platform.SCENE, DOMAIN, f"{serial}_{self._automation.scene_id}"
)
if entity_id:
self._attr_extra_state_attributes = {
**self._attr_extra_state_attributes,
"scene_entity_id": entity_id,
}
self.async_write_ha_state()
self._extra_state_attributes = {
STATE_ATTRIBUTE_ROOM_NAME: room_name,
"scene_name": scene_name,
"scene_id": automation.scene_id,
"execution_time": automation.get_execution_time(),
"execution_days": automation.get_execution_days(),
}
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return schedule attributes with the current scene entity ID."""
entity_registry = er.async_get(self.hass)
serial = self._device_info.serial_number
entity_id = entity_registry.async_get_entity_id(
Platform.SCENE, DOMAIN, f"{serial}_{self._automation.scene_id}"
)
return {
**self._extra_state_attributes,
"scene_entity_id": entity_id,
}

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +79
entity_registry = er.async_get(self.hass)
config_entry_id = self.coordinator.config_entry.entry_id
all_entries = er.async_entries_for_config_entry(
entity_registry, config_entry_id
)
serial = self._device_info.serial_number
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Avoid recomputing cross-reference attributes by scanning the entire entity registry on every coordinator-driven state write (this can run every minute for every scene); cache the resolved entity_ids and refresh them via entity-registry update signals, or use targeted async_get_entity_id lookups for known unique_ids.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +107
shade_entity_ids: list[str] = []
for shade_id in self._shade_ids:
prefix = f"{serial}_{shade_id}"
shade_entity_ids.extend(
e.entity_id
for e in all_entries
if e.domain == Platform.COVER
and (e.unique_id == prefix or e.unique_id.startswith(f"{prefix}_"))
)

automation_entity_ids = [
entity_id
for automation_id in self._automation_ids
if (
entity_id := entity_registry.async_get_entity_id(
Platform.SWITCH, DOMAIN, f"{serial}_{automation_id}"
)
)
]

return {
STATE_ATTRIBUTE_ROOM_NAME: self._room_name,
"shade_ids": self._shade_ids,
"shade_entity_ids": shade_entity_ids,
"scheduled_event_ids": self._automation_ids,
"scheduled_event_entity_ids": automation_entity_ids,
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Sort and de-duplicate the computed ID lists before exposing them as state attributes to keep attribute ordering stable and avoid unnecessary state-change churn when API/registry ordering varies.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +133
scene_to_shade_ids: dict[int, list[int]] = {}
shade_to_scene_ids: dict[int, list[int]] = {}
if scene_member_data:
for member in scene_member_data.raw:
s_id = member["sceneId"]
sh_id = member["shadeId"]
scene_to_shade_ids.setdefault(s_id, []).append(sh_id)
shade_to_scene_ids.setdefault(sh_id, []).append(s_id)
elif hub.api_version >= 3:
# Gen3 embeds shadeIds directly in scene data; no separate API call needed
for scene in scene_data.processed.values():
for sh_id in scene.raw_data.get("shadeIds", []):
scene_to_shade_ids.setdefault(scene.id, []).append(sh_id)
shade_to_scene_ids.setdefault(sh_id, []).append(scene.id)

scene_to_automation_ids: dict[int, list[int]] = {}
if automation_data:
for automation in automation_data.processed.values():
if automation.scene_id is not None:
scene_to_automation_ids.setdefault(automation.scene_id, []).append(
automation.id
)

Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

De-duplicate (and ideally sort) the shade/scene/automation ID mappings when building them so downstream entity attributes remain deterministic and don’t accumulate duplicates if the upstream API returns repeated entries.

Suggested change
scene_to_shade_ids: dict[int, list[int]] = {}
shade_to_scene_ids: dict[int, list[int]] = {}
if scene_member_data:
for member in scene_member_data.raw:
s_id = member["sceneId"]
sh_id = member["shadeId"]
scene_to_shade_ids.setdefault(s_id, []).append(sh_id)
shade_to_scene_ids.setdefault(sh_id, []).append(s_id)
elif hub.api_version >= 3:
# Gen3 embeds shadeIds directly in scene data; no separate API call needed
for scene in scene_data.processed.values():
for sh_id in scene.raw_data.get("shadeIds", []):
scene_to_shade_ids.setdefault(scene.id, []).append(sh_id)
shade_to_scene_ids.setdefault(sh_id, []).append(scene.id)
scene_to_automation_ids: dict[int, list[int]] = {}
if automation_data:
for automation in automation_data.processed.values():
if automation.scene_id is not None:
scene_to_automation_ids.setdefault(automation.scene_id, []).append(
automation.id
)
scene_to_shade_ids_map: dict[int, set[int]] = {}
shade_to_scene_ids_map: dict[int, set[int]] = {}
if scene_member_data:
for member in scene_member_data.raw:
s_id = member["sceneId"]
sh_id = member["shadeId"]
scene_to_shade_ids_map.setdefault(s_id, set()).add(sh_id)
shade_to_scene_ids_map.setdefault(sh_id, set()).add(s_id)
elif hub.api_version >= 3:
# Gen3 embeds shadeIds directly in scene data; no separate API call needed
for scene in scene_data.processed.values():
for sh_id in scene.raw_data.get("shadeIds", []):
scene_to_shade_ids_map.setdefault(scene.id, set()).add(sh_id)
shade_to_scene_ids_map.setdefault(sh_id, set()).add(scene.id)
scene_to_automation_ids_map: dict[int, set[int]] = {}
if automation_data:
for automation in automation_data.processed.values():
if automation.scene_id is not None:
scene_to_automation_ids_map.setdefault(
automation.scene_id, set()
).add(automation.id)
scene_to_shade_ids = {
scene_id: sorted(shade_ids)
for scene_id, shade_ids in sorted(scene_to_shade_ids_map.items())
}
shade_to_scene_ids = {
shade_id: sorted(scene_ids)
for shade_id, scene_ids in sorted(shade_to_scene_ids_map.items())
}
scene_to_automation_ids = {
scene_id: sorted(automation_ids)
for scene_id, automation_ids in sorted(scene_to_automation_ids_map.items())
}

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +158
scene_entity_ids: list[str] = []
if self._scene_ids:
entity_registry = er.async_get(self.hass)
serial = self._device_info.serial_number
for scene_id in self._scene_ids:
entity_id = entity_registry.async_get_entity_id(
Platform.SCENE, DOMAIN, f"{serial}_{scene_id}"
)
if entity_id:
scene_entity_ids.append(entity_id)
return {
STATE_ATTRIBUTE_ROOM_NAME: self._room_name,
"scene_ids": self._scene_ids,
"scene_entity_ids": scene_entity_ids,
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Sort (and optionally de-duplicate) scene_ids / scene_entity_ids before returning them so attribute ordering is stable and doesn’t cause unnecessary state changes when the upstream mapping order varies.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hunterdouglas_powerview: expose scheduled events as switches and add cross-platform entity references

3 participants