Skip to content

Commit

Permalink
Merge pull request #77 from maorcc/correct-entity-names
Browse files Browse the repository at this point in the history
Add support to control how entity names
  • Loading branch information
elad-bar committed Jun 25, 2024
2 parents 4549556 + 0cdd6eb commit 1fbb24c
Show file tree
Hide file tree
Showing 18 changed files with 156 additions and 48 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
- Handle timeouts as managed failure instead of general failure
- Ignore update request when the connection is not established
- Improved log messages of status changes
- Add support to control how entity names will look like, under account device:

| Approach | Meter | Account | Reason |
| ---------------- | ------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Unique (Default) | {address} {meter serial number} | {first name} {last name} {account number} | [can't rename meter device to a meaningful name #39](https://github.com/maorcc/citymind_water_meter/issues/39) |
| Non unique | Meter {meter serial number} | Account {account number} | [Bad Entity Names - seems to be translated from Hebrew #71](https://github.com/maorcc/citymind_water_meter/issues/71) |

## v3.0.5

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def get_log_level(status: StrEnum) -> int:
@staticmethod
def get_ha_error(status: str) -> str | None:
errors = {
str(ConnectivityStatus.InvalidCredentials): "invalid_admin_credentials",
str(ConnectivityStatus.InvalidCredentials): "invalid_credentials",
str(ConnectivityStatus.TemporaryConnected): "missing_permanent_api_key",
str(ConnectivityStatus.MissingAPIKey): "missing_permanent_api_key",
str(ConnectivityStatus.Failed): "invalid_server_details",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/citymind_water_meter/common/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,14 @@
ATTR_SEWAGE_COST = "Sewage Cost"
ATTR_TOTAL_COST = "Monthly Cost"

STORAGE_DATA_USE_UNIQUE_DEVICE_NAMES = "use-unique-device-names"
STORAGE_DATA_METERS = "meters"
STORAGE_DATA_METER_LOW_RATE_CONSUMPTION_THRESHOLD = "low_rate_consumption_threshold"
STORAGE_DATA_METER_LOW_RATE_COST = "low_rate_cost"
STORAGE_DATA_METER_HIGH_RATE_COST = "high_rate_cost"
STORAGE_DATA_METER_SEWAGE_COST = "sewage_cost"

DEFAULT_USE_UNIQUE_DEVICE_NAMES = True
DEFAULT_LOW_RATE_CONSUMPTION_THRESHOLD = 3.5
DEFAULT_LOW_RATE_COST = 7.955
DEFAULT_HIGH_RATE_COST = 14.6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ class IntegrationNumberEntityDescription(
entity_category=EntityCategory.CONFIG,
entity_type=EntityType.ACCOUNT,
),
IntegrationSwitchEntityDescription(
key=EntityKeys.USE_UNIQUE_DEVICE_NAMES,
entity_category=EntityCategory.CONFIG,
entity_type=EntityType.ACCOUNT,
),
]


Expand Down
1 change: 1 addition & 0 deletions custom_components/citymind_water_meter/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ class EntityKeys(StrEnum):
ALERT_LEAK_EMAIL = "alert_leak_email"
ALERT_EXCEEDED_THRESHOLD_SMS = "alert_exceeded_threshold_sms"
ALERT_EXCEEDED_THRESHOLD_EMAIL = "alert_exceeded_threshold_email"
USE_UNIQUE_DEVICE_NAMES = "use_unique_device_name"
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,30 @@ class AccountProcessor(BaseProcessor):
def __init__(self, config_manager: ConfigManager):
super().__init__(config_manager)

self.processor_type = EntityType.ACCOUNT

self._account = None

@property
def processor_type(self) -> EntityType | None:
return EntityType.ACCOUNT

def get(self) -> AccountData | None:
return self._account

def _get_device_info_name(self, meter_id: str | None = None):
name = self._get_account_name()

return name
def get_device_info(self, identifier: str | None = None) -> DeviceInfo:
if self._config_manager.use_unique_device_names:
device_name = self._account.unique_name

def get_device_info(self, meter_id: str | None = None) -> DeviceInfo:
name = self._get_device_info_name()
else:
device_name = self._get_account_name()

municipal_name = self._account.municipal_name
if municipal_name is None:
municipal_name = PROVIDER

device_info = DeviceInfo(
identifiers={(DEFAULT_NAME, name)},
name=name,
model=self.processor_type,
identifiers={(DEFAULT_NAME, device_name)},
name=device_name,
model=str(self.processor_type).capitalize(),
manufacturer=municipal_name,
configuration_url=CITY_MIND_WEBSITE,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class BaseProcessor:
_config_manager: ConfigManager | None = None
_config_data: ConfigData | None = None
_unique_messages: list[str] | None = None
processor_type: EntityType | None = None

def __init__(self, config_manager: ConfigManager):
self._config_manager = config_manager
Expand All @@ -36,10 +35,13 @@ def __init__(self, config_manager: ConfigManager):
self._account_number = None
self._first_name = None
self._last_name = None
self.processor_type = None

self._unique_messages = []

@property
def processor_type(self) -> EntityType | None:
return None

def update(self, api_data: dict):
self._api_data = api_data

Expand Down Expand Up @@ -68,28 +70,27 @@ def _unique_log(self, log_level: int, message: str):
_LOGGER.log(log_level, message)

def _get_account_name(self):
parts = [self._first_name, self._last_name, str(self._account_number)]

relevant_parts = [part for part in parts if part is not None]

name = " ".join(relevant_parts)
name = self._get_default_device_info_name(self._account_number)

return name

def get_device_info(self, item_id: str | None = None) -> DeviceInfo:
def get_device_info(self, identifier: str | None = None) -> DeviceInfo:
pass

def _get_device_info_name(self, meter_id: str | None = None):
parts = [self.processor_type, meter_id]
def _get_default_device_info_name(self, identifier: str | None = None) -> str:
parts = [self.processor_type, identifier]

relevant_parts = [part for part in parts if part is not None]
relevant_parts = [str(part).capitalize() for part in parts if part is not None]

name = " ".join(relevant_parts)

_LOGGER.debug(f"Processor type: {str(self.processor_type)}")
_LOGGER.debug(f"Default device name: {name}")

return name

def _get_device_info_unique_id(self, item_id: str | None = None):
identifier = self._get_device_info_name(item_id)
def _get_device_info_unique_id(self, item_id: str | None = None) -> str:
identifier = self._get_default_device_info_name(item_id)

unique_id = slugify(identifier)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ class MeterProcessor(BaseProcessor):
def __init__(self, config_manager: ConfigManager):
super().__init__(config_manager)

self.processor_type = EntityType.METER

self._meters = {}
self._account_number = None

@property
def processor_type(self) -> EntityType | None:
return EntityType.METER

def get_meters(self) -> list[str]:
return list(self._meters.keys())

Expand All @@ -66,27 +68,22 @@ def get_data(self, meter_id: str) -> MeterData:

return meter

def _get_device_info_name(self, meter_id: str | None = None):
meter = self.get_data(meter_id)
parts = [meter.address, meter.meter_serial_number]

relevant_parts = [part for part in parts if part is not None]
def get_device_info(self, identifier: str | None = None) -> DeviceInfo:
device = self.get_data(identifier)

name = " ".join(relevant_parts)
if self._config_manager.use_unique_device_names:
device_name = device.unique_name

return name

def get_device_info(self, meter_id: str | None = None) -> DeviceInfo:
device = self.get_data(meter_id)
device_name = self._get_device_info_name(device.meter_id)
else:
device_name = self._get_default_device_info_name(device.meter_serial_number)

parent_device_id = self._get_account_name()
unique_id = self._get_device_info_unique_id(device.meter_id)

device_info = DeviceInfo(
identifiers={(DEFAULT_NAME, unique_id)},
name=device_name,
model=self.processor_type,
model=str(self.processor_type).capitalize(),
manufacturer=device.address,
via_device=(DEFAULT_NAME, parent_device_id),
)
Expand Down
24 changes: 21 additions & 3 deletions custom_components/citymind_water_meter/managers/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CONFIGURATION_FILE,
DEFAULT_METER_CONFIG,
DEFAULT_NAME,
DEFAULT_USE_UNIQUE_DEVICE_NAMES,
DOMAIN,
INVALID_TOKEN_SECTION,
SIGNAL_DATA_CHANGED,
Expand All @@ -27,6 +28,7 @@
STORAGE_DATA_METER_LOW_RATE_COST,
STORAGE_DATA_METER_SEWAGE_COST,
STORAGE_DATA_METERS,
STORAGE_DATA_USE_UNIQUE_DEVICE_NAMES,
)
from ..common.entity_descriptions import IntegrationEntityDescription
from ..models.analytics_periods import AnalyticPeriodsData
Expand Down Expand Up @@ -105,6 +107,14 @@ def meters(self):

return result

@property
def use_unique_device_names(self) -> bool:
result = self._data.get(
STORAGE_DATA_USE_UNIQUE_DEVICE_NAMES, DEFAULT_USE_UNIQUE_DEVICE_NAMES
)

return result

@property
def config_data(self) -> ConfigData:
config_data = self._config_data
Expand Down Expand Up @@ -230,23 +240,24 @@ async def _load(self):
self._data = {}

default_configuration = self._get_defaults()
_LOGGER.info(f"default_configuration: {default_configuration}")
_LOGGER.debug(f"Default configuration: {default_configuration}")

for key in default_configuration:
value = default_configuration[key]

if key not in self._data:
_LOGGER.info(f"adding {key}")
_LOGGER.debug(f"Adding {key}")
should_save = True
self._data[key] = value

if should_save:
_LOGGER.info("updated")
_LOGGER.debug("Updating configuration")
await self._save()

@staticmethod
def _get_defaults() -> dict:
data = {
STORAGE_DATA_USE_UNIQUE_DEVICE_NAMES: DEFAULT_USE_UNIQUE_DEVICE_NAMES,
STORAGE_DATA_METERS: {},
}

Expand Down Expand Up @@ -310,6 +321,13 @@ async def _save(self):

await self._store.async_save(store_data)

async def set_use_unique_device_names(self, value: bool) -> None:
self._data[STORAGE_DATA_USE_UNIQUE_DEVICE_NAMES] = value

await self._save()

self._async_dispatcher_send(SIGNAL_DATA_CHANGED)

async def _set_meter_config(self, meter_id: str, key: str, value: float) -> None:
if meter_id not in self.meters:
self._data[STORAGE_DATA_METERS][meter_id] = copy(DEFAULT_METER_CONFIG)
Expand Down
52 changes: 51 additions & 1 deletion custom_components/citymind_water_meter/managers/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@
from homeassistant.components.homeassistant import SERVICE_RELOAD_CONFIG_ENTRY
from homeassistant.const import ATTR_STATE
from homeassistant.core import Event, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import (
DeviceInfo,
async_entries_for_config_entry as devices_by_config_entry,
async_get as async_get_device_registry,
)
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry as entities_by_config_entry,
async_get as async_get_entity_registry,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from ..common.connectivity_status import ConnectivityStatus
Expand Down Expand Up @@ -295,6 +303,7 @@ def _build_data_mapping(self):
EntityKeys.ALERT_LEAK_EMAIL: self._get_alert_setting_data,
EntityKeys.ALERT_LEAK_WHILE_AWAY_SMS: self._get_alert_setting_data,
EntityKeys.ALERT_LEAK_WHILE_AWAY_EMAIL: self._get_alert_setting_data,
EntityKeys.USE_UNIQUE_DEVICE_NAMES: self._get_use_unique_device_names_data,
}

self._data_mapping = data_mapping
Expand Down Expand Up @@ -530,6 +539,19 @@ def _get_alerts_data(self, _entity_description) -> dict | None:

return result

def _get_use_unique_device_names_data(self, _entity_description) -> dict | None:
is_on = self._config_manager.use_unique_device_names

result = {
ATTR_IS_ON: is_on,
ATTR_ACTIONS: {
ACTION_ENTITY_TURN_ON: self._set_use_unique_device_names_enabled,
ACTION_ENTITY_TURN_OFF: self._set_use_unique_device_names_disabled,
},
}

return result

def _get_alert_setting_data(self, entity_description) -> dict | None:
account = self._account_processor.get()
is_on = account.alert_settings.get(entity_description.key, False)
Expand Down Expand Up @@ -578,6 +600,19 @@ async def _set_sewage_cost(self, _entity_description, meter_id: str, value: floa

await self.async_request_refresh()

async def _set_use_unique_device_names_enabled(self, _entity_description):
await self._set_use_unique_device_names_state(True)

async def _set_use_unique_device_names_disabled(self, _entity_description):
await self._set_use_unique_device_names_state(False)

async def _set_use_unique_device_names_state(self, enabled: bool):
_LOGGER.debug(f"Set unique device name state, Value: {enabled}")

await self._config_manager.set_use_unique_device_names(enabled)

await self._remove_and_refresh()

async def _set_alert_setting_enabled(self, entity_description):
await self._set_alert_setting_state(entity_description, True)

Expand Down Expand Up @@ -614,3 +649,18 @@ def _validate_weekday(self):
self._is_weekend = is_weekend

self.update_interval = self.current_update_interval

async def _remove_and_refresh(self):
entity_registry = async_get_entity_registry(self.hass)
device_registry = async_get_device_registry(self.hass)

entities = entities_by_config_entry(entity_registry, self.config_entry.entry_id)
devices = devices_by_config_entry(device_registry, self.config_entry.entry_id)

for entity_entry in entities:
entity_registry.async_remove(entity_entry.entity_id)

for device_entry in devices:
device_registry.async_remove_device(device_entry.id)

await self._reload_integration()
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async def async_step(self, user_input: dict | None = None):
error_key = ConnectivityStatus.get_ha_error(api.status)

except LoginError:
error_key = "invalid_admin_credentials"
error_key = "invalid_credentials"

except InvalidToken:
error_key = "corrupted_encryption_key"
Expand Down
4 changes: 1 addition & 3 deletions custom_components/citymind_water_meter/managers/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,6 @@ async def terminate(self):
async def validate(self):
await self.initialize()

await self.login()

async def update(self):
_LOGGER.debug(
f"Updating data for user {self._config_data.email}, "
Expand Down Expand Up @@ -442,7 +440,7 @@ def _handle_client_error(
if crex.status == 401:
self._set_status(ConnectivityStatus.NotConnected)

else:
elif crex.status > 401:
self._set_status(ConnectivityStatus.Failed, message)

def _handle_server_timeout(self, endpoint: str, method: str):
Expand Down
Loading

0 comments on commit 1fbb24c

Please sign in to comment.