Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/entity naming #50

Merged
merged 5 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,56 @@
"postCreateCommand": ".devcontainer/setup",
"postAttachCommand": ".devcontainer/setup",
"forwardPorts": [8123],
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
"containerEnv": {
"PYTHONASYNCIODEBUG": "1"
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.black-formatter",
"ms-python.isort",
"charliermarsh.ruff",
"ms-python.pylint",
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters"
"GitHub.vscode-pull-request-github",
"GitHub.copilot"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": false,
"python.formatting.provider": "black",
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"],
"pylint.importStrategy": "fromEnvironment",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
"files.trimTrailingWhitespace": true,
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
}
},
"files.trimTrailingWhitespace": true
"terminal.integrated.defaultProfile.linux": "zsh",
"yaml.customTags": [
"!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",
"!include_dir_merge_named scalar"
],
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
}
},
"remoteUser": "vscode",
"features": {
"rust": "latest"
"rust": "latest",
"ghcr.io/devcontainers/features/github-cli:1": {}
}
}
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"charliermarsh.ruff",
"esbenp.prettier-vscode",
"ms-python.python"
]
}
30 changes: 15 additions & 15 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Home Assistant",
"type": "python",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": ["--debug", "-c", "config"]
}
]
}
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Home Assistant",
"type": "debugpy",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": ["--debug", "-c", "config"]
}
]
}
5 changes: 5 additions & 0 deletions .vscode/settings.default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"python.testing.pytestArgs": ["--no-cov"],
"python.testing.pytestEnabled": false,
"pylint.importStrategy": "fromEnvironment"
}
66 changes: 64 additions & 2 deletions custom_components/unfoldedcircle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""The Unfolded Circle Remote integration."""

from __future__ import annotations

from typing import Any
import logging

from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from pyUnfoldedCircleRemote.remote import AuthenticationError, Remote

Expand Down Expand Up @@ -55,6 +57,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Get Basic Device Information
await coordinator.async_config_entry_first_refresh()

@callback
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
"""Migrate Unfolded Circle entity entries.

- Migrates old unique ID's to the new unique ID's
"""
if (
entry.domain != Platform.UPDATE
and "ucr" not in entry.unique_id.lower()
and "ucd" not in entry.unique_id.lower()
and (entry.domain == Platform.SWITCH and "uc.main" not in entry.unique_id)
):
new = f"{coordinator.api.model_number}_{entry.unique_id}"
return {"new_unique_id": entry.unique_id.replace(entry.unique_id, new)}

if (
entry.domain == Platform.UPDATE
and "ucr" not in entry.unique_id.lower()
and "ucd" not in entry.unique_id.lower()
):
new = f"{coordinator.api.model_number}_{coordinator.api.serial_number}_update_status"
return {"new_unique_id": entry.unique_id.replace(entry.unique_id, new)}

# No migration needed
return None

# Migrate unique ID -- Make the ID actually Unique.
# Migrate Device Name -- Make the device name match the psn username
# We can remove this logic after a reasonable period of time has passed.
if entry.version == 1:
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
_migrate_device_identifiers(hass, entry.entry_id, coordinator)
hass.config_entries.async_update_entry(entry, version=2)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
await zeroconf.async_get_async_instance(hass)
Expand Down Expand Up @@ -83,3 +119,29 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
# await async_unload_entry(hass, entry)
# await async_setup_entry(hass, entry)
await hass.config_entries.async_reload(entry.entry_id)


async def async_migrate_entry(hass: HomeAssistant, self):
"""Migrate Entry Support"""
return True


def _migrate_device_identifiers(
hass: HomeAssistant, entry_id: str, coordinator
) -> None:
"""Migrate old device identifiers."""
dev_reg = dr.async_get(hass)
devices: list[dr.DeviceEntry] = dr.async_entries_for_config_entry(dev_reg, entry_id)
for device in devices:
old_identifier = list(next(iter(device.identifiers)))
if (
"ucr" not in old_identifier[1].lower()
and "ucd" not in old_identifier[1].lower()
):
new_identifier = {
(DOMAIN, coordinator.api.model_number, coordinator.api.serial_number)
}
_LOGGER.debug(
"migrate identifier '%s' to '%s'", device.identifiers, new_identifier
)
dev_reg.async_update_device(device.id, new_identifiers=new_identifier)
11 changes: 6 additions & 5 deletions custom_components/unfoldedcircle/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ def __init__(self, coordinator) -> None:
super().__init__(coordinator)

# As per the sensor, this must be a unique value within this domain.
self._attr_unique_id = f"{self.coordinator.api.serial_number}_polling_status"
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_polling_status"

# The name of the entity
self._attr_name = f"{self.coordinator.api.name} Polling Status"
self._attr_has_entity_name = True
self._attr_name = "Polling Status"
self._attr_native_value = self.coordinator.polling_data
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._extra_state_attributes = {}
Expand Down Expand Up @@ -82,9 +83,9 @@ async def async_added_to_hass(self) -> None:
def __init__(self, coordinator) -> None:
"""Initialize Binary Sensor."""
super().__init__(coordinator)

self._attr_unique_id = f"{self.coordinator.api.serial_number}_charging_status"
self._attr_name = f"{self.coordinator.api.name} Charging Status"
self._attr_has_entity_name = True
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_charging_status"
self._attr_name = "Charging Status"
self._attr_native_value = False

@property
Expand Down
12 changes: 6 additions & 6 deletions custom_components/unfoldedcircle/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ class RebootButton(UnfoldedCircleEntity, ButtonEntity):
def __init__(self, coordinator) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{self.coordinator.api.serial_number}_restart_button"
self._attr_name = f"{self.coordinator.api.name} Restart Remote"
self._attr_has_entity_name = True
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_restart_button"
self._attr_name = "Restart Remote"

@property
def available(self) -> bool:
Expand All @@ -53,10 +54,9 @@ class UpdateCheckButton(UnfoldedCircleEntity, ButtonEntity):
def __init__(self, coordinator) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = (
f"{self.coordinator.api.serial_number}_update_check_button"
)
self._attr_name = f"{self.coordinator.api.name} Check for Update"
self._attr_has_entity_name = True
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_update_check_button"
self._attr_name = "Check for Update"

@property
def available(self) -> bool:
Expand Down
6 changes: 5 additions & 1 deletion custom_components/unfoldedcircle/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, self.coordinator.api.serial_number)
(
DOMAIN,
self.coordinator.api.model_number,
self.coordinator.api.serial_number,
)
},
name=self.coordinator.api.name,
manufacturer=self.coordinator.api.manufacturer,
Expand Down
21 changes: 7 additions & 14 deletions custom_components/unfoldedcircle/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,20 @@ def __init__(
) -> None:
"""Initialize a switch."""
super().__init__(coordinator)
self._attr_has_entity_name = True
self.activity_group = activity_group
self.activity = activity
if activity_group is None and activity is None:
self._attr_name = f"{self.coordinator.api.name} Media Player"
self._attr_unique_id = f"{self.coordinator.api.serial_number}_mediaplayer"
self._attr_name = "Media Player"
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_mediaplayer"
self.activities = self.coordinator.api.activities
elif activity is not None:
self._attr_name = (
f"{self.coordinator.api.name} {activity.name} Media Player"
)
self._attr_unique_id = (
f"{self.coordinator.api.serial_number}_{activity.name}_mediaplayer"
)
self._attr_name = f"{activity.name} Media Player"
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_{activity.name}_mediaplayer"
self.activities = [activity]
elif activity_group is not None:
self._attr_name = (
f"{self.coordinator.api.name} {activity_group.name} Media Player"
)
self._attr_unique_id = (
f"{self.coordinator.api.serial_number}_{activity_group.id}_mediaplayer"
)
self._attr_name = f"{activity_group.name} Media Player"
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_{activity_group.id}_mediaplayer"
self.activities = self.activity_group.activities
self._extra_state_attributes = {}
self._current_activity = None
Expand Down
7 changes: 3 additions & 4 deletions custom_components/unfoldedcircle/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,9 @@ def __init__(
self._description = description
self.coordinator = coordinator
self.entity_description = description
self._attr_unique_id = (
f"{self.coordinator.api.serial_number}_{description.unique_id}"
)
self._attr_name = f"{self.coordinator.api.name} {description.name}"
self._attr_has_entity_name = True
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_{description.unique_id}"
self._attr_name = f"{description.name}"
key = "_" + description.key
self._attr_native_value = coordinator.data.get(key)
self._attr_icon = description.icon
Expand Down
6 changes: 3 additions & 3 deletions custom_components/unfoldedcircle/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ class RemoteSensor(UnfoldedCircleEntity, RemoteEntity):
def __init__(self, coordinator) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{self.coordinator.api.serial_number}_remote"
self._attr_name = f"{self.coordinator.api.name} Remote"
self._attr_has_entity_name = True
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_remote"
self._attr_name = "Remote"
self._attr_activity_list = []
self._extra_state_attributes = {}
self._attr_is_on = False

for activity in self.coordinator.api.activities:
self._attr_activity_list.append(activity.name)
# self.update_state()

@property
def is_on(self) -> bool | None:
Expand Down
7 changes: 3 additions & 4 deletions custom_components/unfoldedcircle/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ def __init__(self, coordinator, activity_group) -> None:
"""Initialize a switch."""
super().__init__(coordinator)
self.activity_group = activity_group
self._attr_name = f"{self.coordinator.api.name} {activity_group.name}"
self._attr_unique_id = (
f"{self.coordinator.api.serial_number}_{activity_group._id}"
)
self._attr_has_entity_name = True
self._attr_name = f"{activity_group.name}"
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_{activity_group._id}"
self._state = activity_group.state
self._attr_icon = "mdi:remote-tv"
self._attr_native_value = "OFF"
Expand Down
12 changes: 3 additions & 9 deletions custom_components/unfoldedcircle/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,9 @@ def __init__(
) -> None:
"""Initialize Unfolded Circle Sensor."""
super().__init__(coordinator)
self._attr_unique_id = (
f"{self.coordinator.api.serial_number}_{description.unique_id}"
)
self._attr_name = f"{self.coordinator.api.name} {description.name}"
self._attr_unique_id = f"{coordinator.api.model_number}_{self.coordinator.api.serial_number}_{description.unique_id}"
self._attr_has_entity_name = True
self._attr_name = f"{description.name}"
self._attr_unit_of_measurement = description.unit_of_measurement
self._attr_native_unit_of_measurement = description.unit_of_measurement
self._device_class = description.device_class
Expand Down Expand Up @@ -157,8 +156,3 @@ def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_native_value = self.get_value()
self.async_write_ha_state()

@property
def native_value(self) -> StateType:
"""Return native value for entity."""
return self.get_value()
Loading