Skip to content

Commit

Permalink
Integration name and Device name are synchronized.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbkarlsson authored Mar 12, 2023
1 parent a196852 commit bdf0d42
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 49 deletions.
51 changes: 28 additions & 23 deletions custom_components/ev_smart_charging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@
import asyncio
import logging

from homeassistant.config_entries import (
ConfigEntry,
ConfigEntryChange,
SIGNAL_CONFIG_ENTRY_CHANGED,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send

from custom_components.ev_smart_charging.helpers.general import get_parameter
from homeassistant.helpers.device_registry import async_get as async_device_registry_get
from homeassistant.helpers.device_registry import DeviceRegistry, DeviceEntry
from homeassistant.helpers.entity_registry import async_get as async_entity_registry_get
from homeassistant.helpers.entity_registry import (
EntityRegistry,
async_entries_for_config_entry,
)

from .coordinator import EVSmartChargingCoordinator
from .const import (
CONF_DEVICE_NAME,
DOMAIN,
STARTUP_MESSAGE,
PLATFORMS,
Expand All @@ -39,20 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data.setdefault(DOMAIN, {})
_LOGGER.debug(STARTUP_MESSAGE)

# Make sure the Integration name is the same as the Device name
# This is currently needed since
# homeassistant.config_entries.OptionsFlowManager.async_finish_flow()
# does not pass "title" to self.hass.config_entries.async_update_entry()
# Don't bother to test code copied from async_update_entry()
if entry.title != get_parameter(entry, CONF_DEVICE_NAME):
entry.title = get_parameter(entry, CONF_DEVICE_NAME)
for listener_ref in entry.update_listeners:
if (listener := listener_ref()) is not None: # pragma: no cover
hass.async_create_task(listener(hass, entry)) # pragma: no cover
async_dispatcher_send(
hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, entry
)

coordinator = EVSmartChargingCoordinator(hass, entry)
validation_error = coordinator.validate_input_sensors()
if validation_error is not None:
Expand All @@ -72,6 +57,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

entry.async_on_unload(entry.add_update_listener(async_reload_entry))

# If the name of the integration (config_entry.title) has changed,
# update the device name.
entity_registry: EntityRegistry = async_entity_registry_get(hass)
all_entities = async_entries_for_config_entry(entity_registry, entry.entry_id)
if all_entities:
device_id = all_entities[0].device_id
device_registry: DeviceRegistry = async_device_registry_get(hass)
device: DeviceEntry = device_registry.async_get(device_id)
if device:
if device.name_by_user is not None:
if entry.title != device.name_by_user:
device_registry.async_update_device(
device.id, name_by_user=entry.title
)
else:
if entry.title != device.name:
device_registry.async_update_device(
device.id, name_by_user=entry.title
)

return True


Expand Down
7 changes: 1 addition & 6 deletions custom_components/ev_smart_charging/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,11 @@ async def async_step_init(self, user_input) -> FlowResult:
self._errors[error[0]] = error[1]

if not self._errors:
# Setting title here doesn't seem to have any effect.
return self.async_create_entry(
title=user_input[CONF_DEVICE_NAME], data=user_input
title=self.config_entry.title, data=user_input
)

user_schema = {
vol.Required(
CONF_DEVICE_NAME,
default=get_parameter(self.config_entry, CONF_DEVICE_NAME),
): cv.string,
vol.Required(
CONF_PRICE_SENSOR,
default=get_parameter(self.config_entry, CONF_PRICE_SENSOR),
Expand Down
37 changes: 35 additions & 2 deletions custom_components/ev_smart_charging/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

from datetime import datetime
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import (
ConfigEntry,
)
from homeassistant.const import SERVICE_TURN_ON, SERVICE_TURN_OFF
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.core import HomeAssistant, State, callback, Event
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.device_registry import async_get as async_device_registry_get
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.event import (
async_track_state_change,
async_track_time_change,
Expand Down Expand Up @@ -137,6 +142,34 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
self.listeners.append(
async_track_time_change(hass, self.update_hourly, minute=0, second=0)
)
# Listen for changes to the device.
self.listeners.append(
hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, self.device_updated)
)

@callback
async def device_updated(self, event: Event): # pylint: disable=unused-argument
"""Called when device is updated"""
_LOGGER.debug("EVSmartChargingCoordinator.device_updated()")
if "device_id" in event.data:
entity_registry: EntityRegistry = async_entity_registry_get(self.hass)
all_entities = async_entries_for_config_entry(
entity_registry, self.config_entry.entry_id
)
if all_entities:
device_id = all_entities[0].device_id
if event.data["device_id"] == device_id:
if "changes" in event.data:
if "name_by_user" in event.data["changes"]:
# If the device name is changed, update the integration name
device_registry: DeviceRegistry = async_device_registry_get(
self.hass
)
device = device_registry.async_get(device_id)
if device.name_by_user != self.config_entry.title:
self.hass.config_entries.async_update_entry(
self.config_entry, title=device.name_by_user
)

@callback
async def update_hourly(
Expand Down
6 changes: 2 additions & 4 deletions custom_components/ev_smart_charging/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
import logging
from homeassistant.helpers.entity import Entity

from custom_components.ev_smart_charging.helpers.general import get_parameter

from .const import CONF_DEVICE_NAME, DOMAIN, ICON, NAME, VERSION
from .const import DOMAIN, ICON, NAME, VERSION

_LOGGER = logging.getLogger(__name__)

Expand All @@ -27,7 +25,7 @@ def update_ha_state(self):
def device_info(self):
return {
"identifiers": {(DOMAIN, self.config_entry.entry_id)},
"name": get_parameter(self.config_entry, CONF_DEVICE_NAME),
"name": self.config_entry.title,
"model": VERSION,
"manufacturer": NAME,
}
1 change: 0 additions & 1 deletion custom_components/ev_smart_charging/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"init": {
"description": "Configuration of name and external entities.",
"data": {
"device_name": "Name",
"price_sensor": "Electricity price sensor",
"ev_soc_sensor": "EV SOC entity",
"ev_target_soc_sensor": "EV Target SOC entity (single space to remove)",
Expand Down
68 changes: 68 additions & 0 deletions tests/coordinator/test_coordinator_device_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Test ev_smart_charging coordinator."""

from pytest_homeassistant_custom_component.common import MockConfigEntry

from homeassistant.helpers.device_registry import async_get as async_device_registry_get
from homeassistant.helpers.device_registry import DeviceRegistry, DeviceEntry
from homeassistant.helpers.entity_registry import async_get as async_entity_registry_get
from homeassistant.helpers.entity_registry import (
EntityRegistry,
async_entries_for_config_entry,
)

from custom_components.ev_smart_charging import (
async_reload_entry,
async_setup_entry,
async_unload_entry,
)
from custom_components.ev_smart_charging.coordinator import (
EVSmartChargingCoordinator,
)
from custom_components.ev_smart_charging.const import DOMAIN

from tests.const import MOCK_CONFIG_ALL


# pylint: disable=unused-argument
async def test_coordinator_device_name(hass, bypass_validate_input_sensors):
"""Test entry setup with new integration name."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ALL, entry_id="test")

# Set up the entry and assert that the values set during setup are where we expect
# them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data
# call, no code from custom_components/integration_blueprint/api.py actually runs.
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

# Change device title
entity_registry: EntityRegistry = async_entity_registry_get(hass)
all_entities = async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
device_id = all_entities[0].device_id
device_registry: DeviceRegistry = async_device_registry_get(hass)
device: DeviceEntry = device_registry.async_get(device_id)
device_registry.async_update_device(device.id, name_by_user="New title")
await hass.async_block_till_done()

assert config_entry.title == "New title"

# Reload the entry and assert that the data from above is still there
assert await async_reload_entry(hass, config_entry) is None
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

assert config_entry.title == "New title"

# Unload the entry and verify that the data has been removed
assert await async_unload_entry(hass, config_entry)
await hass.async_block_till_done()
assert config_entry.entry_id not in hass.data[DOMAIN]
3 changes: 2 additions & 1 deletion tests/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
# Assertions allow you to verify that the return value of whatever is on the left
# side of the assertion matches with the right side.


# pylint: disable=unused-argument
async def test_button(hass, bypass_validate_input_sensors):
"""Test buttons."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONFIG_USER_NO_CHARGER, entry_id="test"
domain=DOMAIN, data=MOCK_CONFIG_USER_NO_CHARGER, entry_id="test", title="none"
)

# Set up the entry and assert that the values set during setup are where we expect
Expand Down
6 changes: 3 additions & 3 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ async def test_successful_config_flow_option(
assert result["step_id"] == "init"

result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input=MOCK_CONFIG_CHARGER_NEW
result["flow_id"], user_input=MOCK_CONFIG_USER
)

# Check that the option flow is complete and a new entry is created with
# the input data
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == MOCK_CONFIG_CHARGER_NEW
assert result["data"] == MOCK_CONFIG_USER
if "errors" in result.keys():
assert len(result["errors"]) == 0
assert result["result"]
Expand All @@ -138,7 +138,7 @@ async def test_unsuccessful_config_flow_option(hass: HomeAssistant):
assert result["step_id"] == "init"

result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input=MOCK_CONFIG_CHARGER_NEW
result["flow_id"], user_input=MOCK_CONFIG_USER
)

# Check that the config flow is not complete and that there are errors
Expand Down
55 changes: 55 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# Assertions allow you to verify that the return value of whatever is on the left
# side of the assertion matches with the right side.


# pylint: disable=unused-argument
# async def test_setup_unload_and_reload_entry(hass, bypass_get_data):
async def test_setup_unload_and_reload_entry(hass, bypass_validate_input_sensors):
Expand All @@ -34,20 +35,23 @@ async def test_setup_unload_and_reload_entry(hass, bypass_validate_input_sensors
# them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data
# call, no code from custom_components/integration_blueprint/api.py actually runs.
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

# Reload the entry and assert that the data from above is still there
assert await async_reload_entry(hass, config_entry) is None
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

# Unload the entry and verify that the data has been removed
assert await async_unload_entry(hass, config_entry)
await hass.async_block_till_done()
assert config_entry.entry_id not in hass.data[DOMAIN]


Expand Down Expand Up @@ -136,3 +140,54 @@ async def test_setup_with_migration_from_future(hass, bypass_validate_input_sens

# Migrate from version 9999
assert not await async_migrate_entry(hass, config_entry)


async def test_setup_new_integration_name(hass, bypass_validate_input_sensors):
"""Test entry setup with new integration name."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ALL, entry_id="test")

# Set up the entry and assert that the values set during setup are where we expect
# them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data
# call, no code from custom_components/integration_blueprint/api.py actually runs.
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

# Change title
config_entry.title = "New title"

# Reload the entry and assert that the data from above is still there
assert await async_reload_entry(hass, config_entry) is None
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

test = hass.data["device_registry"].devices
device = hass.data["device_registry"].devices[next(iter(test))]
assert device.name_by_user == "New title"

# Change a changed title
config_entry.title = "New title2"

# Reload the entry and assert that the data from above is still there
assert await async_reload_entry(hass, config_entry) is None
await hass.async_block_till_done()
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)

test = hass.data["device_registry"].devices
device = hass.data["device_registry"].devices[next(iter(test))]
assert device.name_by_user == "New title2"

# Unload the entry and verify that the data has been removed
assert await async_unload_entry(hass, config_entry)
await hass.async_block_till_done()
assert config_entry.entry_id not in hass.data[DOMAIN]
8 changes: 5 additions & 3 deletions tests/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def test_number(hass, bypass_validate_input_sensors):
"""Test sensor properties."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONFIG_MIN_SOC, entry_id="test"
domain=DOMAIN, data=MOCK_CONFIG_MIN_SOC, entry_id="test", title="none"
)

# Set up the entry and assert that the values set during setup are where we expect
Expand Down Expand Up @@ -132,13 +132,15 @@ async def test_number_restore(
"""Test sensor properties."""

# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ALL, entry_id="test")
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONFIG_ALL, entry_id="test", title="none"
)
await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()

number_charging_speed: EVSmartChargingNumberChargingSpeed = hass.data[
"entity_components"
][NUMBER].get_entity("number.ev_smart_charging_charging_speed")
][NUMBER].get_entity("number.none_charging_speed")

await number_charging_speed.async_set_native_value(45)
assert number_charging_speed.native_value == 45
Expand Down
Loading

0 comments on commit bdf0d42

Please sign in to comment.