latest changes
This commit is contained in:
+65
-3
@@ -1,7 +1,69 @@
|
||||
# ignore objects in .storage
|
||||
.storage
|
||||
#.storage
|
||||
# ignore log-files
|
||||
home-assistant.log*
|
||||
#home-assistant.log*
|
||||
|
||||
# ignore databases
|
||||
home-assistant_v2.*
|
||||
#home-assistant_v2.*
|
||||
|
||||
|
||||
s -lovelace
|
||||
## Ignore all
|
||||
*
|
||||
**
|
||||
|
||||
## Allow folders
|
||||
# !*/
|
||||
!/automation2/
|
||||
!/automation2/*.yaml
|
||||
... and more stuff, one of those for every folder basically
|
||||
|
||||
## Allow files
|
||||
!alert.yaml
|
||||
!automations.yaml
|
||||
!.HA_VERSION
|
||||
!.gitkeep
|
||||
!README.md
|
||||
!LICENSE
|
||||
... And more stuff, one for every .yaml file basically.
|
||||
|
||||
############# Failsafe \/ \/ \/ \/ \/ #############
|
||||
|
||||
# File Ignores
|
||||
home-assistant.log.*
|
||||
remote_cmd.yaml
|
||||
**secrets.yaml
|
||||
shell_command.yaml
|
||||
**/test.yaml
|
||||
/zigbee2mqtt/*.yaml
|
||||
|
||||
# Generic ignores
|
||||
*.log
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.pyc
|
||||
._*
|
||||
__pycache__
|
||||
|
||||
# Directory (contents) ignores
|
||||
/blueprints/automation/test**
|
||||
/blueprints/script/test**
|
||||
/Purgatory**
|
||||
/scripts/
|
||||
/themes**
|
||||
/www**
|
||||
/.storage/auth**
|
||||
/.storage/cloud**
|
||||
/.storage/core**
|
||||
/.storage/hacs**
|
||||
/.storage/http
|
||||
*/deps**
|
||||
*/image**
|
||||
*/pop.git**
|
||||
*test**
|
||||
|
||||
# Ignore files created by IDE's
|
||||
*&/.vscode**
|
||||
*/.theia**
|
||||
*/.Trash**
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
"""electrolux status integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_LANGUAGE,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
CONF_COUNTRY_CODE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_RENEW_INTERVAL,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_COUNTRY_CODE,
|
||||
DEFAULT_WEBSOCKET_RENEWAL_DELAY,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
languages,
|
||||
)
|
||||
from .coordinator import ElectroluxCoordinator
|
||||
from .util import get_electrolux_session
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up this integration using YAML is not supported."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
if hass.data.get(DOMAIN) is None:
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
renew_interval = DEFAULT_WEBSOCKET_RENEWAL_DELAY
|
||||
if entry.options.get(CONF_RENEW_INTERVAL):
|
||||
renew_interval = entry.options[CONF_RENEW_INTERVAL]
|
||||
|
||||
username = entry.data.get(CONF_USERNAME)
|
||||
password = entry.data.get(CONF_PASSWORD)
|
||||
country_code = entry.data.get(CONF_COUNTRY_CODE, DEFAULT_COUNTRY_CODE)
|
||||
language = languages.get(entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE), "eng")
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
client = get_electrolux_session(username, password, country_code, session, language)
|
||||
coordinator = ElectroluxCoordinator(
|
||||
hass,
|
||||
client=client,
|
||||
renew_interval=renew_interval,
|
||||
username=username,
|
||||
)
|
||||
|
||||
await coordinator.get_stored_token()
|
||||
if not await coordinator.async_login():
|
||||
raise ConfigEntryAuthFailed("Electrolux wrong credentials")
|
||||
|
||||
# Bug ?
|
||||
if coordinator.config_entry is None:
|
||||
coordinator.config_entry = entry
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
# Initialize entities
|
||||
_LOGGER.debug("async_setup_entry setup_entities")
|
||||
await coordinator.setup_entities()
|
||||
_LOGGER.debug("async_setup_entry listen_websocket")
|
||||
coordinator.listen_websocket()
|
||||
# _LOGGER.debug("async_setup_entry launch_websocket_renewal_task")
|
||||
# await coordinator.launch_websocket_renewal_task()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.api.close)
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
_LOGGER.debug("async_setup_entry async_config_entry_first_refresh")
|
||||
# Fill in the values for first time
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
_LOGGER.debug("async_setup_entry extend PLATFORMS")
|
||||
coordinator.platforms.extend(PLATFORMS)
|
||||
|
||||
# Call async_setup_entry in entity files
|
||||
_LOGGER.debug("async_setup_entry async_forward_entry_setups")
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_LOGGER.debug("async_setup_entry OVER")
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
coordinator: ElectroluxCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
await coordinator.close_websocket()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload config entry."""
|
||||
_LOGGER.debug("Electrolux async_reload_entry %s", entry)
|
||||
await async_unload_entry(hass, entry)
|
||||
await async_setup_entry(hass, entry)
|
||||
@@ -1,701 +0,0 @@
|
||||
"""API for Electrolux Status."""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pyelectroluxocp.apiModels import ApplianceInfoResponse, ApplienceStatusResponse
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.button import ButtonDeviceClass
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
from homeassistant.const import Platform, UnitOfTemperature
|
||||
|
||||
from .binary_sensor import ElectroluxBinarySensor
|
||||
from .button import ElectroluxButton
|
||||
from .catalog_core import CATALOG_BASE, CATALOG_MODEL
|
||||
from .const import (
|
||||
BINARY_SENSOR,
|
||||
BUTTON,
|
||||
ATTRIBUTES_BLACKLIST,
|
||||
NUMBER,
|
||||
PLATFORMS,
|
||||
RENAME_RULES,
|
||||
SELECT,
|
||||
SENSOR,
|
||||
STATIC_ATTRIBUTES,
|
||||
SWITCH, ATTRIBUTES_WHITELIST,
|
||||
)
|
||||
from .entity import ElectroluxEntity
|
||||
from .model import ElectroluxDevice
|
||||
from .number import ElectroluxNumber
|
||||
from .select import ElectroluxSelect
|
||||
from .sensor import ElectroluxSensor
|
||||
from .switch import ElectroluxSwitch
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
HEADERS = {"Content-type": "application/json; charset=UTF-8"}
|
||||
|
||||
def deep_merge_dicts(dict1, dict2):
|
||||
"""
|
||||
Recursively merge two dictionaries.
|
||||
"""
|
||||
result = dict1.copy()
|
||||
for key, value in dict2.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = deep_merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
class ElectroluxLibraryEntity:
|
||||
"""Electrolux Library Entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
status: str,
|
||||
state: ApplienceStatusResponse,
|
||||
appliance_info: ApplianceInfoResponse,
|
||||
capabilities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initaliaze the entity."""
|
||||
self.name = name
|
||||
self.status = status
|
||||
self.state = state
|
||||
self.appliance_info = appliance_info
|
||||
self.capabilities = capabilities
|
||||
|
||||
@property
|
||||
def reported_state(self) -> dict[str, Any]:
|
||||
"""Return the reported state of the appliance."""
|
||||
return self.state.get("properties", {}).get("reported")
|
||||
|
||||
def get_name(self):
|
||||
"""Get entity name."""
|
||||
return self.name
|
||||
|
||||
def get_value(self, attr_name) -> Any:
|
||||
"""Return value by attribute."""
|
||||
if "/" in attr_name:
|
||||
source, attr = attr_name.split("/")
|
||||
return self.reported_state.get(source, {}).get(attr, None)
|
||||
return self.reported_state.get(attr_name, None)
|
||||
|
||||
def get_sensor_name(self, attr_name: str) -> str:
|
||||
"""Get the name of the sensor."""
|
||||
sensor = attr_name
|
||||
for truncate_rule in RENAME_RULES:
|
||||
sensor = re.sub(truncate_rule, "", sensor)
|
||||
sensor = sensor[0].upper() + sensor[1:]
|
||||
sensor = sensor.replace("_", " ")
|
||||
sensor = sensor.replace("/", " ")
|
||||
group = ""
|
||||
words = []
|
||||
for i, char in enumerate(sensor):
|
||||
if group == "":
|
||||
group = char
|
||||
else:
|
||||
if char == " " and len(group) > 0:
|
||||
words.append(group)
|
||||
group = ""
|
||||
continue
|
||||
|
||||
if (
|
||||
(char.isupper() or char.isdigit())
|
||||
and (sensor[i - 1].isupper() or sensor[i - 1].isdigit())
|
||||
and (
|
||||
(i == len(sensor) - 1)
|
||||
or (sensor[i + 1].isupper() or sensor[i + 1].isdigit())
|
||||
)
|
||||
):
|
||||
group += char
|
||||
elif (char.isupper() or char.isdigit()) and sensor[i - 1].islower():
|
||||
if re.match("^[A-Z0-9]+$", group):
|
||||
words.append(group)
|
||||
else:
|
||||
words.append(group.lower())
|
||||
group = char
|
||||
else:
|
||||
group += char
|
||||
if len(group) > 0:
|
||||
if re.match("^[A-Z0-9]+$", group):
|
||||
words.append(group)
|
||||
else:
|
||||
words.append(group.lower())
|
||||
return " ".join(words).lower().capitalize()
|
||||
|
||||
# def get_sensor_name_old(self, attr_name: str, container: str | None = None):
|
||||
# """Convert sensor format.
|
||||
|
||||
# ex: "fCMiscellaneousState/detergentExtradosage" to "Detergent extradosage".
|
||||
# """
|
||||
# attr_name = attr_name.rpartition("/")[-1] or attr_name
|
||||
# attr_name = attr_name[0].upper() + attr_name[1:]
|
||||
# attr_name = " ".join(re.findall("[A-Z][^A-Z]*", attr_name))
|
||||
# return attr_name.capitalize()
|
||||
|
||||
def get_entity_name(self, attr_name: str) -> str:
|
||||
"""Extract Entity Name.
|
||||
|
||||
ex: Convert format "fCMiscellaneousState/EWX1493A_detergentExtradosage" to "XdetergentExtradosage"
|
||||
"""
|
||||
for truncate_rule in RENAME_RULES:
|
||||
attr_name = re.sub(truncate_rule, "", attr_name)
|
||||
|
||||
return attr_name.rpartition("/")[-1] or attr_name
|
||||
|
||||
def get_entity_attr(self, attr_name: str) -> str:
|
||||
"""Extract Entity attr in raw format.
|
||||
|
||||
ex: Convert format "fCMiscellaneousState/EWX1493A_detergentExtradosage" to "EWX1493A_detergentExtradosage"
|
||||
"""
|
||||
return attr_name.rpartition("/")[-1] or attr_name
|
||||
|
||||
def get_category(self, attr_name: str) -> str:
|
||||
"""Extract category.
|
||||
|
||||
ex: "fCMiscellaneousState/detergentExtradosage" to "fCMiscellaneousState".
|
||||
or "" if none
|
||||
"""
|
||||
return attr_name.rpartition("/")[0]
|
||||
|
||||
def get_capability(self, attr_name: str) -> dict[str, Any] | None:
|
||||
"""Retrieve the capability from self.capabilities using the attribute name.
|
||||
|
||||
May contain slashes for nested keys.
|
||||
"""
|
||||
if self.capabilities.get(attr_name, None):
|
||||
return self.capabilities.get(attr_name)
|
||||
|
||||
keys = attr_name.split("/")
|
||||
result = self.capabilities
|
||||
|
||||
for key in keys:
|
||||
result = result.get(key)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
def get_entity_unit(self, attr_name: str):
|
||||
"""Get entity unit type."""
|
||||
capability_def: dict[str, Any] | None = self.get_capability(attr_name)
|
||||
if not capability_def:
|
||||
return None
|
||||
# Type : string, int, number, boolean (other values ignored)
|
||||
type_units = capability_def.get("type", None)
|
||||
if not type_units:
|
||||
return None
|
||||
if type_units == "temperature":
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return None
|
||||
|
||||
def get_entity_device_class(self, attr_name: str):
|
||||
"""Get entity device class."""
|
||||
capability_def: dict[str, Any] | None = self.get_capability(attr_name)
|
||||
if not capability_def:
|
||||
return None
|
||||
# Type : string, int, number, boolean (other values ignored)
|
||||
type_class = capability_def.get("type", None)
|
||||
if not type_class:
|
||||
return None
|
||||
if type_class == "temperature":
|
||||
if capability_def.get("access", None) == "readwrite":
|
||||
return NumberDeviceClass.TEMPERATURE
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
return None
|
||||
|
||||
def get_entity_type(self, attr_name: str) -> Platform | None:
|
||||
"""Get entity type."""
|
||||
|
||||
capability_def: dict[str, Any] | None = self.get_capability(attr_name)
|
||||
if not capability_def:
|
||||
return None
|
||||
|
||||
# Type : string, int, number, boolean (other values ignored)
|
||||
type_object = capability_def.get("type", None)
|
||||
if not type_object:
|
||||
return None
|
||||
|
||||
# Access : read, readwrite (other values ignored)
|
||||
access = capability_def.get("access", None)
|
||||
if not access:
|
||||
return None
|
||||
|
||||
# Exception (Electrolux bug)
|
||||
if (
|
||||
type_object == "boolean"
|
||||
and access == "readwrite"
|
||||
and capability_def.get("values", None) is not None
|
||||
):
|
||||
return SWITCH
|
||||
|
||||
# List of values ? if values is defined and has at least 1 entry
|
||||
values: dict[str, Any] | None = capability_def.get("values", None)
|
||||
if (
|
||||
values
|
||||
and access == "readwrite"
|
||||
and isinstance(values, dict)
|
||||
and len(values) > 0
|
||||
):
|
||||
if (
|
||||
type_object not in ["number", "temperature"]
|
||||
or capability_def.get("min", None) is None
|
||||
):
|
||||
return SELECT
|
||||
|
||||
match type_object:
|
||||
case "boolean":
|
||||
if access == "read":
|
||||
return BINARY_SENSOR
|
||||
if access == "readwrite":
|
||||
return SWITCH
|
||||
case "temperature":
|
||||
if access == "read":
|
||||
return SENSOR
|
||||
if access == "readwrite":
|
||||
return NUMBER
|
||||
case "alert":
|
||||
return SENSOR
|
||||
case _:
|
||||
if (
|
||||
self.get_entity_name(attr_name) == "executeCommand"
|
||||
and access == "read"
|
||||
): # FIX for https://github.com/albaintor/homeassistant_electrolux_status/issues/74
|
||||
return BUTTON
|
||||
if access == "write":
|
||||
return BUTTON
|
||||
if access == "constant":
|
||||
return SENSOR
|
||||
if access == "read" and type_object in [
|
||||
"number",
|
||||
"int",
|
||||
"boolean",
|
||||
"string",
|
||||
]:
|
||||
return SENSOR
|
||||
if type_object in ("int", "number"):
|
||||
return NUMBER
|
||||
_LOGGER.debug(
|
||||
"Electrolux unable to determine type for %s. Type: %s Access: %s",
|
||||
attr_name,
|
||||
type_object,
|
||||
access,
|
||||
)
|
||||
return None
|
||||
|
||||
def sources_list(self) -> list[str] | None:
|
||||
"""List the capability types."""
|
||||
if self.capabilities is None:
|
||||
_LOGGER.warning("Electrolux capabilities list is empty")
|
||||
return None
|
||||
|
||||
# dont load these entities by as they are not useful
|
||||
# we do load some of these directly via STATIC_ATTRIBUTES as
|
||||
# one or another are useful, but not all child values are
|
||||
|
||||
def keep_source(source: str) -> bool:
|
||||
for ignored_pattern in ATTRIBUTES_BLACKLIST:
|
||||
if re.match(ignored_pattern, source):
|
||||
for whitelist_pattern in ATTRIBUTES_WHITELIST:
|
||||
if re.match(whitelist_pattern, source):
|
||||
return True
|
||||
_LOGGER.debug("Exclude source %s from list", source)
|
||||
return False
|
||||
return True
|
||||
|
||||
sources = [
|
||||
key
|
||||
for key in list(self.capabilities.keys())
|
||||
if keep_source(key)
|
||||
]
|
||||
|
||||
for key, value in self.capabilities.items():
|
||||
if not keep_source(key):
|
||||
continue
|
||||
|
||||
if isinstance(value, dict):
|
||||
for sub_key, sub_value in value.items():
|
||||
if (
|
||||
isinstance(sub_value, dict)
|
||||
and "access" in sub_value
|
||||
and "type" in sub_value
|
||||
):
|
||||
sources.append(f"{key}/{sub_key}")
|
||||
elif "access" in value and "type" in value:
|
||||
sources.append(key)
|
||||
|
||||
return sources
|
||||
|
||||
# def sources_list_old(self):
|
||||
# _LOGGER.warning(self.capabilities)
|
||||
# return [
|
||||
# key
|
||||
# for key in list(self.capabilities.keys())
|
||||
# if not key.startswith("applianceCareAndMaintenance")
|
||||
# ]
|
||||
|
||||
|
||||
class Appliance:
|
||||
"""Define the Appliance Class."""
|
||||
|
||||
brand: str
|
||||
device: str
|
||||
entities: list[ElectroluxEntity]
|
||||
coordinator: Any
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
name: str,
|
||||
pnc_id: str,
|
||||
brand: str,
|
||||
model: str,
|
||||
state: ApplienceStatusResponse,
|
||||
) -> None:
|
||||
"""Initiate the appliance."""
|
||||
self.own_capabilties = False
|
||||
self.data = None
|
||||
self.coordinator = coordinator
|
||||
self.model = model
|
||||
self.pnc_id = pnc_id
|
||||
self.name = name
|
||||
self.brand = brand
|
||||
self.state: ApplienceStatusResponse = state
|
||||
|
||||
@property
|
||||
def reported_state(self) -> dict[str, Any]:
|
||||
"""Return the reported state of the appliance."""
|
||||
return self.state.get("properties", {}).get("reported", {})
|
||||
|
||||
@property
|
||||
def appliance_type(self) -> dict[str, Any]:
|
||||
"""Return the reported type of the appliance.
|
||||
|
||||
CR: Refridgerator
|
||||
WM: Washing Machine
|
||||
"""
|
||||
return self.reported_state.get("applianceInfo", {}).get(
|
||||
"applianceType"
|
||||
) or self.state.get("applianceData", {}).get("modelName")
|
||||
|
||||
@property
|
||||
def catalog(self) -> dict[str, ElectroluxDevice]:
|
||||
"""Return the defined catalog for the appliance."""
|
||||
# TODO: Use appliance_type as opposed to model?
|
||||
if self.model in CATALOG_MODEL:
|
||||
_LOGGER.debug("Extending catalog for %s", self.model)
|
||||
# Make a deep copy of the base catalog to preserve it
|
||||
new_catalog = copy.deepcopy(CATALOG_BASE)
|
||||
|
||||
# Get the specific model's extended catalog
|
||||
model_catalog = CATALOG_MODEL[self.model]
|
||||
|
||||
# Update the existing catalog with the extended information for this model
|
||||
for key, device in model_catalog.items():
|
||||
new_catalog[key] = device
|
||||
|
||||
return new_catalog
|
||||
return CATALOG_BASE
|
||||
|
||||
def update_missing_entities(self) -> None:
|
||||
"""Add missing entities when no capabilities returned by the API.
|
||||
|
||||
This is done dynamically but only when the reported state contains the attributes.
|
||||
"""
|
||||
if not self.own_capabilties or not self.reported_state:
|
||||
return
|
||||
|
||||
for key, catalog_item in self.catalog.items():
|
||||
category = self.data.get_category(key)
|
||||
if (
|
||||
category
|
||||
and self.reported_state.get(category, None)
|
||||
and self.reported_state.get(category, None).get(key)
|
||||
) or (not category and self.reported_state.get(key, None)):
|
||||
found: bool = False
|
||||
for entity in self.entities:
|
||||
if entity.entity_attr == key and entity.entity_source == category:
|
||||
found = True
|
||||
keys = key.split("/")
|
||||
capabilities = self.data.capabilities
|
||||
for key in keys[:-1]:
|
||||
capabilities = capabilities.setdefault(key, {})
|
||||
capabilities[keys[-1]] = catalog_item.capability_info
|
||||
break
|
||||
if not found:
|
||||
_LOGGER.debug(
|
||||
"Electrolux discovered new entity from extracted data. Key: %s",
|
||||
key,
|
||||
)
|
||||
if entity := self.get_entity(key):
|
||||
self.entities.extend(entity)
|
||||
|
||||
def get_state(self, attr_name: str) -> dict[str, Any] | None:
|
||||
"""Retrieve the start from self.reported_state using the attribute name.
|
||||
|
||||
May contain slashes for nested keys.
|
||||
"""
|
||||
|
||||
keys = attr_name.split("/")
|
||||
result = self.reported_state
|
||||
|
||||
for key in keys:
|
||||
result = result.get(key)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
def get_entity(self, capability: str) -> list[ElectroluxEntity] | None:
|
||||
"""Return the entity."""
|
||||
entity_type = self.data.get_entity_type(capability)
|
||||
entity_name = self.data.get_entity_name(capability)
|
||||
entity_attr = self.data.get_entity_attr(capability)
|
||||
category = self.data.get_category(capability)
|
||||
capability_info = self.data.get_capability(capability)
|
||||
device_class = self.data.get_entity_device_class(capability)
|
||||
entity_category = None
|
||||
entity_icon = None
|
||||
unit = self.data.get_entity_unit(capability)
|
||||
display_name = self.data.get_sensor_name(capability)
|
||||
|
||||
# get the item definition from the catalog
|
||||
catalog_item = self.catalog.get(capability, None)
|
||||
if catalog_item:
|
||||
if capability_info is None:
|
||||
capability_info = catalog_item.capability_info
|
||||
elif (
|
||||
"values" not in capability_info
|
||||
and "values" in catalog_item.capability_info
|
||||
):
|
||||
capability_info["values"] = catalog_item.capability_info["values"]
|
||||
|
||||
device_class = catalog_item.device_class
|
||||
unit = catalog_item.unit
|
||||
entity_category = catalog_item.entity_category
|
||||
entity_icon = catalog_item.entity_icon
|
||||
|
||||
# override the api determined type by the catalog entity_type
|
||||
if isinstance(device_class, BinarySensorDeviceClass):
|
||||
entity_type = BINARY_SENSOR
|
||||
if isinstance(device_class, ButtonDeviceClass):
|
||||
entity_type = BUTTON
|
||||
if isinstance(device_class, NumberDeviceClass):
|
||||
entity_type = NUMBER
|
||||
if isinstance(device_class, SensorDeviceClass):
|
||||
entity_type = SENSOR
|
||||
if isinstance(device_class, SwitchDeviceClass):
|
||||
entity_type = SWITCH
|
||||
|
||||
# override the api determined type by the catalog entity_platform
|
||||
if catalog_item and isinstance(catalog_item.entity_platform, Platform):
|
||||
entity_type = catalog_item.entity_platform
|
||||
|
||||
_LOGGER.debug(
|
||||
"Electrolux get_entity. entity_type: %s entity_name: %s entity_attr: %s entity_source: %s capability: %s device_class: %s unit: %s, catalog: %s",
|
||||
entity_type,
|
||||
entity_name,
|
||||
entity_attr,
|
||||
category,
|
||||
capability_info,
|
||||
device_class,
|
||||
unit,
|
||||
catalog_item,
|
||||
)
|
||||
|
||||
def electrolux_entity_factory(
|
||||
name: str,
|
||||
entity_type: Platform | None,
|
||||
entity_name: str,
|
||||
entity_attr: str,
|
||||
entity_source: str,
|
||||
capability: str,
|
||||
unit: str,
|
||||
entity_category: str,
|
||||
device_class: str,
|
||||
icon: str,
|
||||
catalog_entry: ElectroluxDevice | None,
|
||||
commands: Any | None = None,
|
||||
):
|
||||
entity_classes = {
|
||||
BINARY_SENSOR: ElectroluxBinarySensor,
|
||||
BUTTON: ElectroluxButton,
|
||||
NUMBER: ElectroluxNumber,
|
||||
SELECT: ElectroluxSelect,
|
||||
SENSOR: ElectroluxSensor,
|
||||
SWITCH: ElectroluxSwitch,
|
||||
}
|
||||
|
||||
entity_class = entity_classes.get(entity_type)
|
||||
|
||||
if entity_class is None:
|
||||
_LOGGER.debug("Unknown entity type %s for %s", entity_type, name)
|
||||
raise ValueError(f"Unknown entity type: {entity_type}")
|
||||
|
||||
entity_params = {
|
||||
"coordinator": self.coordinator,
|
||||
"config_entry": self.coordinator.config_entry,
|
||||
"pnc_id": self.pnc_id,
|
||||
"name": name,
|
||||
"entity_type": entity_type,
|
||||
"entity_name": entity_name,
|
||||
"entity_attr": entity_attr,
|
||||
"entity_source": entity_source,
|
||||
"capability": capability,
|
||||
"unit": unit,
|
||||
"entity_category": entity_category,
|
||||
"device_class": device_class,
|
||||
"icon": icon,
|
||||
"catalog_entry": catalog_entry,
|
||||
}
|
||||
|
||||
if commands is None:
|
||||
return [entity_class(**entity_params)]
|
||||
|
||||
entities: list[
|
||||
ElectroluxBinarySensor
|
||||
| ElectroluxNumber
|
||||
| ElectroluxSensor
|
||||
| ElectroluxSwitch
|
||||
| ElectroluxButton
|
||||
| ElectroluxSelect
|
||||
] = []
|
||||
# Replace entity name and icons for multi-entities attribute (one value = one entity)
|
||||
for command in commands:
|
||||
entity = {**entity_params, "val_to_send": command}
|
||||
if catalog_item:
|
||||
if catalog_item.entity_value_named:
|
||||
entity["name"] = command
|
||||
if (
|
||||
catalog_item.entity_icons_value_map
|
||||
and catalog_item.entity_icons_value_map.get(command, None)
|
||||
):
|
||||
entity["icon"] = catalog_item.entity_icons_value_map.get(
|
||||
command
|
||||
)
|
||||
# Instanciate the new entity and append it
|
||||
entities.append(entity_class(**entity))
|
||||
return entities
|
||||
|
||||
if entity_type in PLATFORMS:
|
||||
commands = (
|
||||
capability_info.get("values", {}) if entity_type == BUTTON else None
|
||||
)
|
||||
return electrolux_entity_factory(
|
||||
name=display_name,
|
||||
entity_type=entity_type,
|
||||
entity_name=entity_name,
|
||||
entity_attr=entity_attr,
|
||||
entity_source=category,
|
||||
capability=capability_info,
|
||||
unit=unit,
|
||||
entity_category=entity_category,
|
||||
device_class=device_class,
|
||||
icon=entity_icon,
|
||||
catalog_entry=catalog_item,
|
||||
commands=commands,
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def setup(self, data: ElectroluxLibraryEntity):
|
||||
"""Configure the entity."""
|
||||
self.data: ElectroluxLibraryEntity = data
|
||||
self.entities: list[ElectroluxEntity] = []
|
||||
entities: list[ElectroluxEntity] = []
|
||||
# Extraction of the appliance capabilities & mapping to the known entities of the component
|
||||
# [ "applianceState", "autoDosing",..., "userSelections/analogTemperature",...]
|
||||
capabilities_names = self.data.sources_list()
|
||||
|
||||
if capabilities_names is None and self.state:
|
||||
# No capabilities returned (unstable API)
|
||||
# We could rebuild them from catalog but this creates entities that are
|
||||
# not required by each device type (fridge, dryer, vacumn etc are all different)
|
||||
_LOGGER.warning("Electrolux API returned no capability definition")
|
||||
|
||||
# Add static attribute
|
||||
# these are attributes that are not in the capability entry
|
||||
# but are returned by the api independantly
|
||||
for static_attribute in STATIC_ATTRIBUTES:
|
||||
_LOGGER.debug("Electrolux static_attribute %s", static_attribute)
|
||||
# attr not found in state, next attr
|
||||
if self.get_state(static_attribute) is None:
|
||||
continue
|
||||
if catalog_item := self.catalog.get(static_attribute, None):
|
||||
if (entity := self.get_entity(static_attribute)) is None:
|
||||
# catalog definition and automatic checks fail to determine type
|
||||
_LOGGER.debug(
|
||||
"Electrolux static_attribute undefined %s", static_attribute
|
||||
)
|
||||
continue
|
||||
# add to the capability dict
|
||||
keys = static_attribute.split("/")
|
||||
capabilities = self.data.capabilities
|
||||
for key in keys[:-1]:
|
||||
capabilities = capabilities.setdefault(key, {})
|
||||
capabilities[keys[-1]] = catalog_item.capability_info
|
||||
_LOGGER.debug("Electrolux adding static_attribute %s", static_attribute)
|
||||
entities.extend(entity)
|
||||
|
||||
# For each capability src
|
||||
if capabilities_names:
|
||||
for capability in capabilities_names:
|
||||
if entity := self.get_entity(capability):
|
||||
entities.extend(entity)
|
||||
else:
|
||||
_LOGGER.debug("Could not create entity for capability %s", capability)
|
||||
|
||||
# Setup each found entity
|
||||
self.entities = entities
|
||||
for entity in entities:
|
||||
entity.setup(data)
|
||||
|
||||
def update_reported_data(self, reported_data: dict[str, Any]):
|
||||
"""Update the reported data."""
|
||||
_LOGGER.debug("Electrolux update reported data %s", reported_data)
|
||||
try:
|
||||
self.reported_state.update(deep_merge_dicts(self.reported_state, reported_data))
|
||||
_LOGGER.debug("Electrolux updated reported data %s", self.state)
|
||||
self.update_missing_entities()
|
||||
for entity in self.entities:
|
||||
entity.update(self.state)
|
||||
|
||||
except Exception as ex: # noqa: BLE001
|
||||
_LOGGER.debug(
|
||||
"Electrolux status could not update reported data with %s. %s",
|
||||
reported_data,
|
||||
ex,
|
||||
)
|
||||
|
||||
def update(self, appliance_status: ApplienceStatusResponse):
|
||||
"""Update appliance status."""
|
||||
self.state = appliance_status
|
||||
self.update_missing_entities()
|
||||
for entity in self.entities:
|
||||
entity.update(self.state)
|
||||
|
||||
|
||||
class Appliances:
|
||||
"""Appliance class definition."""
|
||||
|
||||
def __init__(self, appliances: dict[str, Appliance]) -> None:
|
||||
"""Initialize the class."""
|
||||
self.appliances = appliances
|
||||
|
||||
def get_appliance(self, pnc_id) -> Appliance:
|
||||
"""Return the appliance."""
|
||||
return self.appliances.get(pnc_id, None)
|
||||
|
||||
def get_appliances(self) -> dict[str, Appliance]:
|
||||
"""Return all appliances."""
|
||||
return self.appliances
|
||||
|
||||
def get_appliance_ids(self) -> list[str]:
|
||||
"""Return all appliance ids."""
|
||||
return list(self.appliances)
|
||||
@@ -1,66 +0,0 @@
|
||||
"""Binary sensor platform for Electrolux Status."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import BINARY_SENSOR, DOMAIN
|
||||
from .entity import ElectroluxEntity
|
||||
from .util import string_to_boolean
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure binary sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity
|
||||
for entity in appliance.entities
|
||||
if entity.entity_type == BINARY_SENSOR
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d BINARY_SENSOR entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxBinarySensor(ElectroluxEntity, BinarySensorEntity):
|
||||
"""Electrolux Status binary_sensor class."""
|
||||
|
||||
@property
|
||||
def entity_domain(self):
|
||||
"""Enitity domain for the entry. Used for consistent entity_id."""
|
||||
return BINARY_SENSOR
|
||||
|
||||
@property
|
||||
def invert(self) -> bool:
|
||||
"""Determine if the value returned for the entity needs to be reversed."""
|
||||
if self.catalog_entry:
|
||||
return self.catalog_entry.state_invert
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary_sensor is on."""
|
||||
value = self.extract_value()
|
||||
if isinstance(value, str):
|
||||
value = string_to_boolean(value, True)
|
||||
if value is None:
|
||||
if self.catalog_entry and self.catalog_entry.state_mapping:
|
||||
mapping = self.catalog_entry.state_mapping
|
||||
value = self.get_state_attr(mapping)
|
||||
if value is not None:
|
||||
self._cached_value = value
|
||||
return not self._cached_value if self.invert else self._cached_value
|
||||
@@ -1,140 +0,0 @@
|
||||
"""Button platform for Electrolux Status."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyelectroluxocp.oneAppApi import OneAppApi
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import BUTTON, DOMAIN, icon_mapping
|
||||
from .entity import ElectroluxEntity
|
||||
from .model import ElectroluxDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure button platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity for entity in appliance.entities if entity.entity_type == BUTTON
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d BUTTON entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxButton(ElectroluxEntity, ButtonEntity):
|
||||
"""Electrolux Status button class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
name: str,
|
||||
config_entry,
|
||||
pnc_id: str,
|
||||
entity_type: str,
|
||||
entity_name,
|
||||
entity_attr,
|
||||
entity_source,
|
||||
capability: dict[str, Any],
|
||||
unit: str,
|
||||
device_class: str,
|
||||
entity_category: EntityCategory,
|
||||
icon: str,
|
||||
catalog_entry: ElectroluxDevice | None,
|
||||
val_to_send: str,
|
||||
) -> None:
|
||||
"""Initialize the Button Entity."""
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
capability=capability,
|
||||
name=name,
|
||||
config_entry=config_entry,
|
||||
pnc_id=pnc_id,
|
||||
entity_type=entity_type,
|
||||
entity_name=entity_name,
|
||||
entity_attr=entity_attr,
|
||||
entity_source=entity_source,
|
||||
unit=None,
|
||||
device_class=device_class,
|
||||
entity_category=entity_category,
|
||||
icon=icon,
|
||||
catalog_entry=catalog_entry,
|
||||
)
|
||||
self.val_to_send = val_to_send
|
||||
|
||||
@property
|
||||
def entity_domain(self):
|
||||
"""Enitity domain for the entry. Used for consistent entity_id."""
|
||||
return BUTTON
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID to use for this entity."""
|
||||
return f"{self.config_entry.entry_id}-{self.val_to_send}-{self.entity_attr}-{self.entity_source}-{self.pnc_id}"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
name = self._name
|
||||
if self.catalog_entry and self.catalog_entry.friendly_name:
|
||||
name = (
|
||||
f"{self.get_appliance.name} {self.catalog_entry.friendly_name.lower()}"
|
||||
)
|
||||
# Get the last word from the 'name' variable
|
||||
# and compare to the command we are sending duplicate names
|
||||
# "air filter state reset reset" for instance
|
||||
last_word = name.split()[-1]
|
||||
if last_word.lower() == str(self.val_to_send).lower():
|
||||
return name
|
||||
return f"{name} {self.val_to_send}"
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon of the entity."""
|
||||
return self._icon or icon_mapping.get(
|
||||
self.val_to_send, "mdi:gesture-tap-button"
|
||||
)
|
||||
|
||||
async def send_command(self) -> bool:
|
||||
"""Send a command to the device."""
|
||||
client: OneAppApi = self.api
|
||||
value = self.val_to_send
|
||||
if self.entity_source:
|
||||
if self.entity_source == "userSelections":
|
||||
command = {
|
||||
self.entity_source: {
|
||||
"programUID": self.appliance_status["properties"]['reported']["userSelections"]['programUID'],
|
||||
self.entity_attr: value
|
||||
},
|
||||
}
|
||||
else:
|
||||
command = {self.entity_source: {self.entity_attr: value}}
|
||||
else:
|
||||
command = {self.entity_attr: value}
|
||||
_LOGGER.debug("Electrolux send command %s", command)
|
||||
result = await client.execute_appliance_command(self.pnc_id, command)
|
||||
_LOGGER.debug("Electrolux send command result %s", result)
|
||||
return True
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute a button press."""
|
||||
await self.send_command()
|
||||
# await self.hass.async_add_executor_job(self.send_command)
|
||||
# if self.entity_attr == "ExecuteCommand":
|
||||
# await self.hass.async_add_executor_job(self.coordinator.api.setHacl, self.get_appliance.pnc_id, "0x0403", self.val_to_send, self.entity_source)
|
||||
@@ -1,237 +0,0 @@
|
||||
"""Adds config flow for Electrolux Status."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
CONN_CLASS_CLOUD_PUSH,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_COUNTRY_CODE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
selector,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_LANGUAGE,
|
||||
CONF_NOTIFICATION_DEFAULT,
|
||||
CONF_NOTIFICATION_DIAG,
|
||||
CONF_NOTIFICATION_WARNING,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_COUNTRY_CODE,
|
||||
DOMAIN,
|
||||
languages,
|
||||
)
|
||||
from .util import get_electrolux_session
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElectroluxStatusFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Electrolux Status."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = CONN_CLASS_CLOUD_PUSH
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._errors = {}
|
||||
|
||||
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
self._errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# check if the specified account is configured already
|
||||
# to prevent them from being added twice
|
||||
for entry in self._async_current_entries():
|
||||
if user_input[CONF_USERNAME] == entry.data.get("username", None):
|
||||
return self.async_abort(reason="already_configured_account")
|
||||
|
||||
valid = await self._test_credentials(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_COUNTRY_CODE]
|
||||
)
|
||||
if valid:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
)
|
||||
self._errors["base"] = "invalid_auth"
|
||||
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
return await self.async_step_reauth_validate(entry_data)
|
||||
|
||||
async def async_step_reauth_validate(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle reauth and validation."""
|
||||
self._errors = {}
|
||||
if user_input is not None:
|
||||
valid = await self._test_credentials(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_COUNTRY_CODE]
|
||||
)
|
||||
if valid:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
)
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return await self._show_config_form(user_input)
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Present the configuration options dialog."""
|
||||
return ElectroluxStatusOptionsFlowHandler(config_entry)
|
||||
|
||||
async def _show_config_form(self, user_input): # pylint: disable=unused-argument
|
||||
"""Show the configuration form to edit location data."""
|
||||
data_schema = {
|
||||
vol.Required(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.EMAIL, autocomplete="username")
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD, autocomplete="current-password"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT, autocomplete="country-code"
|
||||
)
|
||||
),
|
||||
}
|
||||
if self.show_advanced_options:
|
||||
data_schema.update(
|
||||
{
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): selector(
|
||||
{
|
||||
"select": {
|
||||
"options": list(languages.keys()),
|
||||
"mode": "dropdown",
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Optional(CONF_NOTIFICATION_DEFAULT, default=True): cv.boolean,
|
||||
vol.Optional(CONF_NOTIFICATION_WARNING, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NOTIFICATION_DIAG, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(data_schema),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def _test_credentials(self, username, password, country_code):
|
||||
"""Return true if credentials is valid."""
|
||||
try:
|
||||
client = get_electrolux_session(
|
||||
username, password, country_code, async_get_clientsession(self.hass)
|
||||
)
|
||||
await client.get_appliances_list()
|
||||
except Exception as inst: # pylint: disable=broad-except # noqa: BLE001
|
||||
_LOGGER.error("Authentication to electrolux failed: %s", inst)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ElectroluxStatusOptionsFlowHandler(OptionsFlow):
|
||||
"""Config flow options handler for Electrolux Status."""
|
||||
|
||||
def __init__(self, config_entry) -> None:
|
||||
"""Initialize HACS options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(config_entry.options)
|
||||
|
||||
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
return await self._update_options()
|
||||
|
||||
selected_language = self.config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE)
|
||||
current_password = self.config_entry.data.get(CONF_PASSWORD, None)
|
||||
current_country_code = self.config_entry.data.get(CONF_COUNTRY_CODE, None)
|
||||
notify_alert = self.config_entry.data.get(CONF_NOTIFICATION_DEFAULT, True)
|
||||
notify_warning = self.config_entry.data.get(CONF_NOTIFICATION_WARNING, False)
|
||||
notify_diagnostic = self.config_entry.data.get(CONF_NOTIFICATION_DIAG, False)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD, default=current_password): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_COUNTRY_CODE, default=current_country_code): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT, autocomplete="country-code"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_LANGUAGE, default=selected_language): selector(
|
||||
{
|
||||
"select": {
|
||||
"options": list(languages.keys()),
|
||||
"mode": "dropdown",
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_NOTIFICATION_DEFAULT, default=notify_alert
|
||||
): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_NOTIFICATION_WARNING, default=notify_warning
|
||||
): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_NOTIFICATION_DIAG, default=notify_diagnostic
|
||||
): cv.boolean,
|
||||
# vol.Optional(
|
||||
# CONF_RENEW_INTERVAL,
|
||||
# default=self.config_entry.options.get(
|
||||
# CONF_RENEW_INTERVAL, DEFAULT_WEBSOCKET_RENEWAL_DELAY
|
||||
# ),
|
||||
# ): cv.positive_int,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_options(self):
|
||||
"""Update config entry options."""
|
||||
_LOGGER.debug("Electrolux updating configuration")
|
||||
data = {
|
||||
**self.config_entry.data,
|
||||
CONF_PASSWORD: self.options[CONF_PASSWORD],
|
||||
CONF_COUNTRY_CODE: self.options[CONF_COUNTRY_CODE],
|
||||
CONF_LANGUAGE: self.options[CONF_LANGUAGE],
|
||||
CONF_NOTIFICATION_DEFAULT: self.options[CONF_NOTIFICATION_DEFAULT],
|
||||
CONF_NOTIFICATION_WARNING: self.options[CONF_NOTIFICATION_WARNING],
|
||||
CONF_NOTIFICATION_DIAG: self.options[CONF_NOTIFICATION_DIAG],
|
||||
}
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||
return self.async_create_entry(
|
||||
title=self.config_entry.data.get(CONF_USERNAME),
|
||||
data=data,
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
"""The electrolux Status constants."""
|
||||
|
||||
import re
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
# Base component constants
|
||||
NAME = "Electrolux status"
|
||||
DOMAIN = "electrolux_status"
|
||||
DOMAIN_DATA = f"{DOMAIN}_data"
|
||||
COMPONENTS_DIRECTORY = "custom_components"
|
||||
LOOKUP_DIRECTORY = "appliance_definitions"
|
||||
LOOKUP_DIRECTORY_PATH = f"{COMPONENTS_DIRECTORY}/{DOMAIN}/{LOOKUP_DIRECTORY}/"
|
||||
|
||||
# Platforms
|
||||
BINARY_SENSOR = Platform.BINARY_SENSOR
|
||||
BUTTON = Platform.BUTTON
|
||||
NUMBER = Platform.NUMBER
|
||||
SELECT = Platform.SELECT
|
||||
SENSOR = Platform.SENSOR
|
||||
SWITCH = Platform.SWITCH
|
||||
PLATFORMS = [BINARY_SENSOR, BUTTON, NUMBER, SELECT, SENSOR, SWITCH]
|
||||
|
||||
# Configuration and options
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_RENEW_INTERVAL = "renew_interval"
|
||||
CONF_NOTIFICATION_DEFAULT = "notifications"
|
||||
CONF_NOTIFICATION_DIAG = "notifications_diagnostic"
|
||||
CONF_NOTIFICATION_WARNING = "notifications_warning"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_LANGUAGE = "English"
|
||||
DEFAULT_COUNTRY_CODE = "us"
|
||||
DEFAULT_WEBSOCKET_RENEWAL_DELAY = 43200 # 12 hours
|
||||
|
||||
# these are attributes that appear in the state file but not in the capabilities.
|
||||
# defining them here and in the catalog will allow these devices to be added dynamically
|
||||
STATIC_ATTRIBUTES = [
|
||||
"connectivityState",
|
||||
"networkInterface/linkQualityIndicator",
|
||||
"applianceMode",
|
||||
]
|
||||
|
||||
# Icon mappings for default executeCommands
|
||||
icon_mapping = {
|
||||
"OFF": "mdi:power-off",
|
||||
"ON": "mdi:power-on",
|
||||
"START": "mdi:play",
|
||||
"STOPRESET": "mdi:stop",
|
||||
"PAUSE": "mdi:pause",
|
||||
"RESUME": "mdi:play-pause",
|
||||
}
|
||||
|
||||
# List of supported Mobile App languages
|
||||
# refer to https://emea-production.api.electrolux.net/masterdata-service/api/v1/languages
|
||||
languages = {
|
||||
"български": "bul",
|
||||
"český": "ces",
|
||||
"Dansk": "dan",
|
||||
"Deutsch": "deu",
|
||||
"ελληνικός": "ell",
|
||||
"English": "eng",
|
||||
"eesti": "est",
|
||||
"Soome": "fin",
|
||||
"Français": "fra",
|
||||
"Hrvatski": "hrv",
|
||||
"magyar": "hun",
|
||||
"Italiano": "ita",
|
||||
"lettone": "lav",
|
||||
"lituano": "lit",
|
||||
"Luxembourgish": "ltz",
|
||||
"nederlands": "nld",
|
||||
"Norsk": "nor",
|
||||
"Polski": "pol",
|
||||
"Português": "por",
|
||||
"Română": "ron",
|
||||
"rusesc": "rus",
|
||||
"slovenský": "slk",
|
||||
"slovinský": "slv",
|
||||
"Español": "spa",
|
||||
"Svenska": "swe",
|
||||
"Türk": "tur",
|
||||
"Ukrayna": "ukr",
|
||||
}
|
||||
|
||||
# List of attributes to ignore and that won't be added as entities (regex format)
|
||||
ATTRIBUTES_BLACKLIST: list[str] = ["^fCMiscellaneous.+",
|
||||
"fcOptisenseLoadWeight.*",
|
||||
"applianceCareAndMaintenance.*",
|
||||
"applianceMainBoardSwVersion",
|
||||
"coolingValveState",
|
||||
"networkInterface",
|
||||
"temperatureRepresentation",
|
||||
]
|
||||
|
||||
ATTRIBUTES_WHITELIST: list[str] = [".*waterUsage",
|
||||
".*tankAReserve",
|
||||
".*tankBReserve"]
|
||||
|
||||
# Rules to simplify the naming of entities
|
||||
RENAME_RULES: list[str] = [r"^userSelections\/[^_]+_", r"^userSelections\/",
|
||||
r"^fCMiscellaneousState\/[^_]+_", r"^fCMiscellaneousState\/"]
|
||||
|
||||
# List of entity names that need to be updated to 0 manually when they are close to 0
|
||||
TIME_ENTITIES_TO_UPDATE = ["timeToEnd"]
|
||||
@@ -1,249 +0,0 @@
|
||||
"""Entity platform for Electrolux Status."""
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from pyelectroluxocp import OneAppApi
|
||||
from pyelectroluxocp.apiModels import ApplienceStatusResponse
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .model import ElectroluxDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure entity platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity
|
||||
for entity in appliance.entities
|
||||
if entity.entity_type == "entity"
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxEntity(CoordinatorEntity):
|
||||
"""Class for Electorolux devices."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
appliance_status: ApplienceStatusResponse
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
name: str,
|
||||
config_entry,
|
||||
pnc_id: str,
|
||||
entity_type: Platform,
|
||||
entity_name,
|
||||
entity_attr,
|
||||
entity_source,
|
||||
capability: dict[str, Any],
|
||||
unit: str,
|
||||
device_class: str,
|
||||
entity_category: EntityCategory,
|
||||
icon: str,
|
||||
catalog_entry: ElectroluxDevice | None = None,
|
||||
) -> None:
|
||||
"""Initaliaze the entity."""
|
||||
super().__init__(coordinator)
|
||||
self.root_attribute = ["properties", "reported"]
|
||||
self.data = None
|
||||
self.coordinator = coordinator
|
||||
self._cached_value = None
|
||||
self._name = name
|
||||
self._icon = icon
|
||||
self._device_class = device_class
|
||||
self._entity_category = entity_category
|
||||
self._catalog_entry = catalog_entry
|
||||
self.api: OneAppApi = coordinator.api
|
||||
self.entity_name = entity_name
|
||||
self.entity_attr = entity_attr
|
||||
self.entity_type = entity_type
|
||||
self.entity_source = entity_source
|
||||
self.config_entry = config_entry
|
||||
self.pnc_id = pnc_id
|
||||
self.unit = unit
|
||||
self.capability = capability
|
||||
self.entity_id = f"{self.entity_domain}.{self.get_appliance.brand}_{self.get_appliance.name}_{self.entity_source}_{self.entity_attr}"
|
||||
if catalog_entry:
|
||||
self.entity_registry_enabled_default = (
|
||||
catalog_entry.entity_registry_enabled_default
|
||||
)
|
||||
_LOGGER.debug("Electrolux new entity %s for appliance %s", name, pnc_id)
|
||||
|
||||
def setup(self, data):
|
||||
"""Initialiaze setup."""
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def entity_domain(self) -> str:
|
||||
"""Enitity domain for the entry."""
|
||||
return "sensor"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID to use for this entity."""
|
||||
return f"{self.config_entry.entry_id}-{self.entity_attr}-{self.entity_source or 'root'}-{self.pnc_id}"
|
||||
|
||||
# Disabled this as this removes the value from display : there is no readonly property for entities
|
||||
# @property
|
||||
# def available(self) -> bool:
|
||||
# if (self._entity_category == EntityCategory.DIAGNOSTIC
|
||||
# or self.entity_attr in ALWAYS_ENABLED_ATTRIBUTES):
|
||||
# return True
|
||||
# connection_state = self.get_connection_state()
|
||||
# if connection_state and connection_state != "disconnected":
|
||||
# return True
|
||||
# return False
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Confirm if device should be polled."""
|
||||
return False
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
# _LOGGER.debug("Electrolux entity got data %s", self.coordinator.data)
|
||||
if self.coordinator.data is None:
|
||||
return
|
||||
appliances = self.coordinator.data.get("appliances", None)
|
||||
self.appliance_status = appliances.get_appliance(self.pnc_id).state
|
||||
self.async_write_ha_state()
|
||||
|
||||
def get_connection_state(self) -> str | None:
|
||||
"""Return connection state."""
|
||||
if self.appliance_status:
|
||||
return self.appliance_status.get("connectionState", None)
|
||||
return None
|
||||
|
||||
def get_state_attr(self, path: str) -> str | None:
|
||||
"""Return value of other appliance attributes.
|
||||
|
||||
Used for the evaluation of state_mapping one property to another.
|
||||
"""
|
||||
if "/" in path:
|
||||
if self.reported_state.get(path, None):
|
||||
return self.reported_state.get(path)
|
||||
source, attr = path.split("/")
|
||||
return self.reported_state.get(source, {}).get(attr, None)
|
||||
return self.reported_state.get(path, None)
|
||||
|
||||
@property
|
||||
def reported_state(self):
|
||||
"""Return reported state of the appliance."""
|
||||
return self.appliance_status.get("properties", {}).get("reported", {})
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
if self.catalog_entry and self.catalog_entry.friendly_name:
|
||||
return self.catalog_entry.friendly_name.capitalize()
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon of the entity."""
|
||||
return self._icon
|
||||
|
||||
# @property
|
||||
# def get_entity(self) -> ApplianceEntity:
|
||||
# return self.get_appliance.get_entity(self.entity_type, self.entity_attr, self.entity_source, None)
|
||||
|
||||
@property
|
||||
def get_appliance(self):
|
||||
"""Return the appliance device."""
|
||||
return self.coordinator.data["appliances"].get_appliance(self.pnc_id)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return identifiers of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.pnc_id)},
|
||||
"name": self.get_appliance.name,
|
||||
"model": self.get_appliance.model,
|
||||
"manufacturer": self.get_appliance.brand,
|
||||
}
|
||||
|
||||
@property
|
||||
def entity_category(self) -> EntityCategory | None:
|
||||
"""Return entity category."""
|
||||
return self._entity_category
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
def extract_value(self):
|
||||
"""Return the appliance attributes of the entity."""
|
||||
root_attribute = self.root_attribute
|
||||
attribute = self.entity_attr
|
||||
if self.appliance_status:
|
||||
root = cast(any, self.appliance_status)
|
||||
# Format returned by push is slightly different from format returned by API : fields are at root level
|
||||
# Let's check if we can find the fields at root first
|
||||
if (
|
||||
self.entity_source and root.get(self.entity_source, None) is not None
|
||||
) or root.get(attribute, None):
|
||||
root_attribute = None
|
||||
if root_attribute:
|
||||
for item in root_attribute:
|
||||
if root:
|
||||
root = root.get(item, None)
|
||||
if root:
|
||||
if self.entity_source:
|
||||
category: dict[str, Any] | None = root.get(self.entity_source, None)
|
||||
if category:
|
||||
return category.get(attribute)
|
||||
else:
|
||||
return root.get(attribute, None)
|
||||
return None
|
||||
|
||||
def update(self, appliance_status: ApplienceStatusResponse):
|
||||
"""Update the appliance status."""
|
||||
self.appliance_status = appliance_status
|
||||
# if self.hass:
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def json_path(self) -> str | None:
|
||||
"""Return the path to the entry."""
|
||||
if self.entity_source:
|
||||
return f"{self.entity_source}/{self.entity_attr}"
|
||||
return self.entity_attr
|
||||
|
||||
@property
|
||||
def catalog_entry(self) -> ElectroluxDevice | None:
|
||||
"""Return matched catalog entry."""
|
||||
return self._catalog_entry
|
||||
|
||||
# @property
|
||||
# def extra_state_attributes(self) -> dict[str, Any]:
|
||||
# """Return the state attributes of the sensor."""
|
||||
# return {
|
||||
# "Path": self.json_path,
|
||||
# "entity_type": str(self.entity_type),
|
||||
# "entity_category": str(self.entity_category),
|
||||
# "device_class": str(self.device_class),
|
||||
# "capability": str(self.capability),
|
||||
# }
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"domain": "electrolux_status",
|
||||
"name": "Electrolux Status",
|
||||
"codeowners": ["@albaintor","@kingy444"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"homekit": {},
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"issue_tracker": "https://github.com/albaintor/homeassistant_electrolux_status/issues",
|
||||
"loggers": ["pyelectroluxocp"],
|
||||
"requirements": [
|
||||
"pyelectroluxocp==0.1.3",
|
||||
"aiofiles==24.1.0"
|
||||
],
|
||||
"ssdp": [],
|
||||
"version": "2.2.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
"""Switch platform for Electrolux Status."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, SENSOR
|
||||
from .entity import ElectroluxEntity
|
||||
from .util import create_notification
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity for entity in appliance.entities if entity.entity_type == SENSOR
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d SENSOR entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxSensor(ElectroluxEntity, SensorEntity):
|
||||
"""Electrolux Status Sensor class."""
|
||||
|
||||
@property
|
||||
def entity_domain(self):
|
||||
"""Enitity domain for the entry. Used for consistent entity_id."""
|
||||
return SENSOR
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Get the display precision."""
|
||||
if self.unit == UnitOfTemperature.CELSIUS:
|
||||
return 2
|
||||
if self.unit == UnitOfTemperature.FAHRENHEIT:
|
||||
return 2
|
||||
if self.unit == UnitOfVolume.LITERS:
|
||||
return 0
|
||||
if self.unit == UnitOfTime.SECONDS:
|
||||
return 0
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | float:
|
||||
"""Return the state of the sensor."""
|
||||
value = self.extract_value()
|
||||
if self.capability.get("access") == "constant":
|
||||
value = self.capability.get("default")
|
||||
elif self.entity_attr == "alerts":
|
||||
value = len(value) if value is not None else 0
|
||||
elif value is not None and isinstance(self.unit, UnitOfTime):
|
||||
# Electrolux bug - prevent negative/disabled timers
|
||||
value = max(value, 0)
|
||||
if self.catalog_entry and self.catalog_entry.value_mapping:
|
||||
# Electrolux presents as string but returns an int
|
||||
# the mapping entry allows us to correctly display this to the frontend
|
||||
mapping = self.catalog_entry.value_mapping
|
||||
_LOGGER.debug("Mapping %s: %s to %s", self.json_path, value, mapping)
|
||||
if value in mapping:
|
||||
value = mapping.get(value, value)
|
||||
if isinstance(value, str):
|
||||
if "_" in value:
|
||||
value = value.replace("_", " ")
|
||||
value = value.title()
|
||||
if value is not None:
|
||||
self._cached_value = value
|
||||
else:
|
||||
value = self._cached_value
|
||||
return value
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return unit of measurement."""
|
||||
# store the value of the sensor in the native format
|
||||
return self.unit
|
||||
|
||||
@property
|
||||
def suggested_unit_of_measurement(self) -> str | None:
|
||||
"""Return suggested unit of measurement."""
|
||||
# change the display unit in the HA frontend
|
||||
if self.unit == UnitOfTime.SECONDS:
|
||||
return UnitOfTime.MINUTES
|
||||
with contextlib.suppress(ValueError):
|
||||
if self._is_valid_suggested_unit(self.unit):
|
||||
return self.unit
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the sensor."""
|
||||
if self.entity_attr == "alerts":
|
||||
alert_types = self.capability.get("values", {})
|
||||
# default is nullable - set a value for display to user
|
||||
alert_types = {key: "OFF" for key in alert_types}
|
||||
if current_alerts := self.extract_value():
|
||||
for alert in current_alerts:
|
||||
name = alert.get("code", "Unknown")
|
||||
severity = alert.get("severity", "Alert")
|
||||
status = alert.get("acknowledgeStatus", "")
|
||||
alert_types[name] = f"{severity}-{status}"
|
||||
create_notification(
|
||||
self.hass,
|
||||
self.config_entry,
|
||||
alert_name=name,
|
||||
alert_severity=severity,
|
||||
alert_status=status,
|
||||
title=self.name,
|
||||
)
|
||||
return alert_types
|
||||
return {}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_configured_account": "Account is already configured",
|
||||
"reconfigure_successful": "Re-configuration was successful",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid authentication"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Language",
|
||||
"notifications": "Raise notifications for ALERT level notices",
|
||||
"notifications_diagnostic": "Raise notifications for DIAGNOSTIC level notices",
|
||||
"notifications_warning": "Raise notifications for WARNING level notices",
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "If you need help with the configuration visit: https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"title": "Electrolux Status"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Language",
|
||||
"notifications": "Raise notifications for ALERT level notices",
|
||||
"notifications_diagnostic": "Raise notifications for DIAGNOSTIC level notices",
|
||||
"notifications_warning": "Raise notifications for WARNING level notices",
|
||||
"password": "Password",
|
||||
"renew_interval": "Renewal interval of websocket (seconds)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo è già configurato",
|
||||
"already_configured_account": "L'account è già configurato",
|
||||
"reconfigure_successful": "La riconfigurazione ha avuto successo",
|
||||
"single_instance_allowed": "Già configurato.Solo una singola configurazione possibile."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Autenticazione non valida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Lingua",
|
||||
"notifications": "Aumenta le notifiche per gli avvisi di livello di allerta",
|
||||
"notifications_diagnostic": "Aumenta le notifiche per gli avvisi a livello diagnostico",
|
||||
"notifications_warning": "Aumenta le notifiche per gli avvisi di livello di avvertimento",
|
||||
"password": "Password",
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"description": "Se hai bisogno di aiuto con la visita di configurazione: https://github.com/albaintor/homeeasistant_electrolux_status",
|
||||
"title": "Stato Electrolux"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Lingua",
|
||||
"notifications": "Aumenta le notifiche per gli avvisi di livello di allerta",
|
||||
"notifications_diagnostic": "Aumenta le notifiche per gli avvisi a livello diagnostico",
|
||||
"notifications_warning": "Aumenta le notifiche per gli avvisi di livello di avvertimento",
|
||||
"password": "Password",
|
||||
"renew_interval": "Intervallo di rinnovo di WebSocket (secondi)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Urządzenie jest już skonfigurowane",
|
||||
"already_configured_account": "Konto jest już skonfigurowane",
|
||||
"reconfigure_successful": "Ponowna konfiguracja zakończyła się powodzeniem",
|
||||
"single_instance_allowed": "Już skonfigurowano. Możliwa tylko jedna konfiguracja."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Nieprawidłowe uwierzytelnienie"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Język",
|
||||
"notifications": "Pokazuj alerty",
|
||||
"notifications_diagnostic": "Pokazuj powoadomienia diagnostyczne",
|
||||
"notifications_warning": "Pokazuj powiadomienia ostrzegawcze",
|
||||
"password": "Hasło",
|
||||
"username": "Nazwa użytkownika"
|
||||
},
|
||||
"description": "Jeśli potrzebujesz pomocy w konfiguracji, odwiedź: https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"title": "Electrolux Status"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Język",
|
||||
"notifications": "Pokazuj alerty",
|
||||
"notifications_diagnostic": "Pokazuj powoadomienia diagnostyczne",
|
||||
"notifications_warning": "Pokazuj powiadomienia ostrzegawcze",
|
||||
"password": "Hasło",
|
||||
"renew_interval": "Okres odnawiania połączenia WebSocket (sekundy)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované",
|
||||
"already_configured_account": "Účet je už nakonfigurovaný",
|
||||
"reconfigure_successful": "Opätovná konfigurácia bola úspešná",
|
||||
"single_instance_allowed": "Už nakonfigurované.Iba jedna možná konfigurácia."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Neplatné overovanie"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Jazyk",
|
||||
"notifications": "Zvýšiť oznámenia o upozorneniach na úrovni výstrahy",
|
||||
"notifications_diagnostic": "Zvýšte upozornenia pre oznámenia o diagnostickej úrovni",
|
||||
"notifications_warning": "Zvýšte oznámenia o upozorneniach na úrovni varovania",
|
||||
"password": "Heslo",
|
||||
"username": "Užívateľské meno"
|
||||
},
|
||||
"description": "Ak potrebujete pomoc s konfiguráciou, navštívte: https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"title": "Status"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Jazyk",
|
||||
"notifications": "Zvýšiť oznámenia o upozorneniach na úrovni výstrahy",
|
||||
"notifications_diagnostic": "Zvýšte upozornenia pre oznámenia o diagnostickej úrovni",
|
||||
"notifications_warning": "Zvýšte oznámenia o upozorneniach na úrovni varovania",
|
||||
"password": "Heslo",
|
||||
"renew_interval": "Interval obnovy WebSocket (sekundy)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
synthwave:
|
||||
# text
|
||||
primary-text-color: '#fff'
|
||||
secondary-text-color: '#ffffffca'
|
||||
text-primary-color: '#f4eee4'
|
||||
disabled-text-color: '#bdbdbd'
|
||||
text-light-primary-color: '#fff'
|
||||
|
||||
# main interface colors
|
||||
primary-color: '#f92aad'
|
||||
primary-background-color: '#2a2139'
|
||||
dark-primary-color: '#f92aad'
|
||||
light-primary-color: '#241b2f'
|
||||
accent-color: '#f92aad'
|
||||
divider-color: '#49549539'
|
||||
paper-dialog-button-color: '#fff'
|
||||
switch-unchecked-button-color: '#fff'
|
||||
outline-color: 'rgba(255, 255, 255, 0.12)'
|
||||
outline-over-color: 'rgba(255, 255, 255, 0.24)'
|
||||
# iron-icon-fill-color: '#fff'
|
||||
yellow: '#ffcc00'
|
||||
green: '#72f1b8cc'
|
||||
|
||||
###
|
||||
|
||||
# background and sidebar
|
||||
card-background-color: '#34294f'
|
||||
app-header-background-color: 'var(--primary-background-color)'
|
||||
paper-card-background-color: 'var(--card-background-color)'
|
||||
secondary-background-color: 'var(--light-primary-color)' # behind the cards on state
|
||||
ha-card-border-radius: '8px'
|
||||
|
||||
# sidebar menu
|
||||
sidebar-text-color: 'var(--secondary-text-color)'
|
||||
# sidebar-background-color: 'var(--paper-listbox-background-color)' # backward compatible with existing themes
|
||||
sidebar-icon-color: 'var(--secondary-text-color)'
|
||||
sidebar-selected-text-color: 'var(--primary-text-color)'
|
||||
sidebar-selected-icon-color: 'var(--primary-text-color)'
|
||||
|
||||
# mwc - for some reason it's buttons
|
||||
mdc-theme-primary: 'var(--dark-primary-color)'
|
||||
mdc-theme-secondary: 'var(--dark-primary-color)'
|
||||
|
||||
# shadows
|
||||
ha-card-box-shadow: '0'
|
||||
|
||||
# icons
|
||||
paper-item-icon-color: 'var(--secondary-text-color)' # Off
|
||||
paper-item-icon-active-color: 'var(--yellow)' # On
|
||||
|
||||
# switches
|
||||
toggle-button-color: 'var(--primary-color)'
|
||||
# --toggle-button-unchecked-color: 'var(--accent-color)'
|
||||
paper-toggle-button-checked-button-color: 'var(--primary-text-color)' # Knob On
|
||||
paper-toggle-button-checked-bar-color: 'var(--dark-primary-color)' # Background On
|
||||
switch-checked-color: 'var(--dark-primary-color)' # Background On
|
||||
paper-toggle-button-unchecked-button-color: 'var(--primary-text-color)' # Knob Off
|
||||
paper-toggle-button-unchecked-bar-color: 'var(--disabled-text-color)' # Background Off
|
||||
|
||||
# Sliders
|
||||
slider-color: 'var(--primary-color)'
|
||||
slider-secondary-color: 'var(--light-primary-color)'
|
||||
slider-bar-color: 'var(--disabled-text-color)'
|
||||
paper-slider-knob-color: 'var(--accent-color)'
|
||||
paper-slider-knob-start-color: 'var(--accent-color)'
|
||||
paper-slider-pin-color: 'var(--accent-color)'
|
||||
paper-slider-active-color: 'var(--dark-primary-color)'
|
||||
# paper-slider-container-color: 'linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat'
|
||||
paper-slider-secondary-color: 'var(--secondary-background-color)'
|
||||
paper-slider-disabled-active-color: 'var(--disabled-text-color)'
|
||||
paper-slider-disabled-secondary-color: 'var(--disabled-text-color)'
|
||||
switch-unchecked-track-color: 'var(--primary-text-color)'
|
||||
|
||||
# radio buttons
|
||||
paper-radio-button-checked-color: 'var(--paper-toggle-button-checked-button-color)'
|
||||
paper-radio-button-unchecked-color: 'var(--paper-toggle-button-unchecked-button-color)'
|
||||
|
||||
# other
|
||||
state-icon-color: 'var(--green)'
|
||||
table-row-background-color: 'var(--divider-color)'
|
||||
table-row-alternative-background-color: 'var(--light-primary-color)'
|
||||
|
||||
# Inputs
|
||||
mdc-select-fill-color: 'var(--card-background-color)'
|
||||
mdc-select-ink-color: 'var(--primary-text-color)'
|
||||
mdc-select-label-ink-color: 'var(--primary-text-color)'
|
||||
mdc-select-dropdown-icon-color: 'var(--primary-text-color)'
|
||||
mdc-text-field-label-ink-color: 'var(--primary-text-color)'
|
||||
mdc-text-field-ink-color: 'var(--primary-text-color)'
|
||||
mdc-text-field-fill-color: 'var(--card-background-color)'
|
||||
input-dropdown-icon-color: 'var(--primary-text-color)'
|
||||
# Inputs Ripple/line
|
||||
mdc-ripple-color: 'var(--primary-text-color)'
|
||||
mdc-text-field-idle-line-color: 'var(--primary-text-color)'
|
||||
mdc-text-field-hover-line-color: 'var(--primary-text-color)'
|
||||
mdc-select-idle-line-color: 'var(--primary-text-color)'
|
||||
mdc-select-hover-line-color: 'var(--primary-text-color)'
|
||||
# Inputs Disabled
|
||||
mdc-select-disabled-fill-color: 'var(--primary-background-color)'
|
||||
mdc-select-disabled-ink-color: 'var(--disabled-text-color)'
|
||||
mdc-select-disabled-dropdown-icon-color: 'var(--disabled-text-color)'
|
||||
mdc-text-field-disabled-fill-color: 'var(--primary-background-color)'
|
||||
mdc-text-field-disabled-ink-color: 'var(--disabled-text-color)'
|
||||
mdc-text-field-disabled-dropdown-icon-color: 'var(--disabled-text-color)'
|
||||
|
||||
# Template Editor
|
||||
codemirror-keyword: '#36f9f6'
|
||||
codemirror-operator: '#36f9f6'
|
||||
codemirror-variable: '#ff7edb'
|
||||
codemirror-variable-2: '#ff7edb'
|
||||
codemirror-variable-3: '#ff7edb'
|
||||
codemirror-builtin: '#36f9f6'
|
||||
codemirror-atom: '#36f9f6'
|
||||
codemirror-number: '#f97e72'
|
||||
codemirror-def: '#fe4450'
|
||||
codemirror-string: '#ff8b39'
|
||||
codemirror-string-2: '#ff8b39'
|
||||
codemirror-comment: '#848bbd'
|
||||
codemirror-tag: '#72f1b8'
|
||||
codemirror-meta: '#36f9f6'
|
||||
codemirror-attribute: '#fede5d'
|
||||
codemirror-qualifier: '#fe4450'
|
||||
codemirror-property: 'var(--primary-color)'
|
||||
codemirror-type: '#fe4450'
|
||||
|
||||
###
|
||||
|
||||
# UI
|
||||
paper-card-header-color: 'var(--text-primary-color)' # Title in settings
|
||||
|
||||
# Left Menu
|
||||
paper-listbox-background-color: 'var(--light-primary-color)' # Background
|
||||
sidebar-background-color: 'var(--light-primary-color)'
|
||||
|
||||
# bar-card compatibility
|
||||
# https://github.com/custom-cards/bar-card
|
||||
custom-bar-card-color: 'var(--accent-color)'
|
||||
|
||||
# fix dropdown background
|
||||
material-background-color: 'var(--light-primary-color)'
|
||||
|
||||
# Scrollbar
|
||||
scrollbar-thumb-color: 'var(--divider-color)'
|
||||
|
||||
# simple-thermostat buttons
|
||||
# https://github.com/nervetattoo/simple-thermostat
|
||||
st-mode-background: 'var(--primary-background-color)'
|
||||
st-mode-active-background: 'var(--dark-primary-color)'
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Reference in New Issue
Block a user