From 91d68e20929316721500a716dc4872443e7c8739 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Thu, 10 Aug 2023 19:46:02 +0200 Subject: [PATCH 01/17] Major refactor * Refactor to use the common HA approach for entities * Make it much simpler to make new entities * Add ZaptecBaseEntity for heavy lifting * Added ZaptecUpdateCoordinator to centrally manage API updates * Added new protocols: binary sensor, button, number in addition to sensor and switch * Added support for "download diagnostics" * Add unique id on component * General: Added type hints User changes: * Entity naming is completely changed. Entity names are read from Zaptec Portal. * Entities are grouped into devices and devices reflect structure * Remove services "start_charging" and "stop_charging" (not available in HW) * Add services "authorize_charging" and "deauthorize_charging" * Many new entities/sensors/buttons instead of relying on attrs and templates * Services also available as buttons which doesn't require `charger_id` API changes: * Made ZaptecBase an ABC base class * Cleaned up ZaptecBase.set_attributes() * Fixed infinite authentication failed loop * Cleaned up stream message reception * Fixed error handling in stream reception * Added MC-NBFX data decoder for stream messages * Set all Zaptec API facing methods as private methods * Provide proper exception on fails * Structurize build() and state() for init and updating * Added properties (e.g. `Charger.operating_mode`) that translates Zaptec API internal numbers to strings --- .gitignore | 3 + LICENSE | 1 + custom_components/zaptec/__init__.py | 351 +++++-- custom_components/zaptec/api.py | 993 ++++++++++-------- custom_components/zaptec/binary_sensor.py | 120 +++ custom_components/zaptec/button.py | 105 ++ custom_components/zaptec/config_flow.py | 11 +- custom_components/zaptec/const.py | 50 +- custom_components/zaptec/diagnostics.py | 190 ++++ custom_components/zaptec/manifest.json | 7 +- custom_components/zaptec/misc.py | 91 +- custom_components/zaptec/number.py | 127 +++ custom_components/zaptec/sensor.py | 329 ++---- custom_components/zaptec/services.py | 111 +- custom_components/zaptec/services.yaml | 34 +- custom_components/zaptec/switch.py | 155 ++- custom_components/zaptec/translations/en.json | 31 + 17 files changed, 1770 insertions(+), 939 deletions(-) create mode 100644 custom_components/zaptec/binary_sensor.py create mode 100644 custom_components/zaptec/button.py create mode 100644 custom_components/zaptec/diagnostics.py create mode 100644 custom_components/zaptec/number.py diff --git a/.gitignore b/.gitignore index 511c410..2b21fe9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ $RECYCLE.BIN/ # ========================= __pycache__ + +# Python virtual environments +venv* diff --git a/LICENSE b/LICENSE index 6f1df00..fa76e13 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2021 Hellowlol and contributers +Copyright (c) 2023 Svein Seldal, @sveinse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index bd50715..9fc7da4 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -1,102 +1,313 @@ -"""Support for zaptec.""" +"""Zaptec component.""" +from __future__ import annotations + import asyncio import logging from datetime import timedelta +from typing import Any -import homeassistant.helpers.config_validation as cv -import voluptuous as vol +import async_timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import discovery +from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_USERNAME, Platform) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, -) -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType - -from . import api -from .const import ( - CONF_ENABLED, - CONF_NAME, - CONF_SENSOR, - CONF_SWITCH, - DOMAIN, - EVENT_NEW_DATA, - PLATFORMS, - STARTUP, -) -from .services import async_setup_services +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import (CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed) + +from .api import Account, ZaptecApiError, ZaptecBase +from .const import (DEFAULT_SCAN_INTERVAL, DOMAIN, MANUFACTURER, MISSING, + REQUEST_REFRESH_DELAY, STARTUP) _LOGGER = logging.getLogger(__name__) +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + # Platform.DEVICE_TRACKER, + # Platform.LOCK, + # Platform.NOTIFY, + Platform.NUMBER, + # Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] -async def _dry_setup(hass, config): - _LOGGER.info(STARTUP) - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - - if not username or not password: - _LOGGER.debug("Missing username and password") - # Add persistent notification too? - return - - # Add the account to a so it can be shared. - # between the sensor and the switch. - if DOMAIN not in hass.data: - hass.data.setdefault(DOMAIN, {}) - acc = api.Account(username, password, async_get_clientsession(hass)) - hass.data[DOMAIN]["api"] = acc - - if acc.is_built is False: - await acc.build() - - async def push_update_for_map(_): - for key, value in acc.map.items(): - state = await value.state() - _LOGGER.debug( - "Pusing update for %s %s %s", key, value.__class__.__name__, state - ) - - async_dispatcher_send(hass, EVENT_NEW_DATA) - - prod = async_track_time_interval( - hass, push_update_for_map, timedelta(seconds=60) - ) - hass.data[DOMAIN].setdefault("producer", []).append(prod) - await async_setup_services(hass) +# FIXME: Informing users that the interface is considerable different +# FIXME: Setting that allows users to continue with old naming scheme? - -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up integration.""" if DOMAIN in hass.data: _LOGGER.info("Delete zaptec from your yaml") return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up zaptec as config entry.""" - await _dry_setup(hass, entry.data) + + _LOGGER.info(STARTUP) + _LOGGER.debug("Setting up entry %s: %s", entry.entry_id, entry.data) + + coordinator = ZaptecUpdateCoordinator( + hass, + entry=entry, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + # Setup all platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + + _LOGGER.debug("Unloading entry %s", entry.entry_id) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - acc = hass.data[DOMAIN]["api"] - # no need to unload stand-alone chargers - await asyncio.gather(*[i.cancel_stream() for i in acc.installs]) - - for unsub in hass.data[DOMAIN]["producer"]: - unsub() - hass.data.pop(DOMAIN) + coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.cancel_streams() return unload_ok -async def async_reload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reload config entry.""" await async_unload_entry(hass, entry) await async_setup_entry(hass, entry) + + +class ZaptecUpdateCoordinator(DataUpdateCoordinator[None]): + + account: Account + + def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: + """Initialize account-wide Zaptec data updater.""" + + _LOGGER.debug("Setting up coordinator") + + self.account = Account( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + client=async_get_clientsession(hass), + ) + + self._entry = entry + scan_interval = entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}-{entry.data['username']}", + update_interval=timedelta(seconds=scan_interval), + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False, + ), + ) + + async def cancel_streams(self): + await asyncio.gather(*( + i.cancel_stream() for i in self.account.installs + )) + + @callback + async def _stream_update(self, event): + """Handle new update event from the zaptec stream. The zaptec objects + are updated in-place prior to this callback being called. + """ + self.async_update_listeners() + + async def _async_update_data(self) -> None: + """Fetch data from Zaptec.""" + + try: + async with async_timeout.timeout(10): + if not self.account.is_built: + # Build the Zaptec hierarchy + await self.account.build() + + # Setup the stream subscription + for install in self.account.installs: + await install.stream(cb=self._stream_update) + return + + # Fetch updates + for k, v in self.account.map.items(): + await v.state() + except ZaptecApiError as err: + _LOGGER.exception(f"{err}") + raise UpdateFailed(err) from err + + +class ZaptecBaseEntity(CoordinatorEntity[ZaptecUpdateCoordinator]): + + coordinator: ZaptecUpdateCoordinator + zaptec_obj: ZaptecBase + entity_description: EntityDescription + _attr_has_entity_name = True + _prev_value: Any = MISSING + + def __init__( + self, + coordinator: ZaptecUpdateCoordinator, + zaptec_object: ZaptecBase, + description: EntityDescription, + device_info: DeviceInfo, + ) -> None: + super().__init__(coordinator) + + self.zaptec_obj = zaptec_object + self.entity_description = description + self._attr_unique_id = f"{zaptec_object.id}_{description.key}" + self._attr_device_info = device_info + + # Call this last if the inheriting class needs to do some addition + # initialization + self._post_init() + + def _post_init(self) -> None: + '''Called after the entity has been initialized. Implement this for a + custom light-weight init in the inheriting class. + ''' + + @callback + def _handle_coordinator_update(self) -> None: + try: + self._update_from_zaptec() + except Exception as exc: + _LOGGER.exception("Error updating switch '%s': %s", self.name, exc) + self._update_from_zaptec_failed() + raise HomeAssistantError(exc) from exc + super()._handle_coordinator_update() + + @callback + def _update_from_zaptec(self) -> None: + '''Called when the coordinator has new data. Implement this in the + inheriting class to update the entity state. + ''' + + @callback + def _update_from_zaptec_failed(self) -> None: + '''Called when the coordinator has failed to update. Implement this in + the inheriting class to update the entity state. + ''' + + @callback + def _get_zaptec_value(self, default=MISSING): + '''Helper to retrieve the value from the Zaptec object. This is to + be called from _handle_coordinator_update() in the inheriting class. + It will fetch the attr given by the entity description key. + ''' + if default is MISSING: + return getattr(self.zaptec_obj, self.key) + return getattr(self.zaptec_obj, self.key, default) + + @callback + def _log_value(self, value, force=False): + '''Helper to log a new value. This is to be called from + _handle_coordinator_update() in the inheriting class. + ''' + prev = self._prev_value + self._prev_value = value + if force or value != prev: + # Only logs when the value changes + _LOGGER.debug(" %s.%s = %s (%s)", + self.__class__.__qualname__, + self.key, + value, type(value)) + + @classmethod + def create_from( + cls, + sensors: list[EntityDescription], + coordinator: ZaptecUpdateCoordinator, + zaptec_obj: ZaptecBase, + device_info: DeviceInfo, + ) -> list[ZaptecBaseEntity]: + '''Helper factory to create a list of entities from a list of + EntityDescription objects. + ''' + + # Start with the common device info and append the provided device info + dev_info = DeviceInfo( + manufacturer=MANUFACTURER, + identifiers={(DOMAIN, zaptec_obj.id)}, + name=zaptec_obj.name, + ) + dev_info.update(device_info) + + entities = [] + for description in sensors: + # Use provided class if it exists, otherwise use the class this + # function was called from + klass = getattr(description, "cls", cls) or cls + entity = klass(coordinator, zaptec_obj, description, dev_info) + entities.append(entity) + return entities + + @classmethod + def create_from_zaptec( + cls, + account: Account, + coordinator: ZaptecUpdateCoordinator, + installation_entities: list[EntityDescription], + circuit_entities: list[EntityDescription], + charger_entities: list[EntityDescription], + ) -> list[ZaptecBaseEntity]: + '''Helper factory to populate the listed entities for the detected + Zaptec devices. It sets the proper device info on the installation, + circuit and charger object in order for them to be grouped in HA. + ''' + entities = [] + + for zap_install in account.installs: + entities.extend(cls.create_from( + installation_entities, coordinator, zap_install, + DeviceInfo( + model=f"{zap_install.name} Installation", + ), + )) + + for zap_circuit in zap_install.circuits: + entities.extend(cls.create_from( + circuit_entities, coordinator, zap_circuit, + DeviceInfo( + model=f"{zap_circuit.name} Circuit", + via_device=(DOMAIN, zap_install.id), + ), + )) + + for zap_charger in zap_circuit.chargers: + entities.extend(cls.create_from( + charger_entities, coordinator, zap_charger, + DeviceInfo( + model=f"{zap_charger.name} Charger", + via_device=(DOMAIN, zap_circuit.id), + ), + )) + + for zap_charger in account.stand_alone_chargers: + entities.extend(cls.create_from( + charger_entities, coordinator, zap_charger, + DeviceInfo( + model=f"{zap_charger.name} Charger", + ), + )) + + return entities + + @property + def key(self): + '''Helper to retrieve the key from the entity description.''' + return self.entity_description.key diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index df14732..f27db9d 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -1,22 +1,24 @@ -# pylint: disable=C0116 - +"""Main API for Zaptec.""" +from __future__ import annotations import asyncio import json import logging -import re +from abc import ABC, abstractmethod +from collections.abc import Iterable from concurrent.futures import CancelledError from functools import partial import aiohttp import async_timeout -_LOGGER = logging.getLogger(__name__) +# pylint: disable=missing-function-docstring -# negative lookahead regex for something -# that looks like json. -jsonish = re.compile(b"(?!.({.+})){.+}") +# Type definitions +TValue = str | int | float | bool +TDict = dict[str, TValue] +_LOGGER = logging.getLogger(__name__) """ stuff are missing from the api docs compared to what the portal uses. @@ -29,183 +31,138 @@ # to Support running this as a script. if __name__ == "__main__": + from const import (API_RETRIES, API_URL, CONST_URL, FALSY, MISSING, + TOKEN_URL, TRUTHY) + from misc import mc_nbfx_decoder, to_under + # remove me later logging.basicConfig(level=logging.DEBUG) - TOKEN_URL = "https://api.zaptec.com/oauth/token" - API_URL = "https://api.zaptec.com/api/" - CONST_URL = "https://api.zaptec.com/api/constants" - - def to_under(word) -> str: - """helper to convert TurnOnThisButton to turn_on_this_button.""" - # Ripped from inflection - word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word) - word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word) - word = word.replace("-", "_") - return word.lower() + logging.getLogger("azure").setLevel(logging.WARNING) else: - from .const import API_URL, CONST_URL, TOKEN_URL - from .misc import to_under + from .const import (API_RETRIES, API_URL, CONST_URL, FALSY, MISSING, + TOKEN_URL, TRUTHY) + from .misc import mc_nbfx_decoder, to_under -class AuthorizationFailedException(Exception): - pass +class ZaptecApiError(Exception): + '''Base exception for all Zaptec API errors''' -# should be a static method of account -async def _update_remaps() -> None: - wanted = ["Observations"] - obs = {} - async with aiohttp.request("GET", CONST_URL) as resp: - if resp.status == 200: - data = await resp.json() - for k, v in data.items(): - if k in wanted: - obs.update(v) - # Add names. - obs.update({value: key for key, value in v.items()}) +class AuthorizationError(ZaptecApiError): + '''Authenatication failed''' - _LOGGER.debug("Update remaps") - return obs +class RequestError(ZaptecApiError): + '''Failed to get the results from the API''' -class ZapBase: - def __init__(self, data, account): + +class RequestRetryError(ZaptecApiError): + '''Retries too many times''' + + +class ZaptecBase(ABC): + + id: str + name: str + _account: "Account" + _attrs: TDict + + def __init__(self, data: TDict, account: "Account") -> None: self._account = account - self._data = data self._attrs = {} + self.set_attributes(data) - def set_attributes(self, data=None): - if data is None: - data = self._data - # _LOGGER.debug("ZapBase set_attributes %s", str(data).encode("utf-8")) - - if not isinstance(data, list): - new_data = [data] - else: - new_data = data - - # known stateid that does not exist in remap - # have emailed zaptec. - missing_from_docs = [806] - - for stuff in new_data: - if "StateId" in stuff: - obs_key = self._account.obs.get(stuff["StateId"]) - if obs_key is None: - if stuff["StateId"] in missing_from_docs: - continue - _LOGGER.info( - "Couldnt find a remap string for %s report it %s", - stuff["StateId"], - stuff, - ) - continue - stuff = {obs_key: stuff.get("ValueAsString")} - - for key, value in stuff.items(): - new_key = to_under(key) - self._attrs[new_key] = value - _LOGGER.debug( - "Setting attribute %s value %s on %s", - new_key, - value, - self.__class__.__name__, - ) + def set_attributes(self, data: TDict) -> bool: + newdata = False + for k, v in data.items(): + new_key = to_under(k) + if new_key not in self._attrs: + _LOGGER.debug(">>> Adding %s.%s (%s) = %s", self.__class__.__qualname__, new_key, k, v) + newdata = True + elif self._attrs[new_key] != v: + _LOGGER.debug(">>> Updating %s.%s (%s) = %s (was %s)", self.__class__.__qualname__, new_key, k, v, self._attrs[new_key]) + newdata = True + self._attrs[new_key] = v + return newdata def __getattr__(self, key): try: return self._attrs[to_under(key)] - except KeyError: - raise + except KeyError as exc: + raise AttributeError(exc) from exc + @abstractmethod + async def build(self) -> None: + """Build the object""" -class Circuit(ZapBase): - """Represents a circuits""" + @abstractmethod + async def state(self) -> None: + """Update the state of the object""" - def __init__(self, data, account): - super().__init__(data, account) - self._chargers = [] - - self.set_attributes() - - async def get_chargers(self): - chargers = [] - for item in self._data["Chargers"]: - data = await self._account.charger(item["Id"]) - c = Charger(data, self._account) - if item["Id"] not in self._account.map: - self._account.map[item["Id"]] = c - chargers.append(c) - self.chargers = chargers - return chargers - - async def state(self): - data = await self._account._request(f"circuits/{self.id}/") - self.set_attributes(data) +class Installation(ZaptecBase): + """Represents an installation""" -class Installation(ZapBase): - """This class represents an Installation""" + circuits: list[Circuit] def __init__(self, data, account): super().__init__(data, account) - # fill out stuff here. self.connection_details = None self.circuits = [] self._stream_task = None self._stream_receiver = None - self.set_attributes() async def build(self): - data = await self._account.hierarchy(self.id) + data = await self._account._req_hierarchy(self.id) + circuits = [] for item in data["Circuits"]: - c = Circuit(item, self._account) - if item["Id"] not in self._account.map: - self._account.map[item["Id"]] = c - await c.get_chargers() - self.circuits.append(c) + circ = Circuit(item, self._account) + _LOGGER.debug(" Circuit %s", item["Id"]) + self._account.register(item["Id"], circ) + await circ.build() + circuits.append(circ) + self.circuits = circuits async def state(self): - data = await self._account.installation(self.id) + _LOGGER.debug("Polling state for %s installation (%s)", self.id, self._attrs.get('name')) + data = await self._account._req_installation(self.id) self.set_attributes(data) async def limit_current(self, **kwargs): """Set a limit now how many amps the installation can use + Use availableCurrent for setting all phases at once. Use + availableCurrentPhase* to set each phase individually. + """ + has_availablecurrent = "availableCurrent" in kwargs + has_availablecurrentphases = all( + k in kwargs for k in ( + "availableCurrentPhase1", + "availableCurrentPhase2", + "availableCurrentPhase3", + ) + ) - Use availableCurrent for 3phase - use just select the phase you want to use. - + if not (has_availablecurrent ^ has_availablecurrentphases): + raise ValueError("Either availableCurrent or all of availableCurrentPhase1, availableCurrentPhase2, availableCurrentPhase3 must be set") - """ - total = "availableCurrent" - phases = [ - "availableCurrentPhase1", - "availableCurrentPhase2", - "availableCurrentPhase3", - ] - - # If any of the phases are present and not None, remove the total field. - if any(k and v is not None - for k, v in kwargs.items() - if k in phases - ): - kwargs.pop(total, None) - - return await self._account._request( + data = await self._account._request( f"installation/{self.id}/update", method="post", data=kwargs ) + # FIXME: Verify assumed data structure + return data async def live_stream_connection_details(self): data = await self._account._request( f"installation/{self.id}/messagingConnectionDetails" ) + # FIXME: Verify assumed data structure self.connection_details = data return data - async def stream(self, cb=None): + async def stream(self, cb=None) -> asyncio.Task: """Kickoff the steam in the background.""" try: from azure.servicebus.aio import ServiceBusClient @@ -217,94 +174,83 @@ async def stream(self, cb=None): await self.cancel_stream() self._stream_task = asyncio.create_task(self._stream(cb=cb)) + return self._stream_task async def _stream(self, cb=None): try: - from azure.servicebus.aio import ServiceBusClient - from azure.servicebus.exceptions import ServiceBusError - except ImportError: - _LOGGER.debug("Azure Service bus is not available. Resolving to polling") - # https://github.com/custom-components/zaptec/issues - return - - conf = await self.live_stream_connection_details() - # Check if we can use it. - if any(True for i in ["Password", "Username", "Host"] if conf.get(i) == ""): - _LOGGER.warning( - "Cant enable live update using the servicebus, enable it in the zaptec portal" - ) - return - - constr = f'Endpoint=sb://{conf["Host"]}/;SharedAccessKeyName={conf["Username"]};SharedAccessKey={conf["Password"]}' - servicebus_client = ServiceBusClient.from_connection_string(conn_str=constr) - _LOGGER.debug("Connecting to servicebus using %s", constr) + try: + from azure.servicebus.aio import ServiceBusClient + from azure.servicebus.exceptions import ServiceBusError + except ImportError: + _LOGGER.debug("Azure Service bus is not available. Resolving to polling") + # https://github.com/custom-components/zaptec/issues + return + + # Get connection details + conf = await self.live_stream_connection_details() + + # Check if we can use it. + if any(True for i in ["Password", "Username", "Host"] if conf.get(i) == ""): + _LOGGER.warning( + "Cant enable live update using the servicebus, enable it in the zaptec portal" + ) + return - # To be removed. - seen = set() - obs_values = set(self._account.obs.values()) - obs = self._account.obs + # Open the connection + constr = (f'Endpoint=sb://{conf["Host"]}/;' + f'SharedAccessKeyName={conf["Username"]};' + f'SharedAccessKey={conf["Password"]}') + servicebus_client = ServiceBusClient.from_connection_string(conn_str=constr) + _LOGGER.debug("Connecting to servicebus using %s", constr) - try: self._stream_receiver = None async with servicebus_client: receiver = servicebus_client.get_subscription_receiver( - topic_name=conf["Topic"], subscription_name=conf["Subscription"] + topic_name=conf["Topic"], + subscription_name=conf["Subscription"] ) # Store the receiver in order to close it and cancel this stream self._stream_receiver = receiver async with receiver: async for msg in receiver: - await asyncio.sleep(0) - _LOGGER.debug("Got a message from the servicebus") - # pretty sure there should be some other - # better way to handle this but it will have to do - # for now. # FIXME - body = b"".join(msg.body) - # body = "".join(body.decode(enc)) - _LOGGER.debug("body was %s", body) - found = jsonish.search(body) - if found: - found = found.group() - _LOGGER.debug("found %s", found) - json_result = json.loads(found.decode("utf-8")) - - _LOGGER.debug("%s", found) - - # Add this should be removed later, only added. - std = json_result.get("StateId") - name = obs.get(std) - - if std and std not in seen: - seen.add(json_result.get("StateId")) - _LOGGER.debug( - "Added %s %s to seen got %s of %s", - std, - to_under(name), - len(seen), - len(obs_values), - ) - _LOGGER.debug( - "Have ids: %s", ", ".join([str(i) for i in seen]) - ) - _LOGGER.debug( - "Have names: %s", - ", ".join([to_under(obs.get(i, "")) for i in seen]), - ) - _LOGGER.debug( - "Missing %s", ", ".join([str(i) for i in obs_values]) - ) + binmsg = "" # For the exception in case it fails before setting the value + try: + # After some blind research it seems the messages + # are encoded with .NET binary xml format (MC-NBFX) + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/mc-nbfx + # Surprisingly there doesn't seem to be any py libries + # for that, so a small scaled down version is added + # here. + binmsg = b"".join(msg.body) + # _LOGGER.debug("Received message %s", binmsg) + + # Decode MC-NBFX message + obj = mc_nbfx_decoder(binmsg) + # _LOGGER.debug("Unecoded message: %s", obj) + + # Convert the json payload + json_result = json.loads(obj[0]["text"]) + _LOGGER.debug("--- Subscription: %s", json_result) + + # Send result to account that will update the objects + self._account.update(json_result) # Execute the callback. if cb: await cb(json_result) - else: - _LOGGER.debug( - "Couldn't extract the json from the message body, %s", body - ) + except Exception as err: + _LOGGER.exception("Couldn't process stream message: %s", err) + _LOGGER.debug("Message: %s", binmsg) + # Pass the message as the stream must continue. # remove the msg from the "queue" await receiver.complete_message(msg) + + except Exception as err: + # Do this in order to show the error in the log. + _LOGGER.exception("Stream failed: %s", err) + finally: # To ensure its not set if not active self._stream_receiver = None @@ -327,44 +273,239 @@ async def cancel_stream(self): # this will still raise a exception, I think its a 3.7 issue. # recheck this when the i have updated to 3.9 - self._stream_task = None + finally: + self._stream_task = None + + +class Circuit(ZaptecBase): + """Represents a circuits""" + + chargers: list["Charger"] + _chargers: list[TDict] + + def __init__(self, data, account): + super().__init__(data, account) + self.chargers = [] + self._chargers = data.get("Chargers", []) or [] + + async def build(self): + """Build the python interface.""" + chargers = [] + for item in self._chargers: + data = await self._account._req_charger(item["Id"]) + chg = Charger(data, self._account) + _LOGGER.debug(" Charger %s", item["Id"]) + self._account.register(item["Id"], chg) + await chg.build() + chargers.append(chg) + self.chargers = chargers + + async def state(self): + _LOGGER.debug("Polling state for %s cicuit (%s)", self.id, self._attrs.get('name')) + data = await self._account._req_circuit(self.id) + self.set_attributes(data) + + +class Charger(ZaptecBase): + """Represents a charger""" + + async def build(self) -> None: + '''Build the object''' + + async def state(self): + '''Update the charger state''' + _LOGGER.debug("Polling state for %s charger (%s)", self.id, self._attrs.get('name')) + + # FIXME: This has multiple set_attributes that might compete for the same key. Fix this. + # FIXME: E.g. is_online is ambulating between True and 1 + + data = await self._account._req_charger(self.id) + self.set_attributes(data) + + data = await self._account._req_charger_state(self.id) + data = Account._state_to_attrs(data, 'StateId', self._account._obs_ids) + self.set_attributes(data) + + # Firmware version is called. SmartMainboardSoftwareApplicationVersion, + # stateid 908 + # I couldn't find a way to see if it was up to date.. + # maybe remove this later if it dont interest ppl. + + # Fetch some additional attributes from settings + data = await self._account._req_charger_settings(self.id) + data = Account._state_to_attrs(data.values(), 'SettingId', self._account._set_ids) + self.set_attributes(data) + + if self.installation_id in self._account.map: + firmware_info = await self._account._req_charger_firmware(self.installation_id) + for fm in firmware_info: + if fm["ChargerId"] == self.id: + self.set_attributes({ + "current_firmware_version": fm["CurrentVersion"], + "available_firmware_version": fm["AvailableVersion"], + "firmware_update_to_date": fm["IsUpToDate"], + }) + + async def command(self, command:str): + + # FIXME: Use the names from the constant json? + + # All methods needs to be checked again + COMMANDS = { + "restart_charger": 102, + "restart_mcu": 103, + "update_settings": 104, + "restart_ntp": 105, + "exit_app_with_code": 106, + "upgrade_firmware": 200, + "upgrade_firmware_forced": 201, + "reset_com_errors": 260, + "reset_notifications": 261, + "reset_com_warnings": 262, + "local_settings": 260, + "set_plc_npw": 320, + "set_plc_cocode": 321, + "set_plc_nmk": 322, + "set_remote_plc_nmk": 323, + "set_remote_plc_npw": 324, + "start_charging": 501, + "stop_charging": 502, + "report_charging_state": 503, + "set_session_id": 504, + "set_user_uuid": 505, + "stop_pause": 506, # Require firmware > 3.2 + "resume_charging": 507, # Require firmware > 3.2 + "show_granted": 601, + "show_denied": 602, + "indicate_app_connect": 603, + "confirm_charge_card_added": 750, + "set_authentication_list": 751, + "debug": 800, + "get_plc_topology": 801, + "reset_plc": 802, + "remote_command": 803, + "run_grid_test": 804, + "run_post_production_test": 901, + "combined_min": 10000, + "deauthorize_stop": 10001, + "combined_max": 10999, + "authorize_charge": None, # Special case + } + + if command not in COMMANDS: + raise ValueError(f"Unknown command {command}") + + if command == "authorize_charge": + data = await self._account._request(f"chargers/{self.id}/authorizecharge", method="post") + # FIXME: Verify assumed data structure + return data + + _LOGGER.debug("Command %s", command) + cmd = f"chargers/{self.id}/SendCommand/{COMMANDS[command]}" + _LOGGER.debug("Calling %s", cmd) + data = await self._account._request(cmd, method="post") + # FIXME: Verify assumed data structure + return data + + async def live(self): + # This don't seems to be documented but the portal uses it + # TODO check what it returns and parse it to attributes + data = await self._account._request("chargers/%s/live" % self.id) + # FIXME: Verify assumed data structure + return data + + async def settings(self): + # TODO check what it returns and parse it to attributes + data = await self._account._request("chargers/%s/settings" % self.id) + # FIXME: Verify assumed data structure + return data + + async def update(self, data): + # https://api.zaptec.com/help/index.html#/Charger/post_api_chargers__id__update + # Not really sure this should be added as ppl might use it wrong + cmd = f"chargers/{self.id}/update" + default = { + # nullable: true + # Adjustable between 0 and 32A. If charge current is below the charger minimum charge current (usually 6A), no charge current will be allocated. + "maxChargeCurrent": 0, + # MaxPhaseinteger($int32) # Enum 1,3 + "maxChargePhases": "", + # The minimum allocated charge current. If there is not enough current available to provide the + # chargers minimum current it will not be able to charge. + # Usually set to match the vehicle minimum current for charging (defaults to 6A) + "minChargeCurrent": None, + # Adjustable between 0 and 32A. If offline charge current is below the charger minimum charge current (usually 6A), + # no charge current will be allocated when offline. + # Offline current override should only be done in special cases where charging stations should not automatically optimize offline current. + # In most cases this setting should be set to -1 to allow ZapCloud to optimise offline current. If -1, offline current will be automatically allocated. + "offlineChargeCurrent": None, + # Phasesinteger($int32) ENUM + # 0 = None + # 1 = Phase_1 + # 2 = Phase_2 + # 4 = Phase_3 + # 7 = All + "offlineChargePhase": None, + # nullable + # The interval in seconds for a charger to report meter values. Defaults to 900 seconds for Pro and 3600 seconds for Go + "meterValueInterval": None, + } + + pass + + # return await self._account.request(cmd, data=data, method="post") + + @property + def is_authorization_required(self): + return self._attrs["is_authorization_required"] in TRUTHY + + @property + def permanent_cable_lock(self): + return self._attrs["permanent_cable_lock"] in TRUTHY + + @property + def operating_mode(self): + modes = {str(v): k for k, v in self._account._const["ChargerOperationModes"].items()} + v = self._attrs["operating_mode"] + return modes.get(str(v), str(v)) + + @property + def charger_operation_mode(self): + modes = {str(v): k for k, v in self._account._const["ChargerOperationModes"].items()} + v = self._attrs["charger_operation_mode"] + return modes.get(str(v), str(v)) class Account: """This class represent an zaptec account""" - def __init__(self, username, password, client=None): + def __init__(self, username: str, password: str, client=None) -> None: + _LOGGER.debug("Account init") self._username = username self._password = password self._client = client self._token_info = {} self._access_token = None - self.installs = [] - self.stand_alone_chargers = [] - # Map using the id for lookupss - self.map = {} - self.obs = {} - if client is None: - self._client = aiohttp.ClientSession() + self.installs: list[Installation] = [] + self.stand_alone_chargers: list[Charger] = [] + self.map: dict[str, ZaptecBase] = {} + self._const = {} + self._obs_ids = {} + self._set_ids = {} self.is_built = False - def update(self, data): - """update for the stream. Note build has to called first.""" - if not isinstance(data, list): - data = [data] + if client is None: + self._client = aiohttp.ClientSession() - for d in data: - # this might be some other ids to, check it #TODO - cls_id = d.pop("ChargerId", None) - if cls_id is not None: - klass = self.map.get(cls_id) - if klass: - klass.set_attributes(d) + def register(self, id: str, data: ZaptecBase): + '''Register an object data with id''' + self.map[id] = data - self.is_built = True + # ======================================================================= + # API METHODS @staticmethod - async def check_login(username, password): + async def check_login(username: str, password: str) -> bool: p = { "username": username, "password": password, @@ -376,13 +517,11 @@ async def check_login(username, password): data = await resp.json() return True else: - raise AuthorizationFailedException + raise AuthorizationError(f"Failed to authenticate. Got status {resp.status}") except aiohttp.ClientConnectorError as err: _LOGGER.exception("Bad things happend while trying to authenticate :(") raise - return False - async def _refresh_token(self): # So for some reason they used grant_type password.. # what the point with oauth then? Anyway this is valid for 24 hour @@ -399,23 +538,30 @@ async def _refresh_token(self): self._token_info.update(data) self._access_token = data.get("access_token") else: - _LOGGER.debug("Failed to refresh token, check your credentials.") + raise AuthorizationError("Failed to refresh token, check your credentials.") - async def _request(self, url, method="get", data=None): + async def _request(self, url: str, method="get", data=None, iteration=1): header = { "Authorization": "Bearer %s" % self._access_token, "Accept": "application/json", } full_url = API_URL + url try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): call = getattr(self._client, method) if data is not None and method == "post": call = partial(call, json=data) + # _LOGGER.debug(f"@@@ Req {method} to '{full_url}' payload {data}") + resp: aiohttp.ClientResponse async with call(full_url, headers=header) as resp: + # _LOGGER.debug(f" @ Res {resp.status} data length {resp.content_length}") + # _LOGGER.debug(f" @ Res {resp.status} header {dict((k, v) for k, v in resp.headers.items())}") + # _LOGGER.debug(f" @ Res {resp.status} content {await resp.text()}") if resp.status == 401: # Unauthorized await self._refresh_token() - return await self._request(url) + if iteration > API_RETRIES: + raise RequestRetryError(f"Request to {full_url} failed after {iteration} retries") + return await self._request(url, iteration=iteration + 1) elif resp.status == 204: # No content content = await resp.read() return content @@ -423,270 +569,184 @@ async def _request(self, url, method="get", data=None): json_result = await resp.json(content_type=None) return json_result else: - _LOGGER.error("Could not get info from %s: %s", full_url, resp) - return None + raise RequestError(f"{method} request to {full_url} failed with status {resp.status}: {resp}") except (asyncio.TimeoutError, aiohttp.ClientError) as err: - _LOGGER.error("Could not get info from %s: %s", full_url, err) + raise RequestError(f"Request to {full_url} failed: {err}") from err - async def hierarchy(self, installation_id): - return await self._request(f"installation/{installation_id}/hierarchy") + async def _req_constants(self): + data = await self._request("constants") + # FIXME: Verify assumed data structure + return data - async def installations(self): + async def _req_installations(self) -> list[TDict]: data = await self._request("installation") - return data + # FIXME: Verify assumed data structure + return data["Data"] - async def installation(self, installation_id): + async def _req_installation(self, installation_id: str) -> TDict: data = await self._request(f"installation/{installation_id}") + # FIXME: Verify assumed data structure # Remove data fields with excessive data, making it bigger than the # HA database appreciates for the size of attributes. + # FIXME: SupportGroup is sub dict. This is not within the declared type supportgroup = data.get('SupportGroup') if supportgroup is not None: if "LogoBase64" in supportgroup: logo = supportgroup["LogoBase64"] - if len(logo) > 1024: - supportgroup["LogoBase64"] = "" %(len(logo)) + supportgroup["LogoBase64"] = "" %(len(logo)) return data - async def charger(self, charger_id): - data = await self._request(f"chargers/{charger_id}") + async def _req_hierarchy(self, installation_id: str) -> dict[str, list[dict[str, TValue]]]: + data = await self._request(f"installation/{installation_id}/hierarchy") + # FIXME: Verify assumed data structure return data - async def charger_firmware(self, installation_id): - data = await self._request(f"chargerFirmware/installation/{installation_id}") + async def _req_circuit(self, circuit_id: str) -> TDict: + data = await self._request(f"circuits/{circuit_id}") + # FIXME: Verify assumed data structure return data - async def chargers(self): - charg = await self._request("chargers") - return [Charger(chrg, self) for chrg in charg.get("Data", []) if chrg] - - async def build(self): - """Make the python interface.""" - if not len(self.obs): - self.obs = await _update_remaps() - installations = await self.installations() - - cls_installs = [] - for data in installations["Data"]: - install_data = await self.installation(data["Id"]) - I = Installation(install_data, self) - - self.map[data["Id"]] = I - await I.build() - cls_installs.append(I) - - self.installs = cls_installs - - so_chargers = await self.chargers() - for charger in so_chargers: - if charger.id not in self.map: - self.map[charger.id] = charger - self.stand_alone_chargers = so_chargers - - -class Charger(ZapBase): - """Represents a charger""" - - def __init__(self, data, account): - super().__init__(data, account) - self.set_attributes() - - # All methods need to be checked again. - async def restart_charger(self): - return await self._send_command(102) - - async def restart_mcu(self): - return await self._send_command(103) - - async def update_settings(self): - return await self._send_command(104) - - async def restart_ntp(self): - return await self._send_command(105) - - async def exit_app_with_code(self): - return await self._send_command(106) - - async def upgrade_firmware(self): - return await self._send_command(200) - - async def upgrade_firmware_forced(self): - return await self._send_command(201) - - async def reset_com_errors(self): - return await self._send_command(260) - - async def reset_notifications(self): - return await self._send_command(261) - - async def reset_com_warnings(self): - return await self._send_command(262) - - async def local_settings(self): - return await self._send_command(260) - - async def set_plc_npw(self): - return await self._send_command(320) - - async def set_plc_cocode(self): - return await self._send_command(321) - - async def set_plc_nmk(self): - return await self._send_command(322) - - async def set_remote_plc_nmk(self): - return await self._send_command(323) - - async def set_remote_plc_npw(self): - return await self._send_command(324) + async def _req_chargers(self) -> list[TDict]: + data = await self._request("chargers") + # FIXME: Verify assumed data structure + return data["Data"] - async def start_charging(self): - _LOGGER.debug("Attempting to start charging") - return await self._send_command(501) - - async def stop_charging(self): - _LOGGER.debug("Attempting to stops charging") - return await self._send_command(502) - - async def report_charging_state(self): - return await self._send_command(503) - - async def set_session_id(self): - return await self._send_command(504) - - async def set_user_uuid(self): - return await self._send_command(505) - - # require firmware > 3.2 - async def stop_pause(self): - return await self._send_command(506) - - # require firmware > 3.2 - async def resume_charging(self): - return await self._send_command(507) - - async def show_granted(self): - return await self._send_command(601) - - async def show_denied(self): - return await self._send_command(602) + async def _req_charger(self, charger_id: str) -> TDict: + data = await self._request(f"chargers/{charger_id}") + # FIXME: Verify assumed data structure + return data - async def indicate_app_connect(self): - return await self._send_command(603) + async def _req_charger_firmware(self, installation_id: str) -> TDict: + data = await self._request(f"chargerFirmware/installation/{installation_id}") + # FIXME: Verify assumed data structure + return data - async def confirm_charge_card_added(self): - return await self._send_command(750) + async def _req_charger_state(self, charger_id: str) -> list[TDict]: + data = await self._request(f"chargers/{charger_id}/state") + # FIXME: Verify assumed data structure + return data - async def set_authentication_list(self): - return await self._send_command(751) + async def _req_charger_settings(self, charger_id: str) -> dict[str, TDict]: + data = await self._request(f"chargers/{charger_id}/settings") + # FIXME: Verify assumed data structure + return data - async def debug(self): - return await self._send_command(800) + # API METHODS DONE + # ======================================================================= - async def get_plc_topology(self): - return await self._send_command(801) + async def build(self): + """Make the python interface.""" + _LOGGER.debug("Discover and build hierarchy") - async def reset_plc(self): - return await self._send_command(802) + installations = await self._req_installations() - async def remote_command(self): - return await self._send_command(803) + installs = [] + for data in installations: + install_data = await self._req_installation(data["Id"]) + _LOGGER.debug(" Installation %s", data["Id"]) + inst = Installation(install_data, self) + self.register(data["Id"], inst) + await inst.build() + installs.append(inst) - async def run_grid_test(self): - return await self._send_command(804) + self.installs = installs - async def run_post_production_test(self): - return await self._send_command(901) + # Will also report chargers listed in installation hierarchy above + chargers = await self._req_chargers() - async def combined_min(self): - return await self._send_command(10000) + so_chargers = [] + for data in chargers: + if data["Id"] in self.map: + continue - async def deauthorize_stop(self): - return await self._send_command(10001) + _LOGGER.debug(" Charger %s", data["Id"]) + chg = Charger(data, self) + self.register(data["Id"], chg) + so_chargers.append(chg) - async def combined_max(self): - return await self._send_command(10999) + self.stand_alone_chargers = so_chargers - async def state(self): - data = await self._account._request(f"chargers/{self.id}/state") - # sett_attributes need to be set before any other call. - self.set_attributes(data) - # Firmware version is called. SmartMainboardSoftwareApplicationVersion, - # stateid 908 - # I couldn't find a way to see if it was up to date.. - # maybe remove this later if it dont interest ppl. + if not self._const: - # Fetch some additional attributes from settings - data = await self._account._request(f"chargers/{self.id}/settings") - max_current_id = str(self._account.obs.get("ChargerMaxCurrent", '')) - min_current_id = str(self._account.obs.get("ChargerMinCurrent", '')) - settings = { - "charger_max_current": data.get(max_current_id,{}).get("Value"), - "charger_min_current": data.get(min_current_id,{}).get("Value"), - } - self.set_attributes(settings) + # Get the API constants + self._const = await self._req_constants() - if self.installation_id in self._account.map: - firmware_info = await self._account.charger_firmware(self.installation_id) - for fm in firmware_info: - if fm["ChargerId"] == self.id: - fixed = { - "current_firmware_version": fm["CurrentVersion"], - "available_firmware_version": fm["AvailableVersion"], - "firmware_update_to_date": fm["IsUpToDate"], - } - self.set_attributes(fixed) + # Get the chargers + device_types = set( + chg.device_type + for chg in self.map.values() + if isinstance(chg, Charger) + ) - async def live(self): - # This don't seems to be documented but the portal uses it - # TODO check what it returns and parse it to attributes - return await self._account._request("chargers/%s/live" % self.id) + # Define the remaps + self._obs_ids = Account._get_remap(self._const, ["Observations", "ObservationIds"], device_types) + self._set_ids = Account._get_remap(self._const, ["Settings", "SettingIds"], device_types) - async def settings(self): - # TODO check what it returns and parse it to attributes - return await self._account._request("chargers/%s/settings" % self.id) + self.is_built = True - async def _send_command(self, id_): - cmd = "chargers/%s/SendCommand/%s" % (self.id, id_) - _LOGGER.debug("Calling %s", cmd) - return await self._account._request(cmd, method="post") + def update(self, data: TDict): + """update for the stream. Note build has to called first.""" - async def update(self, data): - # https://api.zaptec.com/help/index.html#/Charger/post_api_chargers__id__update - # Not really sure this should be added as ppl might use it wrong - cmd = f"chargers/{self.id}/update" - default = { - # nullable: true - # Adjustable between 0 and 32A. If charge current is below the charger minimum charge current (usually 6A), no charge current will be allocated. - "maxChargeCurrent": 0, - # MaxPhaseinteger($int32) # Enum 1,3 - "maxChargePhases": "", - # The minimum allocated charge current. If there is not enough current available to provide the - # chargers minimum current it will not be able to charge. - # Usually set to match the vehicle minimum current for charging (defaults to 6A) - "minChargeCurrent": None, - # Adjustable between 0 and 32A. If offline charge current is below the charger minimum charge current (usually 6A), - # no charge current will be allocated when offline. - # Offline current override should only be done in special cases where charging stations should not automatically optimize offline current. - # In most cases this setting should be set to -1 to allow ZapCloud to optimise offline current. If -1, offline current will be automatically allocated. - "offlineChargeCurrent": None, - # Phasesinteger($int32) ENUM - # 0 = None - # 1 = Phase_1 - # 2 = Phase_2 - # 4 = Phase_3 - # 7 = All - "offlineChargePhase": None, - # nullable - # The interval in seconds for a charger to report meter values. Defaults to 900 seconds for Pro and 3600 seconds for Go - "meterValueInterval": None, - } + cls_id = data.pop("ChargerId", None) + if cls_id is not None: + klass = self.map.get(cls_id) + if klass: + d = Account._state_to_attrs([data], 'StateId', self._obs_ids) + klass.set_attributes(d) + else: + _LOGGER.warning("Got update for unknown charger id %s", cls_id) + else: + _LOGGER.warning("Unknown update message %s", data) - pass + @staticmethod + def _get_remap(const, wanted, device_types=None) -> dict: + ''' Parse the given zaptec constants record `const` and generate + a remap dict for the given `wanted` keys. If `device_types` is + specified, the entries for these device schemas will be merged + with the main remap dict. + Example: + _get_remap(const, ["Observations", "ObservationIds"], [4]) + ''' + ids = {} + for k, v in const.items(): + if k in wanted: + ids.update(v) + + if device_types and k == "Schema": + for schema in v.values(): + for want in wanted: + v2 = schema.get(want) + if v2 is None: + continue + if schema.get("DeviceType") in device_types: + ids.update(v2) + + # make the reverse lookup + ids.update({v: k for k, v in ids.items()}) + return ids - # return await self._account.request(cmd, data=data, method="post") + @staticmethod + def _state_to_attrs(data: Iterable[dict[str, str]], key: str, keydict: dict[str, str]): + ''' Convert a list of state data into a dict of attributes. `key` + is the key that specifies the attribute name. `keydict` is a + dict that maps the key value to an attribute name. + ''' + out = {} + for item in data: + skey = item.get(key) + if skey is None: + _LOGGER.debug("Missing key %s in %s", key, item) + continue + value = item.get("Value", item.get("ValueAsString", MISSING)) + if value is not MISSING: + kv = keydict.get(skey, f"{key} {skey}") + if kv in out: + _LOGGER.debug("Duplicate key %s. Is '%s', new '%s'", kv, out[kv], value) + out[kv] = value + return out if __name__ == "__main__": @@ -700,26 +760,33 @@ async def gogo(): username, password, client=aiohttp.ClientSession( - connector=aiohttp.TCPConnector(verify_ssl=False) + connector=aiohttp.TCPConnector(ssl=False) ), ) - # Builds the interface. - await acc.build() - async def cb(data): - pass + try: + # Builds the interface. + await acc.build() + + # # Save the constants + # with open("constant.json", "w") as outfile: + # json.dump(acc._const, outfile, indent=2) + + # Update the state to get all the attributes. + for obj in acc.map.values(): + await obj.state() - _LOGGER.info("CB") - # print(data) + with open("data.json", "w") as outfile: - for ins in acc.installs: - for circuit in ins.circuits: - data = await circuit.state() - print(data) - # await ins._stream(cb=cb) + async def cb(data): + print(data) + outfile.write(json.dumps(data, indent=2) + '\n') + outfile.flush() - for charger in acc.stand_alone_chargers: - data = await charger.state() - print(data) + for ins in acc.installs: + await ins._stream(cb=cb) + + finally: + await acc._client.close() asyncio.run(gogo()) diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py new file mode 100644 index 0000000..26efe2c --- /dev/null +++ b/custom_components/zaptec/binary_sensor.py @@ -0,0 +1,120 @@ +"""Zaptec component binary sensors.""" +from __future__ import annotations + +import logging +from dataclasses import dataclass + +from homeassistant import const +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZaptecBaseEntity, ZaptecUpdateCoordinator +from .api import Account +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZaptecBinarySensor(ZaptecBaseEntity, BinarySensorEntity): + + @callback + def _update_from_zaptec(self) -> None: + self._attr_is_on = bool(self._get_zaptec_value()) + self._log_value(self._attr_is_on) + + +class ZaptecBinarySensorWithAttrs(ZaptecBinarySensor): + + def _post_init(self): + self._attr_extra_state_attributes = self.zaptec_obj._attrs + self._attr_unique_id = self.zaptec_obj.id + + +class ZaptecBinarySensorLock(ZaptecBinarySensor): + + @callback + def _update_from_zaptec(self) -> None: + self._attr_is_on = not bool(self._get_zaptec_value()) + self._log_value(self._attr_is_on) + + +@dataclass +class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): + + cls: type|None = None + + +INSTALLATION_ENTITIES: list[EntityDescription] = [ + ZapBinarySensorEntityDescription( + key="active", + name="Installation", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:home-lightning-bolt-outline", + has_entity_name=False, + cls=ZaptecBinarySensorWithAttrs, + ), +] + +CIRCUIT_ENTITIES: list[EntityDescription] = [ + ZapBinarySensorEntityDescription( + key="is_active", + name="Circuit", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:orbit", + has_entity_name=False, + cls=ZaptecBinarySensorWithAttrs, + ), +] + +CHARGER_ENTITIES: list[EntityDescription] = [ + ZapBinarySensorEntityDescription( + key="active", + name="Charger", + device_class=BinarySensorDeviceClass.CONNECTIVITY, # False=disconnected, True=connected + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:ev-station", + has_entity_name=False, + cls=ZaptecBinarySensorWithAttrs, + ), + ZapBinarySensorEntityDescription( + key="is_authorization_required", + translation_key="is_authorization_required", + device_class=BinarySensorDeviceClass.LOCK, # False=unlocked, True=locked + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:lock", + cls=ZaptecBinarySensorLock, + ), + ZapBinarySensorEntityDescription( + key="permanent_cable_lock", + translation_key="permanent_cable_lock", + device_class=BinarySensorDeviceClass.LOCK, # False=unlocked, True=locked + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:lock", + cls=ZaptecBinarySensorLock, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + _LOGGER.debug("Setup binary sensors") + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + acc = coordinator.account + + entities = ZaptecBinarySensor.create_from_zaptec( + acc, + coordinator, + INSTALLATION_ENTITIES, + CIRCUIT_ENTITIES, + CHARGER_ENTITIES, + ) + async_add_entities(entities, True) diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py new file mode 100644 index 0000000..62f6efa --- /dev/null +++ b/custom_components/zaptec/button.py @@ -0,0 +1,105 @@ +"""Zaptec component binary sensors.""" +from __future__ import annotations + +import logging +from dataclasses import dataclass + +from homeassistant import const +from homeassistant.components.button import (ButtonDeviceClass, ButtonEntity, + ButtonEntityDescription) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZaptecBaseEntity, ZaptecUpdateCoordinator +from .api import Account, Charger +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZaptecButton(ZaptecBaseEntity, ButtonEntity): + + async def async_press(self) -> None: + """Press the button.""" + _LOGGER.debug( + "Press %s '%s' in %s", + self.__class__.__qualname__, + self.key, + self.zaptec_obj.id + ) + + charger: Charger = self.zaptec_obj + + try: + await charger.command(self.key) + except Exception as exc: + raise HomeAssistantError(exc) from exc + + +@dataclass +class ZapButtonEntityDescription(ButtonEntityDescription): + + cls: type|None = None + + +INSTALLATION_ENTITIES: list[EntityDescription] = [ +] + +CIRCUIT_ENTITIES: list[EntityDescription] = [ +] + +CHARGER_ENTITIES: list[EntityDescription] = [ + ZapButtonEntityDescription( + key="resume_charging", + translation_key="resume_charging", + icon="mdi:play-circle-outline", + ), + ZapButtonEntityDescription( + key="stop_pause", + translation_key="stop_pause", + icon="mdi:pause-circle-outline", + ), + ZapButtonEntityDescription( + key="authorize_charge", + translation_key="authorize_charge", + icon="mdi:lock-check-outline" + ), + ZapButtonEntityDescription( + key="deauthorize_stop", + translation_key="deauthorize_stop", + icon="mdi:lock-remove-outline" + ), + ZapButtonEntityDescription( + key="restart_charger", + translation_key="restart_charger", + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:restart" + ), + ZapButtonEntityDescription( + key="update_firmware", + translation_key="update_firmware", + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:memory" + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + _LOGGER.debug("Setup buttons") + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + acc = coordinator.account + + entities = ZaptecButton.create_from_zaptec( + acc, + coordinator, + INSTALLATION_ENTITIES, + CIRCUIT_ENTITIES, + CHARGER_ENTITIES, + ) + async_add_entities(entities, True) diff --git a/custom_components/zaptec/config_flow.py b/custom_components/zaptec/config_flow.py index ba53961..e4c6d3f 100644 --- a/custom_components/zaptec/config_flow.py +++ b/custom_components/zaptec/config_flow.py @@ -1,12 +1,13 @@ """Adds config flow for zaptec.""" -import http +from __future__ import annotations + import logging import aiohttp import voluptuous as vol from homeassistant import config_entries -from .api import Account, AuthorizationFailedException +from .api import Account, AuthorizationError from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -52,10 +53,14 @@ async def async_step_user( ) except aiohttp.ClientConnectorError: errors["base"] = "connection_failure" - except AuthorizationFailedException: + except AuthorizationError: errors["base"] = "auth_failure" if valid_login: + unique_id = user_input["username"].lower() + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( title=DOMAIN.capitalize(), data=user_input ) diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index 3e491ec..37d210d 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -1,42 +1,44 @@ +"""Zaptec integration constants.""" +from __future__ import annotations + NAME = "zaptec" VERSION = "0.0.6b2" ISSUEURL = "https://github.com/custom-components/zaptec/issues" -STARTUP = """ +DOMAIN = "zaptec" +MANUFACTURER = "Zaptec" + +STARTUP = f""" ------------------------------------------------------------------- -{name} -Version: {version} +{NAME} +Version: {VERSION} This is a custom component If you have any issues with this you need to open an issue here: -{issueurl} +{ISSUEURL} ------------------------------------------------------------------- -""".format( - name=NAME, version=VERSION, issueurl=ISSUEURL -) +""" -DOMAIN = "zaptec" -OBSERVATIONS_REMAPS = {} -WANTED_ATTRIBUTES = [] CHARGE_MODE_MAP = { - "0": ["unknown", "mdi:help-rhombus-outline"], - "1": ["disconnected", "mdi:power-plug-off"], - "2": ["waiting", "mdi:power-sleep"], - "3": ["charging", "mdi:power-plug"], - "5": ["charge_done", "mdi:battery-charging-100"], + "Unknown": ["Unknown", "mdi:help-rhombus-outline"], + "Disconnected": ["Disconnected", "mdi:power-plug-off"], + "Connected_Requesting": ["Waiting", "mdi:timer-sand"], + "Connected_Charging": ["Charging", "mdi:lightning-bolt"], + "Connected_Finished": ["Charge done", "mdi:battery-charging-100"], } -CHARGE_MODE_MAP.update({int(k): v for k, v in CHARGE_MODE_MAP.items()}) TOKEN_URL = "https://api.zaptec.com/oauth/token" API_URL = "https://api.zaptec.com/api/" CONST_URL = "https://api.zaptec.com/api/constants" +API_RETRIES = 5 + +DEFAULT_SCAN_INTERVAL = 60 + +REQUEST_REFRESH_DELAY = 0.3 -CONF_SENSOR = "sensor" -CONF_SWITCH = "switch" -CONF_ENABLED = "enabled" -CONF_NAME = "name" +class Missing: + '''Singleton class representing a missing value.''' +MISSING = Missing() -EVENT_NEW_DATA = "event_new_data_zaptec" -EVENT_NEW_DATA_HOURLY = "event_new_data_hourly_zaptec" -# PLATFORMS = ["sensor"] -PLATFORMS = ["sensor", "switch"] +TRUTHY = ["true", "1", "on", "yes", 1, True] +FALSY = ["false", "0", "off", "no", 0, False] diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py new file mode 100644 index 0000000..953f35c --- /dev/null +++ b/custom_components/zaptec/diagnostics.py @@ -0,0 +1,190 @@ +"""Diagnostics support for Zaptec.""" +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from . import ZaptecUpdateCoordinator +from .api import Account +from .const import DOMAIN + +# IF this is true, the output data will be redacted. +DO_REDACT = True + +# If this is set to True, the redacted data will be included in the output. +# USE WITH CAUTION! This will include sensitive data in the output. +INCLUDE_REDACTS = False + +class Redactor: + """ Class to handle redaction of sensitive data. """ + + # Data fields that must be redacted from the output + REDACT_KEYS = [ + "Address", "City", "Latitude", "Longitude", "ZipCode", + "Pin", "SerialNo", "LogoBase64", + "Id", "CircuitId", "DeviceId", "InstallationId", "MID", "ChargerId", + "Name", "InstallationName", "SignedMeterValue", + "MacWiFi", "LteImsi", "LteIccid", "LteImei", + "NewChargeCard", + ] + + # Keys that will be looked up into the observer id dict + OBS_KEYS = [ + "SettingId", "StateId" + ] + + # Key names that will be redacted if they the dict has a OBS_KEY entry + # and it is in the REDACT_KEYS list. + VALUES = [ + "ValueAsString", "Value", + ] + + def __init__(self, redacted, acc): + self.redacted = redacted + self.acc = acc + self.redacts = {} + self.redact_info = {} + + def redact(self, text: str, make_new=None, ctx=None): + ''' Redact the text if it is present in the redacted dict. + A new redaction is created if make_new is True + ''' + if not self.redacted: + return text + elif text in self.redacts: + return self.redacts[text] + elif make_new is not None: + red = f"<--Redact #{len(self.redacts) + 1}-->" + self.redacts[text] = red + self.redact_info[red] = { # For statistics only + 'text': text, + 'from': f"{make_new} in {ctx}", + } + return red + if isinstance(text, str): + for k, v in self.redacts.items(): + if str(k) in text: + text = text.replace(k, v) + return text + + def redact_obj_inplace(self, obj, ctx=None): + ''' Iterate over obj and redact the fields. NOTE! This function + modifies the argument object in-place. + ''' + if isinstance(obj, list): + for k in obj: + self.redact_obj_inplace(k, ctx=ctx) + return obj + elif not isinstance(obj, dict): + return obj + for k, v in obj.items(): + if isinstance(v, (list, dict)): + self.redact_obj_inplace(v, ctx=ctx) + continue + obj[k] = self.redact(v, make_new=k if k in self.REDACT_KEYS else None, ctx=ctx) + return obj + + def redact_statelist(self, objs, ctx=None): + '''Redact the special state list objects.''' + for obj in objs: + for key in self.OBS_KEYS: + if key not in obj: + continue + keyv = self.acc._obs_ids.get(obj[key]) # FIXME: Access to private member + if keyv is not None: + obj[key] = f"{obj[key]} ({keyv})" + if keyv not in self.REDACT_KEYS: + continue + for value in self.VALUES: + if value not in obj: + continue + obj[value] = self.redact(obj[value], make_new=obj[key], ctx=ctx) + return objs + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + acc: Account = coordinator.account + + out = {} + api = out.setdefault('api', {}) + + # Helper to redact the output data + red = Redactor(DO_REDACT, acc) + + async def req(url): + try: + return await acc._request(url) # FIXME: Access to private member + except Exception as err: + return {"failed": str(err)} + + def gen(url, obj, ctx=None): + red.redact_obj_inplace(obj, ctx=ctx) + api[red.redact(url)] = obj + + # + # API FETCHING + # + + data = await req(url := "installation") + installation_ids = [inst['Id'] for inst in data.get('Data',[])] + gen(url, data, ctx="installation") + + circuit_ids = [] + charger_in_circuits_ids = [] + for inst_id in installation_ids: + data = await req(url := f"installation/{inst_id}/hierarchy") + + for circuit in data.get('Circuits', []): + circuit_ids.append(circuit['Id']) + for data in circuit.get('Chargers', []): + charger_in_circuits_ids.append(data['Id']) + + gen(url, data, ctx="hierarchy") + + data = await req(url := f"installation/{inst_id}") + gen(url, data, ctx="installation") + + for circ_id in circuit_ids: + data = await req(url := f"circuits/{circ_id}") + gen(url, data, ctx="circuit") + + data = await req(url := "chargers") + charger_ids = [charger['Id'] for charger in data.get('Data',[])] + gen(url, data, ctx="chargers") + + for charger_id in set([*charger_ids, *charger_in_circuits_ids]): + data = await req(url := f"chargers/{charger_id}") + gen(url, data, ctx="charger") + + data = await req(url := f"chargers/{charger_id}/state") + red.redact_statelist(data, ctx="state") + gen(url, data, ctx="state") + + data = await req(url := f"chargers/{charger_id}/settings") + red.redact_statelist(data.values(), ctx="settings") + gen(url, data, ctx="settings") + + # + # MAPPINGS + # + + out.setdefault('maps', [ + red.redact_obj_inplace(deepcopy(obj._attrs), ctx='maps') for obj in acc.map.values() + ]) + + # + # REDACTED DATA + # + if INCLUDE_REDACTS: + out.setdefault('redacts', red.redact_info) + + return out diff --git a/custom_components/zaptec/manifest.json b/custom_components/zaptec/manifest.json index 4d09883..6f6f459 100644 --- a/custom_components/zaptec/manifest.json +++ b/custom_components/zaptec/manifest.json @@ -5,8 +5,9 @@ "dependencies": [], "config_flow": true, "codeowners": [ - "hellowlol" + "hellowlol", + "sveinse" ], - "requirements": [], + "requirements": ["azure-servicebus"], "version": "0.0.6b2" -} \ No newline at end of file +} diff --git a/custom_components/zaptec/misc.py b/custom_components/zaptec/misc.py index 0ffb574..ae83814 100644 --- a/custom_components/zaptec/misc.py +++ b/custom_components/zaptec/misc.py @@ -1,11 +1,98 @@ -"""misc helper stuff""" +"""Misc helper stuff.""" +from __future__ import annotations + import re -def to_under(word) -> str: +def to_under(word: str) -> str: """helper to convert TurnOnThisButton to turn_on_this_button.""" # Ripped from inflection word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word) word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word) word = word.replace("-", "_") return word.lower() + + +def mc_nbfx_decoder(msg: bytes) -> None: + """Decoder of .NET Binary Format XML Data structures.""" + + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/mc-nbfx + + def data_producer(data: bytes): + """Generator of data.""" + count = 0 + while data: + block = data[:count] + data = data[count:] + count = (yield block) + + prod = data_producer(msg) + next(prod) + + END_ELEMENT = 0x01 + SHORT_XMLNS_ATTRIBUTE = 0x08 + SHORT_ELEMENT = 0x40 + CHARS8TEXT = 0x98 + CHARS16TEXT = 0x9a + + def read_string(bits16=False): + """Read a string.""" + if bits16: + b = prod.send(2) + length = b[0] + (b[1] << 8) + else: + length = prod.send(1)[0] + return prod.send(length).decode("utf-8") + + def frame_decoder(): + """Decode the stream.""" + while True: + try: + record_type = prod.send(1)[0] + except StopIteration: + return + + if record_type in ( + SHORT_ELEMENT, SHORT_XMLNS_ATTRIBUTE, CHARS8TEXT, CHARS16TEXT, + ): + yield record_type, read_string(bits16=(record_type == CHARS16TEXT)) + elif record_type == END_ELEMENT: + yield record_type, None + else: + raise AttributeError(f"Unknown record type {hex(record_type)}") + + root = [] + frame = frame_decoder() + while True: + try: + record_type, text = next(frame) + except StopIteration: + return root + + # Build the composite object + if record_type == SHORT_ELEMENT: + element = {} + element["name"] = text + root.append(element) + while True: + record_type, text = next(frame) + if record_type == SHORT_XMLNS_ATTRIBUTE: + element["xmlns"] = text + elif record_type in (CHARS8TEXT, CHARS16TEXT): + element["text"] = text + elif record_type == END_ELEMENT: + break + else: + raise AttributeError(f"Unknown record type {hex(record_type)}") + else: + raise AttributeError(f"Unknown record type {hex(record_type)}") + + +if __name__ == "__main__": + from pprint import pprint + + data = b'@\x06string\x083http://schemas.microsoft.com/2003/10/Serialization/\x98\xa5{"DeviceId":"ZAP000000","DeviceType":4,"ChargerId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","StateId":523,"Timestamp":"2023-07-28T09:07:19.23","ValueAsString":"0.000"}\x01' + data = b'@\x06string\x083http://schemas.microsoft.com/2003/10/Serialization/\x9a\x87\x01{"DeviceId":"ZAP000000","DeviceType":4,"ChargerId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","StateId":554,"Timestamp":"2023-08-03T00:00:00.0617Z","ValueAsString":"OCMF|{\\"FV\\":\\"1.0\\",\\"GI\\":\\"ZAPTEC GO\\",\\"GS\\":\\"ZAP000000\\",\\"GV\\":\\"2.1.0.4\\",\\"PG\\":\\"F1\\",\\"RD\\":[{\\"TM\\":\\"2023-08-03T00:00:00,000+00:00 R\\",\\"RV\\":179.715,\\"RI\\":\\"1-0:1.8.0\\",\\"RU\\":\\"kWh\\",\\"RT\\":\\"AC\\",\\"ST\\":\\"G\\"}]}"}\x01' + + out = mc_nbfx_decoder(data) + pprint(out) diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py new file mode 100644 index 0000000..490d9d4 --- /dev/null +++ b/custom_components/zaptec/number.py @@ -0,0 +1,127 @@ +"""Zaptec component number entities.""" +from __future__ import annotations + +import logging +from dataclasses import dataclass + +from homeassistant import const +from homeassistant.components.number import (NumberDeviceClass, NumberEntity, + NumberEntityDescription) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZaptecBaseEntity, ZaptecUpdateCoordinator +from .api import Installation +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZaptecNumber(ZaptecBaseEntity, NumberEntity): + + @callback + def _update_from_zaptec(self) -> None: + self._attr_native_value = self._get_zaptec_value() + self._log_value(self._attr_native_value) + + +class ZaptecAvailableCurrentNumber(ZaptecNumber): + + zaptec_obj: Installation + + def _post_init(self): + # Get the max current rating from the reported max current + self.entity_description.native_max_value = self.zaptec_obj.max_current + + async def async_set_native_value(self, value: float) -> None: + """Update to Zaptec.""" + _LOGGER.debug( + "Setting %s '%s' to '%s' in %s", + self.__class__.__qualname__, + self.key, + value, + self.zaptec_obj.id + ) + try: + await self.zaptec_obj.limit_current(availableCurrent=value) + except Exception as exc: + raise HomeAssistantError(exc) from exc + + await self.coordinator.async_request_refresh() + + +@dataclass +class ZapNumberEntityDescription(NumberEntityDescription): + + cls: type|None = None + + +INSTALLATION_ENTITIES: list[EntityDescription] = [ + ZapNumberEntityDescription( + key="available_current", + translation_key="available_current", + device_class=NumberDeviceClass.CURRENT, + native_min_value=0, + native_max_value=0, + icon="mdi:waves", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + cls=ZaptecAvailableCurrentNumber, + ), + ZapNumberEntityDescription( + key="available_current_phase1", + translation_key="available_current_phase1", + device_class=NumberDeviceClass.CURRENT, + native_min_value=0, + native_max_value=32, # FIXME: Implememt max current per phase + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + # cls=ZaptecAvailableCurrentNumber, # FIXME: Implement 3phase adjustment + ), + ZapNumberEntityDescription( + key="available_current_phase2", + translation_key="available_current_phase2", + device_class=NumberDeviceClass.CURRENT, + native_min_value=0, + native_max_value=32, # FIXME: Implememt max current per phase + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + # cls=ZaptecAvailableCurrentNumber, # FIXME: Implement 3phase adjustment + ), + ZapNumberEntityDescription( + key="available_current_phase3", + translation_key="available_current_phase3", + device_class=NumberDeviceClass.CURRENT, + native_min_value=0, + native_max_value=32, # FIXME: Implememt max current per phase + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + # cls=ZaptecAvailableCurrentNumber, # FIXME: Implement 3phase adjustment + ), +] + +CIRCUIT_ENTITIES: list[EntityDescription] = [ +] + +CHARGER_ENTITIES: list[EntityDescription] = [ +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + _LOGGER.debug("Setup numbers") + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + acc = coordinator.account + + entities = ZaptecNumber.create_from_zaptec( + acc, + coordinator, + INSTALLATION_ENTITIES, + CIRCUIT_ENTITIES, + CHARGER_ENTITIES, + ) + async_add_entities(entities, True) diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index 76da8c1..d9c27a7 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -1,228 +1,113 @@ -# pylint: disable=C0116 +"""Zaptec component sensors.""" +from __future__ import annotations -import asyncio import logging -from datetime import timedelta +from dataclasses import dataclass + +from homeassistant import const +from homeassistant.components.sensor import (SensorDeviceClass, SensorEntity, + SensorEntityDescription) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZaptecBaseEntity, ZaptecUpdateCoordinator +from .api import Account +from .const import * -import aiohttp -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import STATE_UNAVAILABLE +# pylint: disable=missing-function-docstring -# from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +_LOGGER = logging.getLogger(__name__) -from .const import * -_LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) - - -async def _update_remaps() -> None: - wanted = ["Observations"] - async with aiohttp.request("GET", CONST_URL) as resp: - if resp.status == 200: - data = await resp.json() - for k, v in data.items(): - if k in wanted: - OBSERVATIONS_REMAPS.update(v) - # Add names. - OBSERVATIONS_REMAPS.update({value: key for key, value in v.items()}) - - -async def _dry_setup(hass, config, async_add_entities, discovery_info=None): - sensors = [] - acc = hass.data[DOMAIN]["api"] - - async def callback(data): - """Callback thats executed when a new message from the message bus is in.""" - acc.update(data) - # Tell the sensor that there is an update. - async_dispatcher_send(hass, EVENT_NEW_DATA) - - # Not sure this should be added, lets see - hass.data[DOMAIN]["producer"].append(callback) - - for ins in acc.installs: - await ins.stream(cb=callback) - for circuit in ins.circuits: - # _LOGGER.debug("Building circuit %s", circuit) - c = CircuitSensor(circuit, hass) - sensors.append(c) - for charger in circuit.chargers: - _LOGGER.debug("Building charger %s", charger.id) - # Force a update before its added. - await charger.state() - chs = ChargerSensor(charger, hass) - sensors.append(chs) - sensors.append(InstallationSensor(ins, hass)) - - for charger in acc.stand_alone_chargers: - _LOGGER.debug("charger %s", charger.id) - if charger.id in acc.map: - _LOGGER.debug( - "Skipping standalone charger %s as its already exists.", charger.id - ) - continue - else: - # _LOGGER.debug("Building charger %s", charger) - # Force an update before its added. - await charger.state() - chs = ChargerSensor(charger, hass) - sensors.append(chs) - - async_add_entities(sensors, False) - - return True - - -async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None -) -> None: # pylint: disable=W0613 - return True - - -async def async_setup_entry(hass, config_entry, async_add_devices): - return await _dry_setup(hass, config_entry.data, async_add_devices) - - -class ZapMixin: - _attr_device_info = None - _attr_unique_id = None - _attr_has_entity_name = True - _attr_name = None - _attr_native_value = None - - async def _real_update(self): - _LOGGER.debug("Called _real_update for %s", self.__class__.__name__) - # The api already updated and have new data available. - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - await super().async_added_to_hass() - _LOGGER.debug("called async_added_to_hass %s", self.__class__.__name__) - self.async_on_remove( - async_dispatcher_connect(self._hass, EVENT_NEW_DATA, self._real_update) - ) - - @property - def should_pull(self): - return False - - -class CircuitSensor(ZapMixin, SensorEntity): - def __init__(self, circuit, hass): - self._api = circuit - self._attrs = circuit._attrs - self._hass = hass - - @property - def name(self) -> str: - return "zaptec_circuit_%s" % self._api._attrs["id"] - - @property - def extra_state_attributes(self) -> dict: - return self._attrs - - @property - def unique_id(self): - return f"zaptec_{self._attrs['id']}".lower() - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": DOMAIN, - } - - @property - def state(self): - return self._attrs["active"] - - -class InstallationSensor(ZapMixin, SensorEntity): - def __init__(self, api, hass): - self._api = api - self._attrs = api._attrs - self._hass = hass - - @property - def name(self) -> str: - return "zaptec_installation_%s" % self._attrs["id"] - - @property - def extra_state_attributes(self) -> dict: - return self._attrs - - @property - def unique_id(self): - return f"zaptec_{self._attrs['id']}".lower() - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": DOMAIN, - } - - @property - def state(self): - return self._attrs["active"] - - -class ChargerSensor(ZapMixin, SensorEntity): - def __init__(self, api, hass) -> None: - self._api = api - self._hass = hass - self._attrs = api._attrs - self._state = STATE_UNAVAILABLE - - @property - def name(self) -> str: - return f"zaptec_charger_{self._api.id}".lower() - - @property - def icon(self) -> str: - return "mdi:ev-station" - - @property - def entity_picture(self) -> str: - return CHARGE_MODE_MAP[self._attrs["operating_mode"]][1] - - @property - def unique_id(self): - return f"{DOMAIN}_{self._attrs['id']}_chargers".lower() - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": DOMAIN, - } - - @property - def state(self): - return self._state - - @property - def extra_state_attributes(self) -> dict: - return self._attrs - - async def _real_update(self): - _LOGGER.debug("Called _real_update for %s", self.__class__.__name__) - # The api already updated and have new data available. - try: - value = CHARGE_MODE_MAP[self._attrs["operating_mode"]][0] - self._state = value - except KeyError: - # This seems to happen when it starts up. - self._state = STATE_UNAVAILABLE - - self.async_write_ha_state() +class ZaptecSensor(ZaptecBaseEntity, SensorEntity): + + @callback + def _update_from_zaptec(self) -> None: + self._attr_native_value = self._get_zaptec_value() + self._log_value(self._attr_native_value) + + +class ZaptecChargeSensor(ZaptecSensor): + + @callback + def _update_from_zaptec(self) -> None: + state = self._get_zaptec_value() + mode = CHARGE_MODE_MAP.get(state, CHARGE_MODE_MAP["Unknown"]) + self._attr_native_value = mode[0] + self._attr_icon = mode[1] + self._log_value(self._attr_native_value) + + +@dataclass +class ZapSensorEntityDescription(SensorEntityDescription): + """Provide a description of a Zaptec sensor.""" + + cls: type|None = None + + +INSTALLATION_ENTITIES: list[EntityDescription] = [ +] + +CIRCUIT_ENTITIES: list[EntityDescription] = [ + ZapSensorEntityDescription( + key="max_current", + translation_key="max_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=const.EntityCategory.DIAGNOSTIC, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), +] + +CHARGER_ENTITIES: list[EntityDescription] = [ + ZapSensorEntityDescription( + key="operating_mode", + translation_key="operating_mode", + device_class=SensorDeviceClass.ENUM, + options=[x[0] for x in CHARGE_MODE_MAP.values()], + icon="mdi:ev-station", + cls=ZaptecChargeSensor, + ), + ZapSensorEntityDescription( + key="current_phase1", + translation_key="current_phase1", + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), + ZapSensorEntityDescription( + key="current_phase2", + translation_key="current_phase2", + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), + ZapSensorEntityDescription( + key="current_phase3", + translation_key="current_phase3", + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + _LOGGER.debug("Setup sensors") + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + acc: Account = coordinator.account + + entities = ZaptecSensor.create_from_zaptec( + acc, + coordinator, + INSTALLATION_ENTITIES, + CIRCUIT_ENTITIES, + CHARGER_ENTITIES, + ) + async_add_entities(entities, True) diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index f2e747b..31f6394 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -1,13 +1,21 @@ -import asyncio +"""Zaptec components services.""" +from __future__ import annotations + import logging +from typing import Awaitable, Callable import voluptuous as vol +from homeassistant.core import HomeAssistant, ServiceCall +from .api import Account, Charger, Installation from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +TServiceHandler = Callable[[ServiceCall], Awaitable[None]] +# SCHEMAS for services +# ==================== has_id_schema = vol.Schema({vol.Required("charger_id"): str}) has_limit_current_schema = vol.Schema(vol.SomeOf( @@ -27,85 +35,74 @@ ])) -async def async_setup_services(hass): +async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for the Plex component.""" - acc = hass.data[DOMAIN]["api"] - _LOGGER.debug("Setting up services.") + _LOGGER.debug("Set up services") + acc: Account = hass.data[DOMAIN]["api"] - # just the new one for now. - # # require firmware > 3.2 - async def service_handle_stop_pause(service_call): - _LOGGER.debug("called new stop pause") + async def service_handle_stop_pause(service_call: ServiceCall) -> None: + _LOGGER.debug("Called stop pause") charger_id = service_call.data["charger_id"] - return await acc.map[charger_id].stop_pause() + charger: Charger = acc.map[charger_id] + await charger.command("stop_pause") - async def service_handle_resume_charging(service_call): - _LOGGER.debug("service new start and or resume") + async def service_handle_resume_charging(service_call: ServiceCall) -> None: + _LOGGER.debug("Called start and or resume") charger_id = service_call.data["charger_id"] - return await acc.map[charger_id].resume_charging() + charger: Charger = acc.map[charger_id] + await charger.command("resume_charging") - # Add old one to see if they even work. - async def service_handle_start_charging(service_call): - _LOGGER.debug("service old start") + async def service_handle_authorize_charging(service_call: ServiceCall) -> None: + _LOGGER.debug("Called authorize charging") charger_id = service_call.data["charger_id"] - cmd = f"chargers/{charger_id}/SendCommand/501" - return await acc._request(cmd, method="post") + charger: Charger = acc.map[charger_id] + await charger.command("authorize_charge") - async def service_handle_stop_charging(service_call): - _LOGGER.debug("service old stop") + async def service_handle_deauthorize_charging(service_call: ServiceCall) -> None: + _LOGGER.debug("Called deauthorize charging and stop") charger_id = service_call.data["charger_id"] - cmd = f"chargers/{charger_id}/SendCommand/502" - return await acc._request(cmd, method="post") + charger: Charger = acc.map[charger_id] + await charger.command("deauthorize_stop") - async def service_handle_restart_charger(service_call): - _LOGGER.debug("service restart_charger") + async def service_handle_restart_charger(service_call: ServiceCall) -> None: + _LOGGER.debug("Called restart charger") charger_id = service_call.data["charger_id"] - return await acc.map[charger_id].restart_charger() + charger: Charger = acc.map[charger_id] + await charger.command("restart_charger") - async def service_handle_update_firmware(service_call): - _LOGGER.debug("service update_firmware") + async def service_handle_update_firmware(service_call: ServiceCall) -> None: + _LOGGER.debug("Called update firmware") charger_id = service_call.data["charger_id"] - return await acc.map[charger_id].update_firmware() + charger: Charger = acc.map[charger_id] + await charger.command("upgrade_firmware") - async def service_handle_limit_current(service_call): - _LOGGER.debug("update current limit") + async def service_handle_limit_current(service_call: ServiceCall) -> None: + _LOGGER.debug("Called set current limit") installation_id = service_call.data["installation_id"] available_current = service_call.data.get("available_current") available_current_phase1 = service_call.data.get("available_current_phase1") available_current_phase2 = service_call.data.get("available_current_phase2") available_current_phase3 = service_call.data.get("available_current_phase3") - return await acc.map[installation_id].limit_current( + installation: Installation = acc.map[installation_id] + await installation.limit_current( availableCurrent=available_current, availableCurrentPhase1=available_current_phase1, availableCurrentPhase2=available_current_phase2, availableCurrentPhase3=available_current_phase3, ) - hass.services.async_register( - DOMAIN, "stop_pause_charging", service_handle_stop_pause, schema=has_id_schema - ) - - hass.services.async_register( - DOMAIN, "resume_charging", service_handle_resume_charging, schema=has_id_schema - ) - - hass.services.async_register( - DOMAIN, "start_charging", service_handle_start_charging, schema=has_id_schema - ) - - hass.services.async_register( - DOMAIN, "stop_charging", service_handle_stop_charging, schema=has_id_schema - ) - - hass.services.async_register( - DOMAIN, "restart_charger", service_handle_restart_charger, schema=has_id_schema - ) - - hass.services.async_register( - DOMAIN, "update_firmware", service_handle_update_firmware, schema=has_id_schema - ) - - hass.services.async_register( - DOMAIN, "limit_current", service_handle_limit_current, schema=has_limit_current_schema - ) + # LIST OF SERVICES + services: list[tuple[str, vol.Schema, TServiceHandler]] = [ + ("stop_pause_charging", has_id_schema, service_handle_stop_pause), + ("resume_charging", has_id_schema, service_handle_resume_charging), + ("authorize_charging", has_id_schema, service_handle_authorize_charging), + ("deauthorize_charging", has_id_schema, service_handle_deauthorize_charging), + ("restart_charger", has_id_schema, service_handle_restart_charger), + ("update_firmware", has_id_schema, service_handle_update_firmware), + ("limit_current", has_limit_current_schema, service_handle_limit_current), + ] + + # Register the services + for name, schema, handler in services: + hass.services.async_register(DOMAIN, name, handler, schema=schema) diff --git a/custom_components/zaptec/services.yaml b/custom_components/zaptec/services.yaml index 861d9fa..ef4abcb 100644 --- a/custom_components/zaptec/services.yaml +++ b/custom_components/zaptec/services.yaml @@ -1,43 +1,49 @@ -start_charging: - description: "Starts charger. Also authorizes if configured to use authorization." +stop_pause_charging: + description: "Stop/pause charging" fields: charger_id: + required: true description: "The charger id" example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" -stop_charging: - description: "Starts charger. Also authorizes if configured to use authorization." +resume_charging: + description: "Resume/start charging" fields: charger_id: + required: true description: "The charger id" example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" -stop_pause_charging: - description: "New stop/pause" +authorize_charging: + description: "Authorize charging" fields: charger_id: + required: true description: "The charger id" example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" -resume_charging: - description: "New resume command." +deauthorize_charging: + description: "Deauthorize charging and stop" fields: charger_id: + required: true description: "The charger id" example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" -update_firmware: - description: "New resume command." +restart_charger: + description: "Restart charger." fields: charger_id: - description: "Update the firmware." + required: true + description: "The charger id" example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" -restart_charger: - description: "Restart charger." +update_firmware: + description: "New resume command." fields: charger_id: - description: "The charger id" + required: true + description: "Update the firmware." example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" limit_current: diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index e6e3216..6a5ec15 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -1,100 +1,93 @@ -"""Switch platform for blueprint.""" -import logging - -from homeassistant.components.switch import SwitchEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +"""Switch platform for Zaptec.""" +from __future__ import annotations -# from . import SWITCH_SCHEMA_ATTRS +import logging +from dataclasses import dataclass + +from homeassistant.components.switch import (SwitchDeviceClass, SwitchEntity, + SwitchEntityDescription) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZaptecBaseEntity, ZaptecUpdateCoordinator +from .api import Account, Charger from .const import CHARGE_MODE_MAP, DOMAIN _LOGGER = logging.getLogger(__name__) -# Are there some other shit we are supposed to use instead? -# PLATFORM_SCHEMA.extend(SWITCH_SCHEMA_ATTRS) +class ZaptecSwitch(ZaptecBaseEntity, SwitchEntity): -async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None -) -> bool: # pylint: disable=unused-argument - """Setup switch platform.""" - return True + @callback + def _update_from_zaptec(self) -> None: + self._attr_is_on = bool(self._get_zaptec_value()) + self._log_value(self._attr_is_on) -async def async_setup_entry(hass, config_entry, async_add_devices): - """ "Setup the switch using ui.""" - _LOGGER.debug("Setup switch for zaptec") - acc = hass.data.get(DOMAIN, {}).get("api") - if acc is None: - _LOGGER.debug("Didn't setup switch the api wasnt ready") - return False +class ZaptecChargeSwitch(ZaptecSwitch): - switches = [] - chargers = [c for c in acc.map.values() if c and c.__class__.__name__ == "Charger"] + zaptec_obj: Charger - for c in chargers: - switches.append(Switch(c, hass)) + @callback + def _update_from_zaptec(self) -> None: + state = self._get_zaptec_value() + self._attr_is_on = state in ["Connected_Charging"] + self._log_value(self._attr_is_on) - async_add_devices(switches, False) - return True + async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument + """Turn on the switch.""" + try: + await self.zaptec_obj.command('resume_charging') + except Exception as exc: + raise HomeAssistantError(exc) from exc + async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument + """Turn off the switch.""" + try: + await self.zaptec_obj.command('stop_pause') + except Exception as exc: + raise HomeAssistantError(exc) from exc -class Switch(SwitchEntity): - """switch class.""" - def __init__(self, api, hass) -> None: - self._api = api - self._status = False - # wft is this supposed to be? - self._name = "zaptec_%s_switch" % api.id - self._mode = "" - self._hass = hass +@dataclass +class ZapSwitchEntityDescription(SwitchEntityDescription): - async def async_update(self) -> None: - """Update the switch.""" + cls: type|None = None - try: - value = CHARGE_MODE_MAP[self._api._attrs["operating_mode"]][0] - _LOGGER.info( - "Trying to update the switch raw value %s %s", - self._api._attrs["operating_mode"], - CHARGE_MODE_MAP[self._api._attrs["operating_mode"]][0], - ) - return value - except KeyError: - # This seems to happen when it starts up. - _LOGGER.debug("Switch value is unknowns") - return "unknown" - async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument - """Turn on the switch.""" - return await self._api.resume_charging() +INSTALLATION_SWITCH_TYPES: list[EntityDescription] = [ +] - async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument - """Turn off the switch.""" - return await self._api.stop_pause() - - @property - def name(self) -> str: - """Return the name of the switch.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon of this switch.""" - return "" # <-- what should this be? - - @property - def is_on(self) -> bool: - """Return true if the switch is on.""" - if self._api._attrs.get("operating_mode") in [3]: - return True - return False - - # @property - # def state(self) -> bool: - # return self.is_on - - @property - def extra_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._api._attrs +CIRCUIT_SWITCH_TYPES: list[EntityDescription] = [ +] + +CHARGER_SWITCH_TYPES: list[EntityDescription] = [ + ZapSwitchEntityDescription( + key="operating_mode", + translation_key="charging", + device_class=SwitchDeviceClass.SWITCH, + cls=ZaptecChargeSwitch, + ), + # FIXME: Implement a authentication required switch +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + _LOGGER.debug("Setup switches") + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + acc = coordinator.account + + switches = ZaptecSwitch.create_from_zaptec( + acc, + coordinator, + INSTALLATION_SWITCH_TYPES, + CIRCUIT_SWITCH_TYPES, + CHARGER_SWITCH_TYPES, + ) + async_add_entities(switches, True) diff --git a/custom_components/zaptec/translations/en.json b/custom_components/zaptec/translations/en.json index 59c3769..b15e1f1 100644 --- a/custom_components/zaptec/translations/en.json +++ b/custom_components/zaptec/translations/en.json @@ -19,5 +19,36 @@ "abort":{ "single_instance_allowed":"Only a single instance is allowed." } + }, + "entity": { + "sensor": { + "max_current": { "name": "Max current" }, + "charger_operation_mode": { "name": "Charger operation mode" }, + "operating_mode": { "name": "Charger mode" }, + "current_phase1": { "name": "Current phase 1" }, + "current_phase2": { "name": "Current phase 2" }, + "current_phase3": { "name": "Current phase 3" } + }, + "number": { + "available_current": { "name": "Available current" }, + "available_current_phase1": { "name": "Available current phase 1" }, + "available_current_phase2": { "name": "Available current phase 2" }, + "available_current_phase3": { "name": "Available current phase 3" } + }, + "binary_sensor": { + "is_authorization_required": { "name": "Authorization required" }, + "permanent_cable_lock": { "name": "Permanent cable lock" } + }, + "button": { + "resume_charging": { "name": "Resume charging" }, + "stop_pause": { "name": "Stop/pause charging" }, + "restart_charger": { "name": "Restart charger" }, + "update_firmware": { "name": "Update firmware" }, + "authorize_charge": { "name": "Authorize charging" }, + "deauthorize_stop": { "name": "Deauthorize charging"} + }, + "switch": { + "charging": { "name": "Charging" } + } } } From 856249520151e1595ec648af83b700828c1763d7 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Thu, 14 Sep 2023 16:14:52 +0200 Subject: [PATCH 02/17] Improvements * Improved API and interface. Added data validation. Added type conversion. * Added several new HA entities * Fix bugs and smaller improvements --- custom_components/zaptec/__init__.py | 35 +- custom_components/zaptec/api.py | 543 +++++++++++------- custom_components/zaptec/binary_sensor.py | 8 +- custom_components/zaptec/button.py | 19 +- custom_components/zaptec/diagnostics.py | 64 ++- custom_components/zaptec/manifest.json | 2 +- custom_components/zaptec/number.py | 81 ++- custom_components/zaptec/sensor.py | 63 ++ custom_components/zaptec/services.py | 14 +- custom_components/zaptec/switch.py | 67 ++- custom_components/zaptec/translations/en.json | 24 +- custom_components/zaptec/update.py | 84 +++ custom_components/zaptec/validate.py | 161 ++++++ 13 files changed, 869 insertions(+), 296 deletions(-) create mode 100644 custom_components/zaptec/update.py create mode 100644 custom_components/zaptec/validate.py diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index 9fc7da4..290e6b5 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -36,6 +36,7 @@ # Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ] # FIXME: Informing users that the interface is considerable different @@ -204,14 +205,29 @@ def _update_from_zaptec_failed(self) -> None: ''' @callback - def _get_zaptec_value(self, default=MISSING): + def _get_zaptec_value(self, *, default=MISSING, key=None): '''Helper to retrieve the value from the Zaptec object. This is to be called from _handle_coordinator_update() in the inheriting class. It will fetch the attr given by the entity description key. ''' - if default is MISSING: - return getattr(self.zaptec_obj, self.key) - return getattr(self.zaptec_obj, self.key, default) + obj = self.zaptec_obj + key = key or self.key + for k in key.split('.'): + # Do dict because some object contains sub-dicts which must + # be handled differently than attributes + if isinstance(obj, dict): + if default is MISSING: + obj = obj[k] + else: + obj = obj.get(k, default) + else: + if default is MISSING: + obj = getattr(obj, k) + else: + obj = getattr(obj, k, default) + if obj is default: + return obj + return obj @callback def _log_value(self, value, force=False): @@ -219,13 +235,14 @@ def _log_value(self, value, force=False): _handle_coordinator_update() in the inheriting class. ''' prev = self._prev_value - self._prev_value = value if force or value != prev: + self._prev_value = value # Only logs when the value changes - _LOGGER.debug(" %s.%s = %s (%s)", - self.__class__.__qualname__, - self.key, - value, type(value)) + _LOGGER.debug(" %s.%s = <%s> %s (in %s)", + self.__class__.__qualname__, self.key, + type(value).__qualname__, value, + self.zaptec_obj.id, + ) @classmethod def create_from( diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index f27db9d..a4b9b2c 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -8,6 +8,7 @@ from collections.abc import Iterable from concurrent.futures import CancelledError from functools import partial +from typing import Any, Callable import aiohttp import async_timeout @@ -20,6 +21,12 @@ _LOGGER = logging.getLogger(__name__) +# Set to True to debug log all API calls +DEBUG_API_CALLS = False + +# Set to True to debug log all API errors +DEBUG_API_ERRORS = True + """ stuff are missing from the api docs compared to what the portal uses. circuits/{self.id}/live @@ -34,6 +41,7 @@ from const import (API_RETRIES, API_URL, CONST_URL, FALSY, MISSING, TOKEN_URL, TRUTHY) from misc import mc_nbfx_decoder, to_under + from validate import validate # remove me later logging.basicConfig(level=logging.DEBUG) @@ -43,6 +51,7 @@ from .const import (API_RETRIES, API_URL, CONST_URL, FALSY, MISSING, TOKEN_URL, TRUTHY) from .misc import mc_nbfx_decoder, to_under + from .validate import validate class ZaptecApiError(Exception): @@ -62,29 +71,35 @@ class RequestRetryError(ZaptecApiError): class ZaptecBase(ABC): + """Base class for Zaptec objects""" id: str name: str _account: "Account" _attrs: TDict + # Type definitions and convertions on the attributes + ATTR_TYPES: dict[str, Callable] = {} + def __init__(self, data: TDict, account: "Account") -> None: self._account = account self._attrs = {} self.set_attributes(data) def set_attributes(self, data: TDict) -> bool: - newdata = False + """Set the class attributes from the given data""""" for k, v in data.items(): + # Cast the value to the correct type new_key = to_under(k) + new_v = self.ATTR_TYPES.get(new_key, lambda x: x)(v) + new_vt = type(new_v).__qualname__ if new_key not in self._attrs: - _LOGGER.debug(">>> Adding %s.%s (%s) = %s", self.__class__.__qualname__, new_key, k, v) - newdata = True - elif self._attrs[new_key] != v: - _LOGGER.debug(">>> Updating %s.%s (%s) = %s (was %s)", self.__class__.__qualname__, new_key, k, v, self._attrs[new_key]) - newdata = True - self._attrs[new_key] = v - return newdata + qn = self.__class__.__qualname__ + _LOGGER.debug(">>> Adding %s.%s (%s) = <%s> %s", qn, new_key, k, new_vt, new_v) + elif self._attrs[new_key] != new_v: + qn = self.__class__.__qualname__ + _LOGGER.debug(">>> Updating %s.%s (%s) = <%s> %s (was %s)", qn, new_key, k, new_vt, new_v, self._attrs[new_key]) + self._attrs[new_key] = new_v def __getattr__(self, key): try: @@ -92,6 +107,10 @@ def __getattr__(self, key): except KeyError as exc: raise AttributeError(exc) from exc + def asdict(self): + """Return the attributes as a dict""" + return self._attrs + @abstractmethod async def build(self) -> None: """Build the object""" @@ -106,6 +125,11 @@ class Installation(ZaptecBase): circuits: list[Circuit] + # Type conversions for the named attributes + ATTR_TYPES = { + "active": bool, + } + def __init__(self, data, account): super().__init__(data, account) self.connection_details = None @@ -115,50 +139,33 @@ def __init__(self, data, account): self._stream_receiver = None async def build(self): - data = await self._account._req_hierarchy(self.id) + """Build the installation object hierarchy.""" + + # Get state to ensure we have the full and updated data + await self.state() + + # Get the hierarchy of circurits and chargers + hierarchy = await self._account._request(f"installation/{self.id}/hierarchy") circuits = [] - for item in data["Circuits"]: - circ = Circuit(item, self._account) + for item in hierarchy["Circuits"]: _LOGGER.debug(" Circuit %s", item["Id"]) + circ = Circuit(item, self._account) self._account.register(item["Id"], circ) await circ.build() circuits.append(circ) + self.circuits = circuits async def state(self): _LOGGER.debug("Polling state for %s installation (%s)", self.id, self._attrs.get('name')) - data = await self._account._req_installation(self.id) + data = await self.installation_info() self.set_attributes(data) - async def limit_current(self, **kwargs): - """Set a limit now how many amps the installation can use - Use availableCurrent for setting all phases at once. Use - availableCurrentPhase* to set each phase individually. - """ - has_availablecurrent = "availableCurrent" in kwargs - has_availablecurrentphases = all( - k in kwargs for k in ( - "availableCurrentPhase1", - "availableCurrentPhase2", - "availableCurrentPhase3", - ) - ) - - if not (has_availablecurrent ^ has_availablecurrentphases): - raise ValueError("Either availableCurrent or all of availableCurrentPhase1, availableCurrentPhase2, availableCurrentPhase3 must be set") - - data = await self._account._request( - f"installation/{self.id}/update", method="post", data=kwargs - ) - # FIXME: Verify assumed data structure - return data - async def live_stream_connection_details(self): data = await self._account._request( f"installation/{self.id}/messagingConnectionDetails" ) - # FIXME: Verify assumed data structure self.connection_details = data return data @@ -177,6 +184,7 @@ async def stream(self, cb=None) -> asyncio.Task: return self._stream_task async def _stream(self, cb=None): + """Main stream handler""" try: try: from azure.servicebus.aio import ServiceBusClient @@ -187,10 +195,11 @@ async def _stream(self, cb=None): return # Get connection details + # FIXME: The validator will fail if not all fields are set, so how to handle this? conf = await self.live_stream_connection_details() # Check if we can use it. - if any(True for i in ["Password", "Username", "Host"] if conf.get(i) == ""): + if any(True for i in ["Password", "Username", "Host"] if not conf.get(i)): _LOGGER.warning( "Cant enable live update using the servicebus, enable it in the zaptec portal" ) @@ -213,7 +222,8 @@ async def _stream(self, cb=None): self._stream_receiver = receiver async with receiver: async for msg in receiver: - binmsg = "" # For the exception in case it fails before setting the value + # For the exception in case it fails before setting the value + binmsg = "" try: # After some blind research it seems the messages # are encoded with .NET binary xml format (MC-NBFX) @@ -276,54 +286,164 @@ async def cancel_stream(self): finally: self._stream_task = None + #----------------- + # API methods + #----------------- + + async def installation_info(self) -> TDict: + '''Raw request for installation data''' + + # Get the installation data + data = await self._account._request(f"installation/{self.id}") + + # Remove data fields with excessive data, making it bigger than the + # HA database appreciates for the size of attributes. + # FIXME: SupportGroup is sub dict. This is not within the declared type + supportgroup = data.get('SupportGroup') + if supportgroup is not None: + if "LogoBase64" in supportgroup: + logo = supportgroup["LogoBase64"] + supportgroup["LogoBase64"] = f"" + + return data + + async def set_limit_current(self, **kwargs): + """Set a limit now how many amps the installation can use + Use availableCurrent for setting all phases at once. Use + availableCurrentPhase* to set each phase individually. + """ + has_availablecurrent = "availableCurrent" in kwargs + has_availablecurrentphases = all( + k in kwargs for k in ( + "availableCurrentPhase1", + "availableCurrentPhase2", + "availableCurrentPhase3", + ) + ) + + if not (has_availablecurrent ^ has_availablecurrentphases): + raise ValueError( + "Either availableCurrent or all of availableCurrentPhase1, " + "availableCurrentPhase2, availableCurrentPhase3 must be set" + ) + + data = await self._account._request( + f"installation/{self.id}/update", + method="post", data=kwargs + ) + return data + + async def set_authenication_required(self, required: bool): + """Set if authorization is required for charging""" + + # Undocumented feature, but the WEB API uses it. It fetches the + # installation data and updates IsAuthorizationRequired field + data = { + "Id": self.id, + "IsRequiredAuthentication": required, + } + result = await self._account._request( + f"installation/{self.id}", + method="put", data=data + ) + return result class Circuit(ZaptecBase): """Represents a circuits""" chargers: list["Charger"] - _chargers: list[TDict] + + # Type conversions for the named attributes + ATTR_TYPES = { + "is_active": bool, + "is_authorisation_required": bool, + } def __init__(self, data, account): super().__init__(data, account) self.chargers = [] - self._chargers = data.get("Chargers", []) or [] async def build(self): """Build the python interface.""" + + # Get state to ensure we have the full and updated data + await self.state() + chargers = [] - for item in self._chargers: - data = await self._account._req_charger(item["Id"]) - chg = Charger(data, self._account) + for item in self._attrs['chargers']: _LOGGER.debug(" Charger %s", item["Id"]) + chg = Charger(item, self._account) self._account.register(item["Id"], chg) await chg.build() chargers.append(chg) + self.chargers = chargers async def state(self): _LOGGER.debug("Polling state for %s cicuit (%s)", self.id, self._attrs.get('name')) - data = await self._account._req_circuit(self.id) + data = await self.circuit_info() self.set_attributes(data) + #----------------- + # API methods + #----------------- + + async def circuit_info(self) -> TDict: + '''Raw request for circuit data''' + data = await self._account._request(f"circuits/{self.id}") + return data + class Charger(ZaptecBase): """Represents a charger""" + # Type conversions for the named attributes + ATTR_TYPES = { + "active": bool, + "is_authorization_required": lambda x: x in TRUTHY, + "is_online": lambda x: x in TRUTHY, + "charge_current_installation_max_limit": float, + "charger_max_current": float, + "charger_min_current": float, + "completed_session": json.loads, + "current_phase1": float, + "current_phase2": float, + "current_phase3": float, + "permanent_cable_lock": lambda x: x in TRUTHY, + "total_charge_power": float, + "voltage_phase1": float, + "voltage_phase2": float, + "voltage_phase3": float, + } + + def __init__(self, data: TDict, account: "Account") -> None: + super().__init__(data, account) + + # Append the attr types that depends on self + attr_types = self.ATTR_TYPES.copy() + attr_types.update({ + "operating_mode": self.type_operation_mode, + "charger_operation_mode": self.type_operation_mode, + }) + self.ATTR_TYPES = attr_types + async def build(self) -> None: '''Build the object''' + # Don't update state at build, because the state and settings ids + # is not loaded yet. + async def state(self): '''Update the charger state''' _LOGGER.debug("Polling state for %s charger (%s)", self.id, self._attrs.get('name')) - # FIXME: This has multiple set_attributes that might compete for the same key. Fix this. - # FIXME: E.g. is_online is ambulating between True and 1 + # Get the main charger info + charger = await self.charger_info() + self.set_attributes(charger) - data = await self._account._req_charger(self.id) - self.set_attributes(data) - - data = await self._account._req_charger_state(self.id) - data = Account._state_to_attrs(data, 'StateId', self._account._obs_ids) + # Get the state from the charger + state = await self._account._request(f"chargers/{self.id}/state") + data = Account._state_to_attrs(state, 'StateId', self._account._obs_ids) self.set_attributes(data) # Firmware version is called. SmartMainboardSoftwareApplicationVersion, @@ -332,12 +452,15 @@ async def state(self): # maybe remove this later if it dont interest ppl. # Fetch some additional attributes from settings - data = await self._account._req_charger_settings(self.id) - data = Account._state_to_attrs(data.values(), 'SettingId', self._account._set_ids) + settings = await self._account._request(f"chargers/{self.id}/settings") + data = Account._state_to_attrs(settings.values(), 'SettingId', self._account._set_ids) self.set_attributes(data) if self.installation_id in self._account.map: - firmware_info = await self._account._req_charger_firmware(self.installation_id) + firmware_info = await self._account._request( + f"chargerFirmware/installation/{self.installation_id}" + ) + for fm in firmware_info: if fm["ChargerId"] == self.id: self.set_attributes({ @@ -346,81 +469,18 @@ async def state(self): "firmware_update_to_date": fm["IsUpToDate"], }) - async def command(self, command:str): - - # FIXME: Use the names from the constant json? - - # All methods needs to be checked again - COMMANDS = { - "restart_charger": 102, - "restart_mcu": 103, - "update_settings": 104, - "restart_ntp": 105, - "exit_app_with_code": 106, - "upgrade_firmware": 200, - "upgrade_firmware_forced": 201, - "reset_com_errors": 260, - "reset_notifications": 261, - "reset_com_warnings": 262, - "local_settings": 260, - "set_plc_npw": 320, - "set_plc_cocode": 321, - "set_plc_nmk": 322, - "set_remote_plc_nmk": 323, - "set_remote_plc_npw": 324, - "start_charging": 501, - "stop_charging": 502, - "report_charging_state": 503, - "set_session_id": 504, - "set_user_uuid": 505, - "stop_pause": 506, # Require firmware > 3.2 - "resume_charging": 507, # Require firmware > 3.2 - "show_granted": 601, - "show_denied": 602, - "indicate_app_connect": 603, - "confirm_charge_card_added": 750, - "set_authentication_list": 751, - "debug": 800, - "get_plc_topology": 801, - "reset_plc": 802, - "remote_command": 803, - "run_grid_test": 804, - "run_post_production_test": 901, - "combined_min": 10000, - "deauthorize_stop": 10001, - "combined_max": 10999, - "authorize_charge": None, # Special case - } - - if command not in COMMANDS: - raise ValueError(f"Unknown command {command}") - - if command == "authorize_charge": - data = await self._account._request(f"chargers/{self.id}/authorizecharge", method="post") - # FIXME: Verify assumed data structure - return data - - _LOGGER.debug("Command %s", command) - cmd = f"chargers/{self.id}/SendCommand/{COMMANDS[command]}" - _LOGGER.debug("Calling %s", cmd) - data = await self._account._request(cmd, method="post") - # FIXME: Verify assumed data structure - return data - async def live(self): - # This don't seems to be documented but the portal uses it - # TODO check what it returns and parse it to attributes - data = await self._account._request("chargers/%s/live" % self.id) - # FIXME: Verify assumed data structure - return data + # FIXME: Is this an experiment? Omit? - async def settings(self): - # TODO check what it returns and parse it to attributes - data = await self._account._request("chargers/%s/settings" % self.id) - # FIXME: Verify assumed data structure + # This don't seems to be documented but the portal uses it + # FIXME check what it returns and parse it to attributes + data = await self._account._request(f"chargers/{self.id}/live") + # FIXME: Missing validator (see validate) return data async def update(self, data): + # FIXME: Is this in use or an experiment? Should it be removed from production code? + # https://api.zaptec.com/help/index.html#/Charger/post_api_chargers__id__update # Not really sure this should be added as ppl might use it wrong cmd = f"chargers/{self.id}/update" @@ -455,25 +515,76 @@ async def update(self, data): # return await self._account.request(cmd, data=data, method="post") - @property - def is_authorization_required(self): - return self._attrs["is_authorization_required"] in TRUTHY - - @property - def permanent_cable_lock(self): - return self._attrs["permanent_cable_lock"] in TRUTHY - - @property - def operating_mode(self): + def type_operation_mode(self, v): modes = {str(v): k for k, v in self._account._const["ChargerOperationModes"].items()} - v = self._attrs["operating_mode"] return modes.get(str(v), str(v)) - @property - def charger_operation_mode(self): - modes = {str(v): k for k, v in self._account._const["ChargerOperationModes"].items()} - v = self._attrs["charger_operation_mode"] - return modes.get(str(v), str(v)) + #----------------- + # API methods + #----------------- + + async def charger_info(self) -> TDict: + data = await self._account._request(f"chargers/{self.id}") + return data + + async def command(self, command: str): + """Send a command to the charger""" + + cmdid = self._account._cmd_ids.get(command) + if cmdid is None: + raise ValueError(f"Unknown command {command}") + + _LOGGER.debug("Command %s (%s)", command, cmdid) + data = await self._account._request( + f"chargers/{self.id}/SendCommand/{cmdid}", + method="post" + ) + return data + + async def set_settings(self, settings: dict[str, Any]): + """Set settings on the charger""" + + set_ids = self._account._set_ids + values = [{'id': set_ids.get(k), 'value': v} for k, v in settings.items()] + + if any(d for d in values if d['id'] is None): + raise ValueError(f"Unknown setting '{settings}'") + + _LOGGER.debug("Settings %s", settings) + data = await self._account._request( + f"chargers/{self.id}/settings", + method="post", data=values + ) + return data + + async def stop_pause(self): + return await self.command("stop_pause") + + async def resume_charging(self): + return await self.command("resume_charging") + + async def deauthorize_stop(self): + return await self.command("deauthorize_stop") + + async def restart_charger(self): + return await self.command("restart_charger") + + async def upgrade_firmware(self): + return await self.command("upgrade_firmware") + + async def authorize_charge(self): + _LOGGER.debug("Authorize charge") + data = await self._account._request( + f"chargers/{self.id}/authorizecharge", + method="post" + ) + return data + + async def set_current_in_minimum(self, value): + return await self.set_settings({"current_in_minimum": value}) + + async def set_current_in_maxium(self, value): + return await self.set_settings({"current_in_maximum": value}) class Account: @@ -492,6 +603,7 @@ def __init__(self, username: str, password: str, client=None) -> None: self._const = {} self._obs_ids = {} self._set_ids = {} + self._cmd_ids = {} self.is_built = False if client is None: @@ -541,99 +653,78 @@ async def _refresh_token(self): raise AuthorizationError("Failed to refresh token, check your credentials.") async def _request(self, url: str, method="get", data=None, iteration=1): + + def log_request(): + try: + _LOGGER.debug(f"@@@ REQUEST {method.upper()} to '{full_url}' length {len(data or '')}") + if data: + _LOGGER.debug(f" content {data}") + except Exception as err: + _LOGGER.exception("Failed to log response") + + async def log_response(resp: aiohttp.ClientResponse): + try: + _LOGGER.debug(f"@@@ RESPONSE {resp.status} length {resp.content_length}") + _LOGGER.debug(f" header {dict((k, v) for k, v in resp.headers.items())}") + if not resp.content_length: + return + if resp.status != 200: + _LOGGER.debug(f" content {await resp.text()}") + else: + _LOGGER.debug(f" json '{await resp.json(content_type=None)}'") + except Exception as err: + _LOGGER.exception("Failed to log response") + header = { - "Authorization": "Bearer %s" % self._access_token, + "Authorization": f"Bearer {self._access_token}", "Accept": "application/json", } full_url = API_URL + url try: async with async_timeout.timeout(30): + if DEBUG_API_CALLS: + log_request() + call = getattr(self._client, method) - if data is not None and method == "post": + if data is not None and method in ("post", "put"): call = partial(call, json=data) - # _LOGGER.debug(f"@@@ Req {method} to '{full_url}' payload {data}") + resp: aiohttp.ClientResponse async with call(full_url, headers=header) as resp: - # _LOGGER.debug(f" @ Res {resp.status} data length {resp.content_length}") - # _LOGGER.debug(f" @ Res {resp.status} header {dict((k, v) for k, v in resp.headers.items())}") - # _LOGGER.debug(f" @ Res {resp.status} content {await resp.text()}") + if DEBUG_API_CALLS: + await log_response(resp) + if resp.status == 401: # Unauthorized await self._refresh_token() if iteration > API_RETRIES: raise RequestRetryError(f"Request to {full_url} failed after {iteration} retries") return await self._request(url, iteration=iteration + 1) + elif resp.status == 204: # No content content = await resp.read() return content + elif resp.status == 200: # OK + # FIXME: This will raise json error if the json is invalid. How to handle this? json_result = await resp.json(content_type=None) + + # Validate the incoming json data + # FIXME: This raise pydantic.ValidationError if the json is unexpected. How to handle this? + validate(json_result, url=url) + return json_result + else: + if DEBUG_API_ERRORS and not DEBUG_API_CALLS: + _LOGGER.debug("Failing request:") + log_request() + await log_response(resp) + raise RequestError(f"{method} request to {full_url} failed with status {resp.status}: {resp}") except (asyncio.TimeoutError, aiohttp.ClientError) as err: raise RequestError(f"Request to {full_url} failed: {err}") from err - async def _req_constants(self): - data = await self._request("constants") - # FIXME: Verify assumed data structure - return data - - async def _req_installations(self) -> list[TDict]: - data = await self._request("installation") - # FIXME: Verify assumed data structure - return data["Data"] - - async def _req_installation(self, installation_id: str) -> TDict: - data = await self._request(f"installation/{installation_id}") - # FIXME: Verify assumed data structure - - # Remove data fields with excessive data, making it bigger than the - # HA database appreciates for the size of attributes. - # FIXME: SupportGroup is sub dict. This is not within the declared type - supportgroup = data.get('SupportGroup') - if supportgroup is not None: - if "LogoBase64" in supportgroup: - logo = supportgroup["LogoBase64"] - supportgroup["LogoBase64"] = "" %(len(logo)) - - return data - - async def _req_hierarchy(self, installation_id: str) -> dict[str, list[dict[str, TValue]]]: - data = await self._request(f"installation/{installation_id}/hierarchy") - # FIXME: Verify assumed data structure - return data - - async def _req_circuit(self, circuit_id: str) -> TDict: - data = await self._request(f"circuits/{circuit_id}") - # FIXME: Verify assumed data structure - return data - - async def _req_chargers(self) -> list[TDict]: - data = await self._request("chargers") - # FIXME: Verify assumed data structure - return data["Data"] - - async def _req_charger(self, charger_id: str) -> TDict: - data = await self._request(f"chargers/{charger_id}") - # FIXME: Verify assumed data structure - return data - - async def _req_charger_firmware(self, installation_id: str) -> TDict: - data = await self._request(f"chargerFirmware/installation/{installation_id}") - # FIXME: Verify assumed data structure - return data - - async def _req_charger_state(self, charger_id: str) -> list[TDict]: - data = await self._request(f"chargers/{charger_id}/state") - # FIXME: Verify assumed data structure - return data - - async def _req_charger_settings(self, charger_id: str) -> dict[str, TDict]: - data = await self._request(f"chargers/{charger_id}/settings") - # FIXME: Verify assumed data structure - return data - # API METHODS DONE # ======================================================================= @@ -641,30 +732,32 @@ async def build(self): """Make the python interface.""" _LOGGER.debug("Discover and build hierarchy") - installations = await self._req_installations() + # Get list of installations + installations = await self._request("installation") installs = [] - for data in installations: - install_data = await self._req_installation(data["Id"]) + for data in installations["Data"]: _LOGGER.debug(" Installation %s", data["Id"]) - inst = Installation(install_data, self) + inst = Installation(data, self) self.register(data["Id"], inst) await inst.build() installs.append(inst) self.installs = installs + # Get list of chargers # Will also report chargers listed in installation hierarchy above - chargers = await self._req_chargers() + chargers = await self._request("chargers") so_chargers = [] - for data in chargers: + for data in chargers["Data"]: if data["Id"] in self.map: continue _LOGGER.debug(" Charger %s", data["Id"]) chg = Charger(data, self) self.register(data["Id"], chg) + await chg.build() so_chargers.append(chg) self.stand_alone_chargers = so_chargers @@ -672,9 +765,9 @@ async def build(self): if not self._const: # Get the API constants - self._const = await self._req_constants() + self._const = await self._request("constants") - # Get the chargers + # Find the chargers device types device_types = set( chg.device_type for chg in self.map.values() @@ -684,6 +777,12 @@ async def build(self): # Define the remaps self._obs_ids = Account._get_remap(self._const, ["Observations", "ObservationIds"], device_types) self._set_ids = Account._get_remap(self._const, ["Settings", "SettingIds"], device_types) + self._cmd_ids = Account._get_remap(self._const, ["Commands", "CommandIds"], device_types) + + # Update the state on all chargers + for data in self.map.values(): + if isinstance(data, Charger): + await data.state() self.is_built = True @@ -752,6 +851,7 @@ def _state_to_attrs(data: Iterable[dict[str, str]], key: str, keydict: dict[str, if __name__ == "__main__": # Just to execute the script manually. import os + from pprint import pprint async def gogo(): username = os.environ.get("zaptec_username") @@ -775,16 +875,17 @@ async def gogo(): # Update the state to get all the attributes. for obj in acc.map.values(): await obj.state() + pprint(obj.asdict()) - with open("data.json", "w") as outfile: + # with open("data.json", "w") as outfile: - async def cb(data): - print(data) - outfile.write(json.dumps(data, indent=2) + '\n') - outfile.flush() + # async def cb(data): + # print(data) + # outfile.write(json.dumps(data, indent=2) + '\n') + # outfile.flush() - for ins in acc.installs: - await ins._stream(cb=cb) + # for ins in acc.installs: + # await ins._stream(cb=cb) finally: await acc._client.close() diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py index 26efe2c..1ff70b4 100644 --- a/custom_components/zaptec/binary_sensor.py +++ b/custom_components/zaptec/binary_sensor.py @@ -24,14 +24,14 @@ class ZaptecBinarySensor(ZaptecBaseEntity, BinarySensorEntity): @callback def _update_from_zaptec(self) -> None: - self._attr_is_on = bool(self._get_zaptec_value()) + self._attr_is_on = self._get_zaptec_value() self._log_value(self._attr_is_on) class ZaptecBinarySensorWithAttrs(ZaptecBinarySensor): def _post_init(self): - self._attr_extra_state_attributes = self.zaptec_obj._attrs + self._attr_extra_state_attributes = self.zaptec_obj.asdict() self._attr_unique_id = self.zaptec_obj.id @@ -39,7 +39,7 @@ class ZaptecBinarySensorLock(ZaptecBinarySensor): @callback def _update_from_zaptec(self) -> None: - self._attr_is_on = not bool(self._get_zaptec_value()) + self._attr_is_on = not self._get_zaptec_value() self._log_value(self._attr_is_on) @@ -98,7 +98,7 @@ class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): entity_category=const.EntityCategory.DIAGNOSTIC, icon="mdi:lock", cls=ZaptecBinarySensorLock, - ) + ), ] diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index 62f6efa..6ad4544 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -22,22 +22,23 @@ class ZaptecButton(ZaptecBaseEntity, ButtonEntity): + zaptec_obj: Charger + async def async_press(self) -> None: """Press the button.""" _LOGGER.debug( - "Press %s '%s' in %s", - self.__class__.__qualname__, - self.key, - self.zaptec_obj.id + "Press %s.%s (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id, ) - charger: Charger = self.zaptec_obj - try: - await charger.command(self.key) + await self.zaptec_obj.command(self.key) except Exception as exc: raise HomeAssistantError(exc) from exc + await self.coordinator.async_request_refresh() + @dataclass class ZapButtonEntityDescription(ButtonEntityDescription): @@ -79,8 +80,8 @@ class ZapButtonEntityDescription(ButtonEntityDescription): icon="mdi:restart" ), ZapButtonEntityDescription( - key="update_firmware", - translation_key="update_firmware", + key="upgrade_firmware", + translation_key="upgrade_firmware", entity_category=const.EntityCategory.DIAGNOSTIC, icon="mdi:memory" ), diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py index 953f35c..324b186 100644 --- a/custom_components/zaptec/diagnostics.py +++ b/custom_components/zaptec/diagnostics.py @@ -4,9 +4,11 @@ from copy import deepcopy from typing import Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntry +# to Support running this as a script. +if __name__ != "__main__": + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.device_registry import DeviceEntry from . import ZaptecUpdateCoordinator from .api import Account @@ -145,8 +147,8 @@ def gen(url, obj, ctx=None): for circuit in data.get('Circuits', []): circuit_ids.append(circuit['Id']) - for data in circuit.get('Chargers', []): - charger_in_circuits_ids.append(data['Id']) + for charger in circuit.get('Chargers', []): + charger_in_circuits_ids.append(charger['Id']) gen(url, data, ctx="hierarchy") @@ -188,3 +190,55 @@ def gen(url, obj, ctx=None): out.setdefault('redacts', red.redact_info) return out + + +if __name__ == "__main__": + + # Just to execute the script manually. Must be run using + # python -m custom_components.zaptec.diagnostics + import asyncio + import aiohttp + import os + from pprint import pprint + from dataclasses import dataclass + + async def gogo(): + username = os.environ.get("zaptec_username") + password = os.environ.get("zaptec_password") + acc = Account( + username, + password, + client=aiohttp.ClientSession( + connector=aiohttp.TCPConnector(ssl=False) + ), + ) + + try: + + # + # Mocking to pretend to be a hass instance + # + @dataclass + class FakeHass: + data: dict + + @dataclass + class FakeConfig: + entry_id: str + + @dataclass + class FakeCoordinator: + account: Account + + coordinator = FakeCoordinator(account=acc) + config = FakeConfig(entry_id='') + hass = FakeHass(data={DOMAIN: {config.entry_id: coordinator}}) + + # Get the diagnostics info + out = await async_get_device_diagnostics(hass, config, None) + pprint(out) + + finally: + await acc._client.close() + + asyncio.run(gogo()) diff --git a/custom_components/zaptec/manifest.json b/custom_components/zaptec/manifest.json index 6f6f459..9aae47c 100644 --- a/custom_components/zaptec/manifest.json +++ b/custom_components/zaptec/manifest.json @@ -8,6 +8,6 @@ "hellowlol", "sveinse" ], - "requirements": ["azure-servicebus"], + "requirements": ["azure-servicebus", "pydantic"], "version": "0.0.6b2" } diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index 490d9d4..37d1e23 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -12,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.const import EntityCategory from . import ZaptecBaseEntity, ZaptecUpdateCoordinator from .api import Installation @@ -39,14 +40,41 @@ def _post_init(self): async def async_set_native_value(self, value: float) -> None: """Update to Zaptec.""" _LOGGER.debug( - "Setting %s '%s' to '%s' in %s", - self.__class__.__qualname__, - self.key, - value, + "Setting %s.%s to <%s> %s (in %s)", + self.__class__.__qualname__, self.key, + type(value).__qualname__, value, self.zaptec_obj.id ) + + try: + await self.zaptec_obj.set_limit_current(availableCurrent=value) + except Exception as exc: + raise HomeAssistantError(exc) from exc + + await self.coordinator.async_request_refresh() + + +class ZaptecSettingNumber(ZaptecNumber): + + zaptec_obj: Installation + + def _post_init(self): + # Get the max current rating from the reported max current + self.entity_description.native_max_value = self.zaptec_obj.charge_current_installation_max_limit + + async def async_set_native_value(self, value: float) -> None: + """Update to Zaptec.""" + _LOGGER.debug( + "Setting %s.%s to <%s> %s (in %s)", + self.__class__.__qualname__, self.key, + type(value).__qualname__, value, + self.zaptec_obj.id + ) + try: - await self.zaptec_obj.limit_current(availableCurrent=value) + await self.zaptec_obj.set_settings({ + self.entity_description.setting: value + }) except Exception as exc: raise HomeAssistantError(exc) from exc @@ -57,6 +85,7 @@ async def async_set_native_value(self, value: float) -> None: class ZapNumberEntityDescription(NumberEntityDescription): cls: type|None = None + setting: str|None = None INSTALLATION_ENTITIES: list[EntityDescription] = [ @@ -70,44 +99,38 @@ class ZapNumberEntityDescription(NumberEntityDescription): native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, cls=ZaptecAvailableCurrentNumber, ), +] + +CIRCUIT_ENTITIES: list[EntityDescription] = [ +] + +CHARGER_ENTITIES: list[EntityDescription] = [ ZapNumberEntityDescription( - key="available_current_phase1", - translation_key="available_current_phase1", - device_class=NumberDeviceClass.CURRENT, - native_min_value=0, - native_max_value=32, # FIXME: Implememt max current per phase - icon="mdi:current-ac", - native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, - # cls=ZaptecAvailableCurrentNumber, # FIXME: Implement 3phase adjustment - ), - ZapNumberEntityDescription( - key="available_current_phase2", - translation_key="available_current_phase2", + key="charger_min_current", + translation_key="charger_min_current", device_class=NumberDeviceClass.CURRENT, + entity_category=EntityCategory.CONFIG, native_min_value=0, - native_max_value=32, # FIXME: Implememt max current per phase + native_max_value=32, icon="mdi:current-ac", native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, - # cls=ZaptecAvailableCurrentNumber, # FIXME: Implement 3phase adjustment + cls=ZaptecSettingNumber, + setting="CurrentInMinimum", ), ZapNumberEntityDescription( - key="available_current_phase3", - translation_key="available_current_phase3", + key="charger_max_current", + translation_key="charger_max_current", device_class=NumberDeviceClass.CURRENT, + entity_category=EntityCategory.CONFIG, native_min_value=0, - native_max_value=32, # FIXME: Implememt max current per phase + native_max_value=32, icon="mdi:current-ac", native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, - # cls=ZaptecAvailableCurrentNumber, # FIXME: Implement 3phase adjustment + cls=ZaptecSettingNumber, + setting="CurrentInMaximum", ), ] -CIRCUIT_ENTITIES: list[EntityDescription] = [ -] - -CHARGER_ENTITIES: list[EntityDescription] = [ -] - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index d9c27a7..6e4ff33 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -49,6 +49,27 @@ class ZapSensorEntityDescription(SensorEntityDescription): INSTALLATION_ENTITIES: list[EntityDescription] = [ + SensorEntityDescription( + key="available_current_phase1", + translation_key="available_current_phase1", + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), + SensorEntityDescription( + key="available_current_phase2", + translation_key="available_current_phase2", + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), + SensorEntityDescription( + key="available_current_phase3", + translation_key="available_current_phase3", + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-ac", + native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, + ), ] CIRCUIT_ENTITIES: list[EntityDescription] = [ @@ -92,6 +113,48 @@ class ZapSensorEntityDescription(SensorEntityDescription): icon="mdi:current-ac", native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, ), + ZapSensorEntityDescription( + key="voltage_phase1", + translation_key="voltage_phase1", + device_class=SensorDeviceClass.VOLTAGE, + icon="mdi:sine-wave", + native_unit_of_measurement=const.UnitOfElectricPotential.VOLT, + ), + ZapSensorEntityDescription( + key="voltage_phase2", + translation_key="voltage_phase2", + device_class=SensorDeviceClass.VOLTAGE, + icon="mdi:sine-wave", + native_unit_of_measurement=const.UnitOfElectricPotential.VOLT, + ), + ZapSensorEntityDescription( + key="voltage_phase3", + translation_key="voltage_phase3", + device_class=SensorDeviceClass.VOLTAGE, + icon="mdi:sine-wave", + native_unit_of_measurement=const.UnitOfElectricPotential.VOLT, + ), + ZapSensorEntityDescription( + key="total_charge_power", + translation_key="total_charge_power", + device_class=SensorDeviceClass.POWER, + icon="mdi:flash", + native_unit_of_measurement=const.UnitOfPower.KILO_WATT, + ), + ZapSensorEntityDescription( + key="signed_meter_value_kwh", + translation_key="signed_meter_value", + device_class=SensorDeviceClass.ENERGY, + icon="mdi:counter", + native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, + ), + ZapSensorEntityDescription( + key='completed_session.Energy', + translation_key="completed_session_energy", + device_class=SensorDeviceClass.ENERGY, + icon="mdi:counter", + native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, + ), ] diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index 31f6394..17c7c39 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -45,37 +45,37 @@ async def service_handle_stop_pause(service_call: ServiceCall) -> None: _LOGGER.debug("Called stop pause") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.command("stop_pause") + await charger.stop_pause() async def service_handle_resume_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called start and or resume") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.command("resume_charging") + await charger.resume_charging() async def service_handle_authorize_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called authorize charging") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.command("authorize_charge") + await charger.authorize_charge() async def service_handle_deauthorize_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called deauthorize charging and stop") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.command("deauthorize_stop") + await charger.deauthorize_stop() async def service_handle_restart_charger(service_call: ServiceCall) -> None: _LOGGER.debug("Called restart charger") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.command("restart_charger") + await charger.restart_charger() async def service_handle_update_firmware(service_call: ServiceCall) -> None: _LOGGER.debug("Called update firmware") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.command("upgrade_firmware") + await charger.upgrade_firmware() async def service_handle_limit_current(service_call: ServiceCall) -> None: _LOGGER.debug("Called set current limit") @@ -85,7 +85,7 @@ async def service_handle_limit_current(service_call: ServiceCall) -> None: available_current_phase2 = service_call.data.get("available_current_phase2") available_current_phase3 = service_call.data.get("available_current_phase3") installation: Installation = acc.map[installation_id] - await installation.limit_current( + await installation.set_limit_current( availableCurrent=available_current, availableCurrentPhase1=available_current_phase1, availableCurrentPhase2=available_current_phase2, diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index 6a5ec15..95e1ce0 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ZaptecBaseEntity, ZaptecUpdateCoordinator -from .api import Account, Charger +from .api import Account, Charger, Installation from .const import CHARGE_MODE_MAP, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class ZaptecSwitch(ZaptecBaseEntity, SwitchEntity): @callback def _update_from_zaptec(self) -> None: - self._attr_is_on = bool(self._get_zaptec_value()) + self._attr_is_on = self._get_zaptec_value() self._log_value(self._attr_is_on) @@ -39,18 +39,69 @@ def _update_from_zaptec(self) -> None: async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument """Turn on the switch.""" + _LOGGER.debug( + "Turn on %s.%s (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id, + ) + + try: + await self.zaptec_obj.resume_charging() + except Exception as exc: + raise HomeAssistantError(exc) from exc + + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument + """Turn off the switch.""" + _LOGGER.debug( + "Turn off %s.%s (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id, + ) + + try: + await self.zaptec_obj.stop_pause() + except Exception as exc: + raise HomeAssistantError(exc) from exc + + await self.coordinator.async_request_refresh() + + +class ZaptecAuthorizationRequiredSwitch(ZaptecSwitch): + + zaptec_obj: Installation + + async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument + """Turn on the switch.""" + _LOGGER.debug( + "Turn on %s.%s (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id, + ) + try: - await self.zaptec_obj.command('resume_charging') + await self.zaptec_obj.set_authenication_required(True) except Exception as exc: raise HomeAssistantError(exc) from exc + await self.coordinator.async_request_refresh() + async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument """Turn off the switch.""" + _LOGGER.debug( + "Turn off %s.%s (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id, + ) + try: - await self.zaptec_obj.command('stop_pause') + await self.zaptec_obj.set_authenication_required(False) except Exception as exc: raise HomeAssistantError(exc) from exc + await self.coordinator.async_request_refresh() + @dataclass class ZapSwitchEntityDescription(SwitchEntityDescription): @@ -59,6 +110,13 @@ class ZapSwitchEntityDescription(SwitchEntityDescription): INSTALLATION_SWITCH_TYPES: list[EntityDescription] = [ + ZapSwitchEntityDescription( + key="is_required_authentication", + translation_key="authorization_required", + device_class=SwitchDeviceClass.SWITCH, + icon="mdi:lock-check-outline", + cls=ZaptecAuthorizationRequiredSwitch, + ), ] CIRCUIT_SWITCH_TYPES: list[EntityDescription] = [ @@ -71,7 +129,6 @@ class ZapSwitchEntityDescription(SwitchEntityDescription): device_class=SwitchDeviceClass.SWITCH, cls=ZaptecChargeSwitch, ), - # FIXME: Implement a authentication required switch ] diff --git a/custom_components/zaptec/translations/en.json b/custom_components/zaptec/translations/en.json index b15e1f1..c25ac63 100644 --- a/custom_components/zaptec/translations/en.json +++ b/custom_components/zaptec/translations/en.json @@ -27,14 +27,22 @@ "operating_mode": { "name": "Charger mode" }, "current_phase1": { "name": "Current phase 1" }, "current_phase2": { "name": "Current phase 2" }, - "current_phase3": { "name": "Current phase 3" } - }, - "number": { - "available_current": { "name": "Available current" }, + "current_phase3": { "name": "Current phase 3" }, + "voltage_phase1": { "name": "Voltage phase 1" }, + "voltage_phase2": { "name": "Voltage phase 2" }, + "voltage_phase3": { "name": "Voltage phase 3" }, + "total_charge_power": { "name": "Total charge power" }, + "signed_meter_value": { "name": "Energy meter"}, + "completed_session_energy": { "name": "Completed energy session" }, "available_current_phase1": { "name": "Available current phase 1" }, "available_current_phase2": { "name": "Available current phase 2" }, "available_current_phase3": { "name": "Available current phase 3" } }, + "number": { + "available_current": { "name": "Available current" }, + "charger_min_current": { "name": "Charger min current" }, + "charger_max_current": { "name": "Charger max current" } + }, "binary_sensor": { "is_authorization_required": { "name": "Authorization required" }, "permanent_cable_lock": { "name": "Permanent cable lock" } @@ -43,12 +51,16 @@ "resume_charging": { "name": "Resume charging" }, "stop_pause": { "name": "Stop/pause charging" }, "restart_charger": { "name": "Restart charger" }, - "update_firmware": { "name": "Update firmware" }, + "upgrade_firmware": { "name": "Upgrade firmware" }, "authorize_charge": { "name": "Authorize charging" }, "deauthorize_stop": { "name": "Deauthorize charging"} }, "switch": { - "charging": { "name": "Charging" } + "charging": { "name": "Charging" }, + "authorization_required": { "name": "Authorization required" } + }, + "update": { + "firmware_update": { "name": "Firmware update" } } } } diff --git a/custom_components/zaptec/update.py b/custom_components/zaptec/update.py new file mode 100644 index 0000000..390046d --- /dev/null +++ b/custom_components/zaptec/update.py @@ -0,0 +1,84 @@ +"""Zaptec component update.""" +from __future__ import annotations + +import logging +from dataclasses import dataclass + +from homeassistant import const +from homeassistant.components.update import (UpdateDeviceClass, UpdateEntity, + UpdateEntityDescription) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZaptecBaseEntity, ZaptecUpdateCoordinator +from .api import Account +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZaptecUpdate(ZaptecBaseEntity, UpdateEntity): + + @callback + def _update_from_zaptec(self) -> None: + self._attr_installed_version = self._get_zaptec_value(key="current_firmware_version") + self._attr_latest_version = self._get_zaptec_value(key="available_firmware_version") + self._log_value(self._attr_installed_version) + + async def async_install(self, version, backup): + _LOGGER.debug( + "Updating firmware %s.%s (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id + ) + + try: + await self.zaptec_obj.upgrade_firmware() + except Exception as exc: + raise HomeAssistantError(exc) from exc + + await self.coordinator.async_request_refresh() + + +@dataclass +class ZapUpdateEntityDescription(UpdateEntityDescription): + + cls: type|None = None + + +INSTALLATION_ENTITIES: list[EntityDescription] = [ +] + +CIRCUIT_ENTITIES: list[EntityDescription] = [ +] + +CHARGER_ENTITIES: list[EntityDescription] = [ + UpdateEntityDescription( + key="firmware_update", + translation_key="firmware_update_to_date", + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=const.EntityCategory.DIAGNOSTIC, + # icon="mdi:lock", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + _LOGGER.debug("Setup binary sensors") + + coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + acc = coordinator.account + + entities = ZaptecUpdate.create_from_zaptec( + acc, + coordinator, + INSTALLATION_ENTITIES, + CIRCUIT_ENTITIES, + CHARGER_ENTITIES, + ) + async_add_entities(entities, True) diff --git a/custom_components/zaptec/validate.py b/custom_components/zaptec/validate.py new file mode 100644 index 0000000..caef945 --- /dev/null +++ b/custom_components/zaptec/validate.py @@ -0,0 +1,161 @@ +"""Data validator for Zaptec API data.""" +from __future__ import annotations + +import logging +import re + +try: + from pydantic.v1 import BaseModel, ConfigDict, ValidationError +except ImportError: + from pydantic import BaseModel, ConfigDict, ValidationError + +_LOGGER = logging.getLogger(__name__) + + +class TypeWrapper: + """Workaround class for v1 pydantic""" + + +class Installation(BaseModel): + model_config = ConfigDict(extra="allow") + Id: str + + +class Installations(BaseModel): + model_config = ConfigDict(extra="allow") + Data: list[Installation] + + +class Charger(BaseModel): + model_config = ConfigDict(extra="allow") + Id: str + Name: str + + +class ChargerState(BaseModel): + model_config = ConfigDict(extra="allow") + StateId: int + ValueAsString: str + + +# pydantic v2 +# ChargerStates = TypeAdapter[list[ChargerState]] +class ChargerStates(TypeWrapper, BaseModel): + _data: list[ChargerState] + + +class ChargerSetting(BaseModel): + model_config = ConfigDict(extra="allow") + SettingsId: int + Value: str = '' + + +# pydantic v2 +# ChargerSettings = TypeAdapter[dict[str, ChargerSetting]] +class ChargerSettings(TypeWrapper, BaseModel): + _data: dict[str, ChargerSetting] + + +class Chargers(BaseModel): + model_config = ConfigDict(extra="allow") + Data: list[Charger] + + +class Circuit(BaseModel): + model_config = ConfigDict(extra="allow") + Id: str + Name: str + + +class CircuitHierarchy(BaseModel): + model_config = ConfigDict(extra="allow") + Id: str + Name: str + Chargers: list[Charger] + + +class Hierarchy(BaseModel): + model_config = ConfigDict(extra="allow") + Id: str + Name: str + Circuits: list[CircuitHierarchy] + + +class ChargerFirmware(BaseModel): + model_config = ConfigDict(extra="allow") + ChargerId: str + CurrentVersion: str + AvailableVersion: str + IsUpToDate: bool + + +# pydantic v2 +# ChargerFirmwares = TypeAdapter[list[ChargerFirmware]] +class ChargerFirmwares(TypeWrapper, BaseModel): + _data: list[ChargerFirmware] + + +class InstallationConnectionDetails(BaseModel): + model_config = ConfigDict(extra="allow") + Host: str + Password: str + # Port: int + Subscription: str + # Type: int + Username: str + Topic: str + + +# Mapping of URL to pydantic model +URLS = { + 'installation': Installations, + 'chargers': Chargers, + 'constants': None, + r'installation/[0-9a-f\-]+': Installation, + r'installation/[0-9a-f\-]+/hierarchy': Hierarchy, + r'circuits/[0-9a-f\-]+': Circuit, + r'chargers/[0-9a-f\-]+': Charger, + r'chargers/[0-9a-f\-]+/state': ChargerStates, + r'chargers/[0-9a-f\-]+/settings': ChargerSettings, + r'chargerFirmware/installation/[0-9a-f\-]+': ChargerFirmwares, + r'installation/[0-9a-f\-]+/messagingConnectionDetails': InstallationConnectionDetails, +} + +_URLS = [ + (k, re.compile(k), v) for k, v in URLS.items() +] + +def validate(data, url): + """Validate the data.""" + + for pat, re_pat, model in _URLS: + + # Mathes either the exact string or its regexp + if url == pat or re_pat.fullmatch(url): + + try: + d = data + + # pydantic v1 + if isinstance(model, TypeWrapper): + d = {'_data': data} + + if isinstance(model, BaseModel): + # pydantic v1 + model.parse_obj(d) + + # pydantic v2 + # model.model_validate(data, strict=True) + + # pydantic v2 + # elif isinstance(model, TypeAdapter): + # model.validate_python(data, strict=True) + + except ValidationError as err: + _LOGGER.error("Failed to validate %s (pattern %s): %s", url, pat, err) + raise + + return + + _LOGGER.warning("Missing validator for url %s", url) + _LOGGER.warning("Data: %s", data) From 1ca1859fb0421bf24ccc14d626756b0125aaca78 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Sun, 17 Sep 2023 01:02:48 +0200 Subject: [PATCH 03/17] Fix issues * Fix failure to start if zaptec user have no extra privileges, sveinse/zaptec#10 * Ensure entities are marked as "Unavailable" if not available in Zaptec * Fix text of binary sensor "charger.is_authorization_required" * Fix more user friendly error messages --- custom_components/zaptec/__init__.py | 19 +-- custom_components/zaptec/api.py | 120 +++++++++++++----- custom_components/zaptec/binary_sensor.py | 24 ++-- custom_components/zaptec/button.py | 2 +- custom_components/zaptec/number.py | 17 ++- custom_components/zaptec/sensor.py | 30 +++-- custom_components/zaptec/switch.py | 28 ++-- custom_components/zaptec/translations/en.json | 8 +- custom_components/zaptec/update.py | 19 ++- 9 files changed, 182 insertions(+), 85 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index 290e6b5..891c6a0 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -187,9 +187,7 @@ def _handle_coordinator_update(self) -> None: try: self._update_from_zaptec() except Exception as exc: - _LOGGER.exception("Error updating switch '%s': %s", self.name, exc) - self._update_from_zaptec_failed() - raise HomeAssistantError(exc) from exc + raise HomeAssistantError(f"Error updating entity {self.key}") from exc super()._handle_coordinator_update() @callback @@ -198,12 +196,6 @@ def _update_from_zaptec(self) -> None: inheriting class to update the entity state. ''' - @callback - def _update_from_zaptec_failed(self) -> None: - '''Called when the coordinator has failed to update. Implement this in - the inheriting class to update the entity state. - ''' - @callback def _get_zaptec_value(self, *, default=MISSING, key=None): '''Helper to retrieve the value from the Zaptec object. This is to @@ -244,6 +236,15 @@ def _log_value(self, value, force=False): self.zaptec_obj.id, ) + @callback + def _log_unavailable(self): + '''Helper to log when unavailable. + ''' + _LOGGER.debug(" %s.%s = UNAVAILABLE (in %s)", + self.__class__.__qualname__, self.key, + self.zaptec_obj.id, + ) + @classmethod def create_from( cls, diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index a4b9b2c..3f0418a 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -65,6 +65,14 @@ class AuthorizationError(ZaptecApiError): class RequestError(ZaptecApiError): '''Failed to get the results from the API''' + def __init__(self, message, error_code): + super().__init__(message) + self.error_code = error_code + + +class RequestTimeoutError(ZaptecApiError): + '''Failed to get the results from the API''' + class RequestRetryError(ZaptecApiError): '''Retries too many times''' @@ -107,6 +115,12 @@ def __getattr__(self, key): except KeyError as exc: raise AttributeError(exc) from exc + def get(self, key, default=MISSING): + if default is MISSING: + return self._attrs[to_under(key)] + else: + return self._attrs.get(to_under(key), default) + def asdict(self): """Return the attributes as a dict""" return self._attrs @@ -145,7 +159,14 @@ async def build(self): await self.state() # Get the hierarchy of circurits and chargers - hierarchy = await self._account._request(f"installation/{self.id}/hierarchy") + try: + hierarchy = await self._account._request(f"installation/{self.id}/hierarchy") + except RequestError as err: + if err.error_code == 403: + _LOGGER.warning("Access denied to installation hierarchy of %s. The user might not have access.", self.id) + self.circuits = [] + return + raise circuits = [] for item in hierarchy["Circuits"]: @@ -190,19 +211,17 @@ async def _stream(self, cb=None): from azure.servicebus.aio import ServiceBusClient from azure.servicebus.exceptions import ServiceBusError except ImportError: - _LOGGER.debug("Azure Service bus is not available. Resolving to polling") + _LOGGER.warning("Azure Service bus is not available. Resolving to polling") # https://github.com/custom-components/zaptec/issues return # Get connection details - # FIXME: The validator will fail if not all fields are set, so how to handle this? - conf = await self.live_stream_connection_details() - - # Check if we can use it. - if any(True for i in ["Password", "Username", "Host"] if not conf.get(i)): - _LOGGER.warning( - "Cant enable live update using the servicebus, enable it in the zaptec portal" - ) + try: + conf = await self.live_stream_connection_details() + except RequestError as err: + if err.error_code != 403: + raise + _LOGGER.warning("Failed to get live stream info. Check if user have access in the zaptec portal") return # Open the connection @@ -437,14 +456,32 @@ async def state(self): '''Update the charger state''' _LOGGER.debug("Polling state for %s charger (%s)", self.id, self._attrs.get('name')) - # Get the main charger info - charger = await self.charger_info() - self.set_attributes(charger) + try: + # Get the main charger info + charger = await self.charger_info() + self.set_attributes(charger) + except RequestError as err: + # An unprivileged user will get a 403 error, but the user is able + # to get _some_ info about the charger by getting a list of + # chargers. + if err.error_code != 403: + raise + _LOGGER.debug("Access denied to charger %s, attempting list", self.id) + chargers = await self._account._request("chargers") + for chg in chargers["Data"]: + if chg["Id"] == self.id: + self.set_attributes(chg) + break # Get the state from the charger - state = await self._account._request(f"chargers/{self.id}/state") - data = Account._state_to_attrs(state, 'StateId', self._account._obs_ids) - self.set_attributes(data) + try: + state = await self._account._request(f"chargers/{self.id}/state") + data = Account._state_to_attrs(state, 'StateId', self._account._obs_ids) + self.set_attributes(data) + except RequestError as err: + if err.error_code != 403: + raise + _LOGGER.debug("Access denied to charger %s state", self.id) # Firmware version is called. SmartMainboardSoftwareApplicationVersion, # stateid 908 @@ -452,22 +489,32 @@ async def state(self): # maybe remove this later if it dont interest ppl. # Fetch some additional attributes from settings - settings = await self._account._request(f"chargers/{self.id}/settings") - data = Account._state_to_attrs(settings.values(), 'SettingId', self._account._set_ids) - self.set_attributes(data) + try: + settings = await self._account._request(f"chargers/{self.id}/settings") + data = Account._state_to_attrs(settings.values(), 'SettingId', self._account._set_ids) + self.set_attributes(data) + except RequestError as err: + if err.error_code != 403: + raise + _LOGGER.debug("Access denied to charger %s settings", self.id) if self.installation_id in self._account.map: - firmware_info = await self._account._request( - f"chargerFirmware/installation/{self.installation_id}" - ) + try: + firmware_info = await self._account._request( + f"chargerFirmware/installation/{self.installation_id}" + ) - for fm in firmware_info: - if fm["ChargerId"] == self.id: - self.set_attributes({ - "current_firmware_version": fm["CurrentVersion"], - "available_firmware_version": fm["AvailableVersion"], - "firmware_update_to_date": fm["IsUpToDate"], - }) + for fm in firmware_info: + if fm["ChargerId"] == self.id: + self.set_attributes({ + "current_firmware_version": fm["CurrentVersion"], + "available_firmware_version": fm["AvailableVersion"], + "firmware_update_to_date": fm["IsUpToDate"], + }) + except RequestError as err: + if err.error_code != 403: + raise + _LOGGER.debug("Access denied to charger %s firmware info", self.id) async def live(self): # FIXME: Is this an experiment? Omit? @@ -549,7 +596,7 @@ async def set_settings(self, settings: dict[str, Any]): if any(d for d in values if d['id'] is None): raise ValueError(f"Unknown setting '{settings}'") - + _LOGGER.debug("Settings %s", settings) data = await self._account._request( f"chargers/{self.id}/settings", @@ -562,13 +609,13 @@ async def stop_pause(self): async def resume_charging(self): return await self.command("resume_charging") - + async def deauthorize_stop(self): return await self.command("deauthorize_stop") - + async def restart_charger(self): return await self.command("restart_charger") - + async def upgrade_firmware(self): return await self.command("upgrade_firmware") @@ -720,10 +767,13 @@ async def log_response(resp: aiohttp.ClientResponse): log_request() await log_response(resp) - raise RequestError(f"{method} request to {full_url} failed with status {resp.status}: {resp}") + raise RequestError( + f"{method} request to {full_url} failed with status {resp.status}: {resp}", + resp.status + ) except (asyncio.TimeoutError, aiohttp.ClientError) as err: - raise RequestError(f"Request to {full_url} failed: {err}") from err + raise RequestTimeoutError(f"Request to {full_url} failed: {err}") from err # API METHODS DONE # ======================================================================= diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py index 1ff70b4..1a24a96 100644 --- a/custom_components/zaptec/binary_sensor.py +++ b/custom_components/zaptec/binary_sensor.py @@ -24,8 +24,13 @@ class ZaptecBinarySensor(ZaptecBaseEntity, BinarySensorEntity): @callback def _update_from_zaptec(self) -> None: - self._attr_is_on = self._get_zaptec_value() - self._log_value(self._attr_is_on) + try: + self._attr_is_on = self._get_zaptec_value() + self._attr_available = True + self._log_value(self._attr_is_on) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() class ZaptecBinarySensorWithAttrs(ZaptecBinarySensor): @@ -39,8 +44,13 @@ class ZaptecBinarySensorLock(ZaptecBinarySensor): @callback def _update_from_zaptec(self) -> None: - self._attr_is_on = not self._get_zaptec_value() - self._log_value(self._attr_is_on) + try: + self._attr_is_on = not self._get_zaptec_value() + self._attr_available = True + self._log_value(self._attr_is_on) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() @dataclass @@ -53,7 +63,7 @@ class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): ZapBinarySensorEntityDescription( key="active", name="Installation", - device_class=BinarySensorDeviceClass.CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, # False=disconnected, True=connected entity_category=const.EntityCategory.DIAGNOSTIC, icon="mdi:home-lightning-bolt-outline", has_entity_name=False, @@ -65,7 +75,7 @@ class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): ZapBinarySensorEntityDescription( key="is_active", name="Circuit", - device_class=BinarySensorDeviceClass.CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, # False=disconnected, True=connected entity_category=const.EntityCategory.DIAGNOSTIC, icon="mdi:orbit", has_entity_name=False, @@ -86,10 +96,8 @@ class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): ZapBinarySensorEntityDescription( key="is_authorization_required", translation_key="is_authorization_required", - device_class=BinarySensorDeviceClass.LOCK, # False=unlocked, True=locked entity_category=const.EntityCategory.DIAGNOSTIC, icon="mdi:lock", - cls=ZaptecBinarySensorLock, ), ZapBinarySensorEntityDescription( key="permanent_cable_lock", diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index 6ad4544..42e707f 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -35,7 +35,7 @@ async def async_press(self) -> None: try: await self.zaptec_obj.command(self.key) except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError(f"Running command '{self.key}' failed") from exc await self.coordinator.async_request_refresh() diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index 37d1e23..ea7e5ae 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -25,8 +25,13 @@ class ZaptecNumber(ZaptecBaseEntity, NumberEntity): @callback def _update_from_zaptec(self) -> None: - self._attr_native_value = self._get_zaptec_value() - self._log_value(self._attr_native_value) + try: + self._attr_native_value = self._get_zaptec_value() + self._attr_available = True + self._log_value(self._attr_native_value) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() class ZaptecAvailableCurrentNumber(ZaptecNumber): @@ -35,7 +40,7 @@ class ZaptecAvailableCurrentNumber(ZaptecNumber): def _post_init(self): # Get the max current rating from the reported max current - self.entity_description.native_max_value = self.zaptec_obj.max_current + self.entity_description.native_max_value = self.zaptec_obj.get('max_current', 32) async def async_set_native_value(self, value: float) -> None: """Update to Zaptec.""" @@ -49,7 +54,7 @@ async def async_set_native_value(self, value: float) -> None: try: await self.zaptec_obj.set_limit_current(availableCurrent=value) except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError(f"Set current limit to {value} failed") from exc await self.coordinator.async_request_refresh() @@ -60,7 +65,7 @@ class ZaptecSettingNumber(ZaptecNumber): def _post_init(self): # Get the max current rating from the reported max current - self.entity_description.native_max_value = self.zaptec_obj.charge_current_installation_max_limit + self.entity_description.native_max_value = self.zaptec_obj.get('charge_current_installation_max_limit', 32) async def async_set_native_value(self, value: float) -> None: """Update to Zaptec.""" @@ -76,7 +81,7 @@ async def async_set_native_value(self, value: float) -> None: self.entity_description.setting: value }) except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError(f"Setting {self.entity_description.setting} to {value} failed") from exc await self.coordinator.async_request_refresh() diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index 6e4ff33..594f4ed 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -26,19 +26,29 @@ class ZaptecSensor(ZaptecBaseEntity, SensorEntity): @callback def _update_from_zaptec(self) -> None: - self._attr_native_value = self._get_zaptec_value() - self._log_value(self._attr_native_value) + try: + self._attr_native_value = self._get_zaptec_value() + self._attr_available = True + self._log_value(self._attr_native_value) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() class ZaptecChargeSensor(ZaptecSensor): @callback def _update_from_zaptec(self) -> None: - state = self._get_zaptec_value() - mode = CHARGE_MODE_MAP.get(state, CHARGE_MODE_MAP["Unknown"]) - self._attr_native_value = mode[0] - self._attr_icon = mode[1] - self._log_value(self._attr_native_value) + try: + state = self._get_zaptec_value() + mode = CHARGE_MODE_MAP.get(state, CHARGE_MODE_MAP["Unknown"]) + self._attr_native_value = mode[0] + self._attr_icon = mode[1] + self._attr_available = True + self._log_value(self._attr_native_value) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() @dataclass @@ -49,21 +59,21 @@ class ZapSensorEntityDescription(SensorEntityDescription): INSTALLATION_ENTITIES: list[EntityDescription] = [ - SensorEntityDescription( + ZapSensorEntityDescription( key="available_current_phase1", translation_key="available_current_phase1", device_class=SensorDeviceClass.CURRENT, icon="mdi:current-ac", native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, ), - SensorEntityDescription( + ZapSensorEntityDescription( key="available_current_phase2", translation_key="available_current_phase2", device_class=SensorDeviceClass.CURRENT, icon="mdi:current-ac", native_unit_of_measurement=const.UnitOfElectricCurrent.AMPERE, ), - SensorEntityDescription( + ZapSensorEntityDescription( key="available_current_phase3", translation_key="available_current_phase3", device_class=SensorDeviceClass.CURRENT, diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index 95e1ce0..cc91a46 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -23,8 +23,13 @@ class ZaptecSwitch(ZaptecBaseEntity, SwitchEntity): @callback def _update_from_zaptec(self) -> None: - self._attr_is_on = self._get_zaptec_value() - self._log_value(self._attr_is_on) + try: + self._attr_is_on = self._get_zaptec_value() + self._attr_available = True + self._log_value(self._attr_is_on) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() class ZaptecChargeSwitch(ZaptecSwitch): @@ -33,9 +38,14 @@ class ZaptecChargeSwitch(ZaptecSwitch): @callback def _update_from_zaptec(self) -> None: - state = self._get_zaptec_value() - self._attr_is_on = state in ["Connected_Charging"] - self._log_value(self._attr_is_on) + try: + state = self._get_zaptec_value() + self._attr_is_on = state in ["Connected_Charging"] + self._attr_available = True + self._log_value(self._attr_is_on) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument """Turn on the switch.""" @@ -48,7 +58,7 @@ async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument try: await self.zaptec_obj.resume_charging() except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError("Resuming charging failed") from exc await self.coordinator.async_request_refresh() @@ -63,7 +73,7 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument try: await self.zaptec_obj.stop_pause() except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError("Stop/pausing charging failed") from exc await self.coordinator.async_request_refresh() @@ -83,7 +93,7 @@ async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument try: await self.zaptec_obj.set_authenication_required(True) except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError("Setting authorization required failed") from exc await self.coordinator.async_request_refresh() @@ -98,7 +108,7 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument try: await self.zaptec_obj.set_authenication_required(False) except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError("Setting authorization required failed") from exc await self.coordinator.async_request_refresh() diff --git a/custom_components/zaptec/translations/en.json b/custom_components/zaptec/translations/en.json index c25ac63..9a71345 100644 --- a/custom_components/zaptec/translations/en.json +++ b/custom_components/zaptec/translations/en.json @@ -44,7 +44,13 @@ "charger_max_current": { "name": "Charger max current" } }, "binary_sensor": { - "is_authorization_required": { "name": "Authorization required" }, + "is_authorization_required": { + "name": "Authorization required", + "state": { + "off": "Not required", + "on": "Required" + } + }, "permanent_cable_lock": { "name": "Permanent cable lock" } }, "button": { diff --git a/custom_components/zaptec/update.py b/custom_components/zaptec/update.py index 390046d..51d949b 100644 --- a/custom_components/zaptec/update.py +++ b/custom_components/zaptec/update.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ZaptecBaseEntity, ZaptecUpdateCoordinator -from .api import Account +from .api import Account, Charger from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -22,11 +22,18 @@ class ZaptecUpdate(ZaptecBaseEntity, UpdateEntity): + zaptec_obj: Charger + @callback def _update_from_zaptec(self) -> None: - self._attr_installed_version = self._get_zaptec_value(key="current_firmware_version") - self._attr_latest_version = self._get_zaptec_value(key="available_firmware_version") - self._log_value(self._attr_installed_version) + try: + self._attr_installed_version = self._get_zaptec_value(key="current_firmware_version") + self._attr_latest_version = self._get_zaptec_value(key="available_firmware_version") + self._attr_available = True + self._log_value(self._attr_installed_version) + except (KeyError, AttributeError): + self._attr_available = False + self._log_unavailable() async def async_install(self, version, backup): _LOGGER.debug( @@ -38,7 +45,7 @@ async def async_install(self, version, backup): try: await self.zaptec_obj.upgrade_firmware() except Exception as exc: - raise HomeAssistantError(exc) from exc + raise HomeAssistantError("Sending update firmware command failed") from exc await self.coordinator.async_request_refresh() @@ -56,7 +63,7 @@ class ZapUpdateEntityDescription(UpdateEntityDescription): ] CHARGER_ENTITIES: list[EntityDescription] = [ - UpdateEntityDescription( + ZapUpdateEntityDescription( key="firmware_update", translation_key="firmware_update_to_date", device_class=UpdateDeviceClass.FIRMWARE, From 0dcb22b6826536c6c76a432ae03c03b294f3bd6c Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Sun, 17 Sep 2023 01:04:04 +0200 Subject: [PATCH 04/17] Fixes issues after testing * Fix sveinse/zaptec#2, sveinse/zaptec#10, sveinse/zaptec#15 * Handle starting zaptec with non-privileged user * Fix "unavailable" state on entities * Fix non-working buttons and commands * Fix error reporting and logging * Fix unity bug in "total_charge_power" * Added "total_charge_power_session" * Update API data validators --- custom_components/zaptec/api.py | 29 ++++++++++++++----- custom_components/zaptec/button.py | 8 ++--- custom_components/zaptec/sensor.py | 9 +++++- custom_components/zaptec/services.py | 12 ++++---- custom_components/zaptec/services.yaml | 2 +- custom_components/zaptec/switch.py | 2 +- custom_components/zaptec/translations/en.json | 9 +++--- custom_components/zaptec/validate.py | 5 +++- 8 files changed, 51 insertions(+), 25 deletions(-) diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index 3f0418a..e4d650c 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -259,7 +259,11 @@ async def _stream(self, cb=None): # Convert the json payload json_result = json.loads(obj[0]["text"]) - _LOGGER.debug("--- Subscription: %s", json_result) + + json_log = json_result.copy() + if "StateId" in json_log: + json_log["StateId"] = f"{json_log['StateId']} ({self._account._obs_ids.get(json_log['StateId'])})" + _LOGGER.debug("--- Subscription: %s", json_log) # Send result to account that will update the objects self._account.update(json_result) @@ -577,6 +581,11 @@ async def charger_info(self) -> TDict: async def command(self, command: str): """Send a command to the charger""" + if command == 'authorize_charge': + return await self.authorize_charge() + + # Fetching the name from the const is perhaps not a good idea + # if Zaptec is changing them. cmdid = self._account._cmd_ids.get(command) if cmdid is None: raise ValueError(f"Unknown command {command}") @@ -604,14 +613,14 @@ async def set_settings(self, settings: dict[str, Any]): ) return data - async def stop_pause(self): - return await self.command("stop_pause") + async def stop_charging_final(self): + return await self.command("stop_charging_final") async def resume_charging(self): return await self.command("resume_charging") - async def deauthorize_stop(self): - return await self.command("deauthorize_stop") + async def deauthorize_and_stop(self): + return await self.command("deauthorize_and_stop") async def restart_charger(self): return await self.command("restart_charger") @@ -711,9 +720,10 @@ def log_request(): async def log_response(resp: aiohttp.ClientResponse): try: - _LOGGER.debug(f"@@@ RESPONSE {resp.status} length {resp.content_length}") + contents = await resp.read() + _LOGGER.debug(f"@@@ RESPONSE {resp.status} length {len(contents)}") _LOGGER.debug(f" header {dict((k, v) for k, v in resp.headers.items())}") - if not resp.content_length: + if not contents: return if resp.status != 200: _LOGGER.debug(f" content {await resp.text()}") @@ -829,6 +839,11 @@ async def build(self): self._set_ids = Account._get_remap(self._const, ["Settings", "SettingIds"], device_types) self._cmd_ids = Account._get_remap(self._const, ["Commands", "CommandIds"], device_types) + # Commands can also be specified as lower case strings + self._cmd_ids.update({ + to_under(k): v for k, v in self._cmd_ids.items() if isinstance(k, str) + }) + # Update the state on all chargers for data in self.map.values(): if isinstance(data, Charger): diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index 42e707f..b0e21ed 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -59,8 +59,8 @@ class ZapButtonEntityDescription(ButtonEntityDescription): icon="mdi:play-circle-outline", ), ZapButtonEntityDescription( - key="stop_pause", - translation_key="stop_pause", + key="stop_charging_final", + translation_key="stop_charging", icon="mdi:pause-circle-outline", ), ZapButtonEntityDescription( @@ -69,8 +69,8 @@ class ZapButtonEntityDescription(ButtonEntityDescription): icon="mdi:lock-check-outline" ), ZapButtonEntityDescription( - key="deauthorize_stop", - translation_key="deauthorize_stop", + key="deauthorize_and_stop", + translation_key="deauthorize_and_stop", icon="mdi:lock-remove-outline" ), ZapButtonEntityDescription( diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index 594f4ed..1efdaf5 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -149,7 +149,14 @@ class ZapSensorEntityDescription(SensorEntityDescription): translation_key="total_charge_power", device_class=SensorDeviceClass.POWER, icon="mdi:flash", - native_unit_of_measurement=const.UnitOfPower.KILO_WATT, + native_unit_of_measurement=const.UnitOfPower.WATT, + ), + ZapSensorEntityDescription( + key="total_charge_power_session", + translation_key="total_charge_power_session", + device_class=SensorDeviceClass.ENERGY, + icon="mdi:counter", + native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, ), ZapSensorEntityDescription( key="signed_meter_value_kwh", diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index 17c7c39..e6beab4 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -41,14 +41,14 @@ async def async_setup_services(hass: HomeAssistant) -> None: _LOGGER.debug("Set up services") acc: Account = hass.data[DOMAIN]["api"] - async def service_handle_stop_pause(service_call: ServiceCall) -> None: - _LOGGER.debug("Called stop pause") + async def service_handle_stop_charging(service_call: ServiceCall) -> None: + _LOGGER.debug("Called stop charging") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.stop_pause() + await charger.stop_charging_final() async def service_handle_resume_charging(service_call: ServiceCall) -> None: - _LOGGER.debug("Called start and or resume") + _LOGGER.debug("Called resume charging") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] await charger.resume_charging() @@ -63,7 +63,7 @@ async def service_handle_deauthorize_charging(service_call: ServiceCall) -> None _LOGGER.debug("Called deauthorize charging and stop") charger_id = service_call.data["charger_id"] charger: Charger = acc.map[charger_id] - await charger.deauthorize_stop() + await charger.deauthorize_and_stop() async def service_handle_restart_charger(service_call: ServiceCall) -> None: _LOGGER.debug("Called restart charger") @@ -94,7 +94,7 @@ async def service_handle_limit_current(service_call: ServiceCall) -> None: # LIST OF SERVICES services: list[tuple[str, vol.Schema, TServiceHandler]] = [ - ("stop_pause_charging", has_id_schema, service_handle_stop_pause), + ("stop_charging", has_id_schema, service_handle_stop_charging), ("resume_charging", has_id_schema, service_handle_resume_charging), ("authorize_charging", has_id_schema, service_handle_authorize_charging), ("deauthorize_charging", has_id_schema, service_handle_deauthorize_charging), diff --git a/custom_components/zaptec/services.yaml b/custom_components/zaptec/services.yaml index ef4abcb..afc9b51 100644 --- a/custom_components/zaptec/services.yaml +++ b/custom_components/zaptec/services.yaml @@ -1,4 +1,4 @@ -stop_pause_charging: +stop_charging: description: "Stop/pause charging" fields: charger_id: diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index cc91a46..c233c2f 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -71,7 +71,7 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument ) try: - await self.zaptec_obj.stop_pause() + await self.zaptec_obj.stop_charging_final() except Exception as exc: raise HomeAssistantError("Stop/pausing charging failed") from exc diff --git a/custom_components/zaptec/translations/en.json b/custom_components/zaptec/translations/en.json index 9a71345..62d0e25 100644 --- a/custom_components/zaptec/translations/en.json +++ b/custom_components/zaptec/translations/en.json @@ -31,9 +31,10 @@ "voltage_phase1": { "name": "Voltage phase 1" }, "voltage_phase2": { "name": "Voltage phase 2" }, "voltage_phase3": { "name": "Voltage phase 3" }, - "total_charge_power": { "name": "Total charge power" }, + "total_charge_power": { "name": "Charge power" }, + "total_charge_power_session": { "name": "Session total charge" }, "signed_meter_value": { "name": "Energy meter"}, - "completed_session_energy": { "name": "Completed energy session" }, + "completed_session_energy": { "name": "Completed session energy" }, "available_current_phase1": { "name": "Available current phase 1" }, "available_current_phase2": { "name": "Available current phase 2" }, "available_current_phase3": { "name": "Available current phase 3" } @@ -55,11 +56,11 @@ }, "button": { "resume_charging": { "name": "Resume charging" }, - "stop_pause": { "name": "Stop/pause charging" }, + "stop_charging": { "name": "Stop charging" }, "restart_charger": { "name": "Restart charger" }, "upgrade_firmware": { "name": "Upgrade firmware" }, "authorize_charge": { "name": "Authorize charging" }, - "deauthorize_stop": { "name": "Deauthorize charging"} + "deauthorize_and_stop": { "name": "Deauthorize charging"} }, "switch": { "charging": { "name": "Charging" }, diff --git a/custom_components/zaptec/validate.py b/custom_components/zaptec/validate.py index caef945..70c28e9 100644 --- a/custom_components/zaptec/validate.py +++ b/custom_components/zaptec/validate.py @@ -113,12 +113,15 @@ class InstallationConnectionDetails(BaseModel): 'constants': None, r'installation/[0-9a-f\-]+': Installation, r'installation/[0-9a-f\-]+/hierarchy': Hierarchy, + r'installation/[0-9a-f\-]+/update': None, + r'installation/[0-9a-f\-]+/messagingConnectionDetails': InstallationConnectionDetails, r'circuits/[0-9a-f\-]+': Circuit, r'chargers/[0-9a-f\-]+': Charger, r'chargers/[0-9a-f\-]+/state': ChargerStates, r'chargers/[0-9a-f\-]+/settings': ChargerSettings, + r'chargers/[0-9a-f\-]+/authorizecharge': None, + r'chargers/[0-9a-f\-]+/SendCommand/[0-9]+': None, r'chargerFirmware/installation/[0-9a-f\-]+': ChargerFirmwares, - r'installation/[0-9a-f\-]+/messagingConnectionDetails': InstallationConnectionDetails, } _URLS = [ From c79fca8e3b01b7dacab1b266981af3f245fd6972 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 18 Sep 2023 01:47:48 +0200 Subject: [PATCH 05/17] Formatted project with black --- custom_components/zaptec/__init__.py | 172 +++++++++------ custom_components/zaptec/api.py | 258 +++++++++++++--------- custom_components/zaptec/binary_sensor.py | 11 +- custom_components/zaptec/button.py | 28 +-- custom_components/zaptec/const.py | 5 +- custom_components/zaptec/diagnostics.py | 96 ++++---- custom_components/zaptec/misc.py | 9 +- custom_components/zaptec/number.py | 50 +++-- custom_components/zaptec/sensor.py | 13 +- custom_components/zaptec/services.py | 47 ++-- custom_components/zaptec/switch.py | 28 +-- custom_components/zaptec/update.py | 30 +-- custom_components/zaptec/validate.py | 43 ++-- 13 files changed, 458 insertions(+), 332 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index 891c6a0..859aafb 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -8,21 +8,33 @@ import async_timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, - CONF_USERNAME, Platform) +from homeassistant.const import ( + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import (CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .api import Account, ZaptecApiError, ZaptecBase -from .const import (DEFAULT_SCAN_INTERVAL, DOMAIN, MANUFACTURER, MISSING, - REQUEST_REFRESH_DELAY, STARTUP) +from .const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MANUFACTURER, + MISSING, + REQUEST_REFRESH_DELAY, + STARTUP, +) _LOGGER = logging.getLogger(__name__) @@ -42,6 +54,7 @@ # FIXME: Informing users that the interface is considerable different # FIXME: Setting that allows users to continue with old naming scheme? + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up integration.""" if DOMAIN in hass.data: @@ -90,7 +103,6 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: class ZaptecUpdateCoordinator(DataUpdateCoordinator[None]): - account: Account def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: @@ -113,19 +125,20 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: name=f"{DOMAIN}-{entry.data['username']}", update_interval=timedelta(seconds=scan_interval), request_refresh_debouncer=Debouncer( - hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False, + hass, + _LOGGER, + cooldown=REQUEST_REFRESH_DELAY, + immediate=False, ), ) async def cancel_streams(self): - await asyncio.gather(*( - i.cancel_stream() for i in self.account.installs - )) + await asyncio.gather(*(i.cancel_stream() for i in self.account.installs)) @callback async def _stream_update(self, event): """Handle new update event from the zaptec stream. The zaptec objects - are updated in-place prior to this callback being called. + are updated in-place prior to this callback being called. """ self.async_update_listeners() @@ -152,7 +165,6 @@ async def _async_update_data(self) -> None: class ZaptecBaseEntity(CoordinatorEntity[ZaptecUpdateCoordinator]): - coordinator: ZaptecUpdateCoordinator zaptec_obj: ZaptecBase entity_description: EntityDescription @@ -178,9 +190,9 @@ def __init__( self._post_init() def _post_init(self) -> None: - '''Called after the entity has been initialized. Implement this for a - custom light-weight init in the inheriting class. - ''' + """Called after the entity has been initialized. Implement this for a + custom light-weight init in the inheriting class. + """ @callback def _handle_coordinator_update(self) -> None: @@ -192,19 +204,19 @@ def _handle_coordinator_update(self) -> None: @callback def _update_from_zaptec(self) -> None: - '''Called when the coordinator has new data. Implement this in the - inheriting class to update the entity state. - ''' + """Called when the coordinator has new data. Implement this in the + inheriting class to update the entity state. + """ @callback def _get_zaptec_value(self, *, default=MISSING, key=None): - '''Helper to retrieve the value from the Zaptec object. This is to - be called from _handle_coordinator_update() in the inheriting class. - It will fetch the attr given by the entity description key. - ''' + """Helper to retrieve the value from the Zaptec object. This is to + be called from _handle_coordinator_update() in the inheriting class. + It will fetch the attr given by the entity description key. + """ obj = self.zaptec_obj key = key or self.key - for k in key.split('.'): + for k in key.split("."): # Do dict because some object contains sub-dicts which must # be handled differently than attributes if isinstance(obj, dict): @@ -223,26 +235,30 @@ def _get_zaptec_value(self, *, default=MISSING, key=None): @callback def _log_value(self, value, force=False): - '''Helper to log a new value. This is to be called from - _handle_coordinator_update() in the inheriting class. - ''' + """Helper to log a new value. This is to be called from + _handle_coordinator_update() in the inheriting class. + """ prev = self._prev_value if force or value != prev: self._prev_value = value # Only logs when the value changes - _LOGGER.debug(" %s.%s = <%s> %s (in %s)", - self.__class__.__qualname__, self.key, - type(value).__qualname__, value, - self.zaptec_obj.id, + _LOGGER.debug( + " %s.%s = <%s> %s (in %s)", + self.__class__.__qualname__, + self.key, + type(value).__qualname__, + value, + self.zaptec_obj.id, ) @callback def _log_unavailable(self): - '''Helper to log when unavailable. - ''' - _LOGGER.debug(" %s.%s = UNAVAILABLE (in %s)", - self.__class__.__qualname__, self.key, - self.zaptec_obj.id, + """Helper to log when unavailable.""" + _LOGGER.debug( + " %s.%s = UNAVAILABLE (in %s)", + self.__class__.__qualname__, + self.key, + self.zaptec_obj.id, ) @classmethod @@ -253,9 +269,9 @@ def create_from( zaptec_obj: ZaptecBase, device_info: DeviceInfo, ) -> list[ZaptecBaseEntity]: - '''Helper factory to create a list of entities from a list of - EntityDescription objects. - ''' + """Helper factory to create a list of entities from a list of + EntityDescription objects. + """ # Start with the common device info and append the provided device info dev_info = DeviceInfo( @@ -283,49 +299,65 @@ def create_from_zaptec( circuit_entities: list[EntityDescription], charger_entities: list[EntityDescription], ) -> list[ZaptecBaseEntity]: - '''Helper factory to populate the listed entities for the detected - Zaptec devices. It sets the proper device info on the installation, - circuit and charger object in order for them to be grouped in HA. - ''' + """Helper factory to populate the listed entities for the detected + Zaptec devices. It sets the proper device info on the installation, + circuit and charger object in order for them to be grouped in HA. + """ entities = [] for zap_install in account.installs: - entities.extend(cls.create_from( - installation_entities, coordinator, zap_install, - DeviceInfo( - model=f"{zap_install.name} Installation", - ), - )) - - for zap_circuit in zap_install.circuits: - entities.extend(cls.create_from( - circuit_entities, coordinator, zap_circuit, + entities.extend( + cls.create_from( + installation_entities, + coordinator, + zap_install, DeviceInfo( - model=f"{zap_circuit.name} Circuit", - via_device=(DOMAIN, zap_install.id), + model=f"{zap_install.name} Installation", ), - )) + ) + ) - for zap_charger in zap_circuit.chargers: - entities.extend(cls.create_from( - charger_entities, coordinator, zap_charger, + for zap_circuit in zap_install.circuits: + entities.extend( + cls.create_from( + circuit_entities, + coordinator, + zap_circuit, DeviceInfo( - model=f"{zap_charger.name} Charger", - via_device=(DOMAIN, zap_circuit.id), + model=f"{zap_circuit.name} Circuit", + via_device=(DOMAIN, zap_install.id), ), - )) + ) + ) + + for zap_charger in zap_circuit.chargers: + entities.extend( + cls.create_from( + charger_entities, + coordinator, + zap_charger, + DeviceInfo( + model=f"{zap_charger.name} Charger", + via_device=(DOMAIN, zap_circuit.id), + ), + ) + ) for zap_charger in account.stand_alone_chargers: - entities.extend(cls.create_from( - charger_entities, coordinator, zap_charger, - DeviceInfo( - model=f"{zap_charger.name} Charger", - ), - )) + entities.extend( + cls.create_from( + charger_entities, + coordinator, + zap_charger, + DeviceInfo( + model=f"{zap_charger.name} Charger", + ), + ) + ) return entities @property def key(self): - '''Helper to retrieve the key from the entity description.''' + """Helper to retrieve the key from the entity description.""" return self.entity_description.key diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index e4d650c..1525b30 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -38,8 +38,7 @@ # to Support running this as a script. if __name__ == "__main__": - from const import (API_RETRIES, API_URL, CONST_URL, FALSY, MISSING, - TOKEN_URL, TRUTHY) + from const import API_RETRIES, API_URL, FALSY, MISSING, TOKEN_URL, TRUTHY from misc import mc_nbfx_decoder, to_under from validate import validate @@ -48,22 +47,28 @@ logging.getLogger("azure").setLevel(logging.WARNING) else: - from .const import (API_RETRIES, API_URL, CONST_URL, FALSY, MISSING, - TOKEN_URL, TRUTHY) + from .const import ( + API_RETRIES, + API_URL, + FALSY, + MISSING, + TOKEN_URL, + TRUTHY, + ) from .misc import mc_nbfx_decoder, to_under from .validate import validate class ZaptecApiError(Exception): - '''Base exception for all Zaptec API errors''' + """Base exception for all Zaptec API errors""" class AuthorizationError(ZaptecApiError): - '''Authenatication failed''' + """Authenatication failed""" class RequestError(ZaptecApiError): - '''Failed to get the results from the API''' + """Failed to get the results from the API""" def __init__(self, message, error_code): super().__init__(message) @@ -71,11 +76,11 @@ def __init__(self, message, error_code): class RequestTimeoutError(ZaptecApiError): - '''Failed to get the results from the API''' + """Failed to get the results from the API""" class RequestRetryError(ZaptecApiError): - '''Retries too many times''' + """Retries too many times""" class ZaptecBase(ABC): @@ -95,7 +100,7 @@ def __init__(self, data: TDict, account: "Account") -> None: self.set_attributes(data) def set_attributes(self, data: TDict) -> bool: - """Set the class attributes from the given data""""" + """Set the class attributes from the given data""" for k, v in data.items(): # Cast the value to the correct type new_key = to_under(k) @@ -103,10 +108,20 @@ def set_attributes(self, data: TDict) -> bool: new_vt = type(new_v).__qualname__ if new_key not in self._attrs: qn = self.__class__.__qualname__ - _LOGGER.debug(">>> Adding %s.%s (%s) = <%s> %s", qn, new_key, k, new_vt, new_v) + _LOGGER.debug( + ">>> Adding %s.%s (%s) = <%s> %s", qn, new_key, k, new_vt, new_v + ) elif self._attrs[new_key] != new_v: qn = self.__class__.__qualname__ - _LOGGER.debug(">>> Updating %s.%s (%s) = <%s> %s (was %s)", qn, new_key, k, new_vt, new_v, self._attrs[new_key]) + _LOGGER.debug( + ">>> Updating %s.%s (%s) = <%s> %s (was %s)", + qn, + new_key, + k, + new_vt, + new_v, + self._attrs[new_key], + ) self._attrs[new_key] = new_v def __getattr__(self, key): @@ -160,10 +175,15 @@ async def build(self): # Get the hierarchy of circurits and chargers try: - hierarchy = await self._account._request(f"installation/{self.id}/hierarchy") + hierarchy = await self._account._request( + f"installation/{self.id}/hierarchy" + ) except RequestError as err: if err.error_code == 403: - _LOGGER.warning("Access denied to installation hierarchy of %s. The user might not have access.", self.id) + _LOGGER.warning( + "Access denied to installation hierarchy of %s. The user might not have access.", + self.id, + ) self.circuits = [] return raise @@ -179,7 +199,9 @@ async def build(self): self.circuits = circuits async def state(self): - _LOGGER.debug("Polling state for %s installation (%s)", self.id, self._attrs.get('name')) + _LOGGER.debug( + "Polling state for %s installation (%s)", self.id, self._attrs.get("name") + ) data = await self.installation_info() self.set_attributes(data) @@ -211,7 +233,9 @@ async def _stream(self, cb=None): from azure.servicebus.aio import ServiceBusClient from azure.servicebus.exceptions import ServiceBusError except ImportError: - _LOGGER.warning("Azure Service bus is not available. Resolving to polling") + _LOGGER.warning( + "Azure Service bus is not available. Resolving to polling" + ) # https://github.com/custom-components/zaptec/issues return @@ -221,21 +245,24 @@ async def _stream(self, cb=None): except RequestError as err: if err.error_code != 403: raise - _LOGGER.warning("Failed to get live stream info. Check if user have access in the zaptec portal") + _LOGGER.warning( + "Failed to get live stream info. Check if user have access in the zaptec portal" + ) return # Open the connection - constr = (f'Endpoint=sb://{conf["Host"]}/;' - f'SharedAccessKeyName={conf["Username"]};' - f'SharedAccessKey={conf["Password"]}') + constr = ( + f'Endpoint=sb://{conf["Host"]}/;' + f'SharedAccessKeyName={conf["Username"]};' + f'SharedAccessKey={conf["Password"]}' + ) servicebus_client = ServiceBusClient.from_connection_string(conn_str=constr) _LOGGER.debug("Connecting to servicebus using %s", constr) self._stream_receiver = None async with servicebus_client: receiver = servicebus_client.get_subscription_receiver( - topic_name=conf["Topic"], - subscription_name=conf["Subscription"] + topic_name=conf["Topic"], subscription_name=conf["Subscription"] ) # Store the receiver in order to close it and cancel this stream self._stream_receiver = receiver @@ -262,7 +289,9 @@ async def _stream(self, cb=None): json_log = json_result.copy() if "StateId" in json_log: - json_log["StateId"] = f"{json_log['StateId']} ({self._account._obs_ids.get(json_log['StateId'])})" + json_log[ + "StateId" + ] = f"{json_log['StateId']} ({self._account._obs_ids.get(json_log['StateId'])})" _LOGGER.debug("--- Subscription: %s", json_log) # Send result to account that will update the objects @@ -273,7 +302,9 @@ async def _stream(self, cb=None): await cb(json_result) except Exception as err: - _LOGGER.exception("Couldn't process stream message: %s", err) + _LOGGER.exception( + "Couldn't process stream message: %s", err + ) _LOGGER.debug("Message: %s", binmsg) # Pass the message as the stream must continue. @@ -309,12 +340,12 @@ async def cancel_stream(self): finally: self._stream_task = None - #----------------- + # ----------------- # API methods - #----------------- + # ----------------- async def installation_info(self) -> TDict: - '''Raw request for installation data''' + """Raw request for installation data""" # Get the installation data data = await self._account._request(f"installation/{self.id}") @@ -322,7 +353,7 @@ async def installation_info(self) -> TDict: # Remove data fields with excessive data, making it bigger than the # HA database appreciates for the size of attributes. # FIXME: SupportGroup is sub dict. This is not within the declared type - supportgroup = data.get('SupportGroup') + supportgroup = data.get("SupportGroup") if supportgroup is not None: if "LogoBase64" in supportgroup: logo = supportgroup["LogoBase64"] @@ -337,10 +368,11 @@ async def set_limit_current(self, **kwargs): """ has_availablecurrent = "availableCurrent" in kwargs has_availablecurrentphases = all( - k in kwargs for k in ( - "availableCurrentPhase1", - "availableCurrentPhase2", - "availableCurrentPhase3", + k in kwargs + for k in ( + "availableCurrentPhase1", + "availableCurrentPhase2", + "availableCurrentPhase3", ) ) @@ -351,8 +383,7 @@ async def set_limit_current(self, **kwargs): ) data = await self._account._request( - f"installation/{self.id}/update", - method="post", data=kwargs + f"installation/{self.id}/update", method="post", data=kwargs ) return data @@ -366,11 +397,11 @@ async def set_authenication_required(self, required: bool): "IsRequiredAuthentication": required, } result = await self._account._request( - f"installation/{self.id}", - method="put", data=data + f"installation/{self.id}", method="put", data=data ) return result + class Circuit(ZaptecBase): """Represents a circuits""" @@ -393,7 +424,7 @@ async def build(self): await self.state() chargers = [] - for item in self._attrs['chargers']: + for item in self._attrs["chargers"]: _LOGGER.debug(" Charger %s", item["Id"]) chg = Charger(item, self._account) self._account.register(item["Id"], chg) @@ -403,16 +434,18 @@ async def build(self): self.chargers = chargers async def state(self): - _LOGGER.debug("Polling state for %s cicuit (%s)", self.id, self._attrs.get('name')) + _LOGGER.debug( + "Polling state for %s cicuit (%s)", self.id, self._attrs.get("name") + ) data = await self.circuit_info() self.set_attributes(data) - #----------------- + # ----------------- # API methods - #----------------- + # ----------------- async def circuit_info(self) -> TDict: - '''Raw request for circuit data''' + """Raw request for circuit data""" data = await self._account._request(f"circuits/{self.id}") return data @@ -444,21 +477,25 @@ def __init__(self, data: TDict, account: "Account") -> None: # Append the attr types that depends on self attr_types = self.ATTR_TYPES.copy() - attr_types.update({ - "operating_mode": self.type_operation_mode, - "charger_operation_mode": self.type_operation_mode, - }) + attr_types.update( + { + "operating_mode": self.type_operation_mode, + "charger_operation_mode": self.type_operation_mode, + } + ) self.ATTR_TYPES = attr_types async def build(self) -> None: - '''Build the object''' + """Build the object""" # Don't update state at build, because the state and settings ids # is not loaded yet. async def state(self): - '''Update the charger state''' - _LOGGER.debug("Polling state for %s charger (%s)", self.id, self._attrs.get('name')) + """Update the charger state""" + _LOGGER.debug( + "Polling state for %s charger (%s)", self.id, self._attrs.get("name") + ) try: # Get the main charger info @@ -480,7 +517,7 @@ async def state(self): # Get the state from the charger try: state = await self._account._request(f"chargers/{self.id}/state") - data = Account._state_to_attrs(state, 'StateId', self._account._obs_ids) + data = Account._state_to_attrs(state, "StateId", self._account._obs_ids) self.set_attributes(data) except RequestError as err: if err.error_code != 403: @@ -495,7 +532,9 @@ async def state(self): # Fetch some additional attributes from settings try: settings = await self._account._request(f"chargers/{self.id}/settings") - data = Account._state_to_attrs(settings.values(), 'SettingId', self._account._set_ids) + data = Account._state_to_attrs( + settings.values(), "SettingId", self._account._set_ids + ) self.set_attributes(data) except RequestError as err: if err.error_code != 403: @@ -510,11 +549,13 @@ async def state(self): for fm in firmware_info: if fm["ChargerId"] == self.id: - self.set_attributes({ - "current_firmware_version": fm["CurrentVersion"], - "available_firmware_version": fm["AvailableVersion"], - "firmware_update_to_date": fm["IsUpToDate"], - }) + self.set_attributes( + { + "current_firmware_version": fm["CurrentVersion"], + "available_firmware_version": fm["AvailableVersion"], + "firmware_update_to_date": fm["IsUpToDate"], + } + ) except RequestError as err: if err.error_code != 403: raise @@ -567,12 +608,14 @@ async def update(self, data): # return await self._account.request(cmd, data=data, method="post") def type_operation_mode(self, v): - modes = {str(v): k for k, v in self._account._const["ChargerOperationModes"].items()} + modes = { + str(v): k for k, v in self._account._const["ChargerOperationModes"].items() + } return modes.get(str(v), str(v)) - #----------------- + # ----------------- # API methods - #----------------- + # ----------------- async def charger_info(self) -> TDict: data = await self._account._request(f"chargers/{self.id}") @@ -581,7 +624,7 @@ async def charger_info(self) -> TDict: async def command(self, command: str): """Send a command to the charger""" - if command == 'authorize_charge': + if command == "authorize_charge": return await self.authorize_charge() # Fetching the name from the const is perhaps not a good idea @@ -592,8 +635,7 @@ async def command(self, command: str): _LOGGER.debug("Command %s (%s)", command, cmdid) data = await self._account._request( - f"chargers/{self.id}/SendCommand/{cmdid}", - method="post" + f"chargers/{self.id}/SendCommand/{cmdid}", method="post" ) return data @@ -601,15 +643,14 @@ async def set_settings(self, settings: dict[str, Any]): """Set settings on the charger""" set_ids = self._account._set_ids - values = [{'id': set_ids.get(k), 'value': v} for k, v in settings.items()] + values = [{"id": set_ids.get(k), "value": v} for k, v in settings.items()] - if any(d for d in values if d['id'] is None): + if any(d for d in values if d["id"] is None): raise ValueError(f"Unknown setting '{settings}'") _LOGGER.debug("Settings %s", settings) data = await self._account._request( - f"chargers/{self.id}/settings", - method="post", data=values + f"chargers/{self.id}/settings", method="post", data=values ) return data @@ -631,8 +672,7 @@ async def upgrade_firmware(self): async def authorize_charge(self): _LOGGER.debug("Authorize charge") data = await self._account._request( - f"chargers/{self.id}/authorizecharge", - method="post" + f"chargers/{self.id}/authorizecharge", method="post" ) return data @@ -666,7 +706,7 @@ def __init__(self, username: str, password: str, client=None) -> None: self._client = aiohttp.ClientSession() def register(self, id: str, data: ZaptecBase): - '''Register an object data with id''' + """Register an object data with id""" self.map[id] = data # ======================================================================= @@ -685,7 +725,9 @@ async def check_login(username: str, password: str) -> bool: data = await resp.json() return True else: - raise AuthorizationError(f"Failed to authenticate. Got status {resp.status}") + raise AuthorizationError( + f"Failed to authenticate. Got status {resp.status}" + ) except aiohttp.ClientConnectorError as err: _LOGGER.exception("Bad things happend while trying to authenticate :(") raise @@ -706,13 +748,16 @@ async def _refresh_token(self): self._token_info.update(data) self._access_token = data.get("access_token") else: - raise AuthorizationError("Failed to refresh token, check your credentials.") + raise AuthorizationError( + "Failed to refresh token, check your credentials." + ) async def _request(self, url: str, method="get", data=None, iteration=1): - def log_request(): try: - _LOGGER.debug(f"@@@ REQUEST {method.upper()} to '{full_url}' length {len(data or '')}") + _LOGGER.debug( + f"@@@ REQUEST {method.upper()} to '{full_url}' length {len(data or '')}" + ) if data: _LOGGER.debug(f" content {data}") except Exception as err: @@ -722,7 +767,9 @@ async def log_response(resp: aiohttp.ClientResponse): try: contents = await resp.read() _LOGGER.debug(f"@@@ RESPONSE {resp.status} length {len(contents)}") - _LOGGER.debug(f" header {dict((k, v) for k, v in resp.headers.items())}") + _LOGGER.debug( + f" header {dict((k, v) for k, v in resp.headers.items())}" + ) if not contents: return if resp.status != 200: @@ -754,7 +801,9 @@ async def log_response(resp: aiohttp.ClientResponse): if resp.status == 401: # Unauthorized await self._refresh_token() if iteration > API_RETRIES: - raise RequestRetryError(f"Request to {full_url} failed after {iteration} retries") + raise RequestRetryError( + f"Request to {full_url} failed after {iteration} retries" + ) return await self._request(url, iteration=iteration + 1) elif resp.status == 204: # No content @@ -779,7 +828,7 @@ async def log_response(resp: aiohttp.ClientResponse): raise RequestError( f"{method} request to {full_url} failed with status {resp.status}: {resp}", - resp.status + resp.status, ) except (asyncio.TimeoutError, aiohttp.ClientError) as err: @@ -823,26 +872,29 @@ async def build(self): self.stand_alone_chargers = so_chargers if not self._const: - # Get the API constants self._const = await self._request("constants") # Find the chargers device types device_types = set( - chg.device_type - for chg in self.map.values() - if isinstance(chg, Charger) + chg.device_type for chg in self.map.values() if isinstance(chg, Charger) ) # Define the remaps - self._obs_ids = Account._get_remap(self._const, ["Observations", "ObservationIds"], device_types) - self._set_ids = Account._get_remap(self._const, ["Settings", "SettingIds"], device_types) - self._cmd_ids = Account._get_remap(self._const, ["Commands", "CommandIds"], device_types) + self._obs_ids = Account._get_remap( + self._const, ["Observations", "ObservationIds"], device_types + ) + self._set_ids = Account._get_remap( + self._const, ["Settings", "SettingIds"], device_types + ) + self._cmd_ids = Account._get_remap( + self._const, ["Commands", "CommandIds"], device_types + ) # Commands can also be specified as lower case strings - self._cmd_ids.update({ - to_under(k): v for k, v in self._cmd_ids.items() if isinstance(k, str) - }) + self._cmd_ids.update( + {to_under(k): v for k, v in self._cmd_ids.items() if isinstance(k, str)} + ) # Update the state on all chargers for data in self.map.values(): @@ -858,7 +910,7 @@ def update(self, data: TDict): if cls_id is not None: klass = self.map.get(cls_id) if klass: - d = Account._state_to_attrs([data], 'StateId', self._obs_ids) + d = Account._state_to_attrs([data], "StateId", self._obs_ids) klass.set_attributes(d) else: _LOGGER.warning("Got update for unknown charger id %s", cls_id) @@ -867,13 +919,13 @@ def update(self, data: TDict): @staticmethod def _get_remap(const, wanted, device_types=None) -> dict: - ''' Parse the given zaptec constants record `const` and generate - a remap dict for the given `wanted` keys. If `device_types` is - specified, the entries for these device schemas will be merged - with the main remap dict. - Example: - _get_remap(const, ["Observations", "ObservationIds"], [4]) - ''' + """Parse the given zaptec constants record `const` and generate + a remap dict for the given `wanted` keys. If `device_types` is + specified, the entries for these device schemas will be merged + with the main remap dict. + Example: + _get_remap(const, ["Observations", "ObservationIds"], [4]) + """ ids = {} for k, v in const.items(): if k in wanted: @@ -893,11 +945,13 @@ def _get_remap(const, wanted, device_types=None) -> dict: return ids @staticmethod - def _state_to_attrs(data: Iterable[dict[str, str]], key: str, keydict: dict[str, str]): - ''' Convert a list of state data into a dict of attributes. `key` - is the key that specifies the attribute name. `keydict` is a - dict that maps the key value to an attribute name. - ''' + def _state_to_attrs( + data: Iterable[dict[str, str]], key: str, keydict: dict[str, str] + ): + """Convert a list of state data into a dict of attributes. `key` + is the key that specifies the attribute name. `keydict` is a + dict that maps the key value to an attribute name. + """ out = {} for item in data: skey = item.get(key) @@ -908,7 +962,9 @@ def _state_to_attrs(data: Iterable[dict[str, str]], key: str, keydict: dict[str, if value is not MISSING: kv = keydict.get(skey, f"{key} {skey}") if kv in out: - _LOGGER.debug("Duplicate key %s. Is '%s', new '%s'", kv, out[kv], value) + _LOGGER.debug( + "Duplicate key %s. Is '%s', new '%s'", kv, out[kv], value + ) out[kv] = value return out @@ -924,9 +980,7 @@ async def gogo(): acc = Account( username, password, - client=aiohttp.ClientSession( - connector=aiohttp.TCPConnector(ssl=False) - ), + client=aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)), ) try: diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py index 1a24a96..8087589 100644 --- a/custom_components/zaptec/binary_sensor.py +++ b/custom_components/zaptec/binary_sensor.py @@ -6,7 +6,10 @@ from homeassistant import const from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription) + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -21,7 +24,6 @@ class ZaptecBinarySensor(ZaptecBaseEntity, BinarySensorEntity): - @callback def _update_from_zaptec(self) -> None: try: @@ -34,14 +36,12 @@ def _update_from_zaptec(self) -> None: class ZaptecBinarySensorWithAttrs(ZaptecBinarySensor): - def _post_init(self): self._attr_extra_state_attributes = self.zaptec_obj.asdict() self._attr_unique_id = self.zaptec_obj.id class ZaptecBinarySensorLock(ZaptecBinarySensor): - @callback def _update_from_zaptec(self) -> None: try: @@ -55,8 +55,7 @@ def _update_from_zaptec(self) -> None: @dataclass class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): - - cls: type|None = None + cls: type | None = None INSTALLATION_ENTITIES: list[EntityDescription] = [ diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index b0e21ed..f2d73e2 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from homeassistant import const -from homeassistant.components.button import (ButtonDeviceClass, ButtonEntity, - ButtonEntityDescription) +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -21,14 +24,14 @@ class ZaptecButton(ZaptecBaseEntity, ButtonEntity): - zaptec_obj: Charger async def async_press(self) -> None: """Press the button.""" _LOGGER.debug( "Press %s.%s (in %s)", - self.__class__.__qualname__, self.key, + self.__class__.__qualname__, + self.key, self.zaptec_obj.id, ) @@ -42,15 +45,12 @@ async def async_press(self) -> None: @dataclass class ZapButtonEntityDescription(ButtonEntityDescription): - - cls: type|None = None + cls: type | None = None -INSTALLATION_ENTITIES: list[EntityDescription] = [ -] +INSTALLATION_ENTITIES: list[EntityDescription] = [] -CIRCUIT_ENTITIES: list[EntityDescription] = [ -] +CIRCUIT_ENTITIES: list[EntityDescription] = [] CHARGER_ENTITIES: list[EntityDescription] = [ ZapButtonEntityDescription( @@ -66,24 +66,24 @@ class ZapButtonEntityDescription(ButtonEntityDescription): ZapButtonEntityDescription( key="authorize_charge", translation_key="authorize_charge", - icon="mdi:lock-check-outline" + icon="mdi:lock-check-outline", ), ZapButtonEntityDescription( key="deauthorize_and_stop", translation_key="deauthorize_and_stop", - icon="mdi:lock-remove-outline" + icon="mdi:lock-remove-outline", ), ZapButtonEntityDescription( key="restart_charger", translation_key="restart_charger", entity_category=const.EntityCategory.DIAGNOSTIC, - icon="mdi:restart" + icon="mdi:restart", ), ZapButtonEntityDescription( key="upgrade_firmware", translation_key="upgrade_firmware", entity_category=const.EntityCategory.DIAGNOSTIC, - icon="mdi:memory" + icon="mdi:memory", ), ] diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index 37d210d..e200c09 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -36,8 +36,11 @@ REQUEST_REFRESH_DELAY = 0.3 + class Missing: - '''Singleton class representing a missing value.''' + """Singleton class representing a missing value.""" + + MISSING = Missing() TRUTHY = ["true", "1", "on", "yes", 1, True] diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py index 324b186..f7dd2ad 100644 --- a/custom_components/zaptec/diagnostics.py +++ b/custom_components/zaptec/diagnostics.py @@ -21,28 +21,44 @@ # USE WITH CAUTION! This will include sensitive data in the output. INCLUDE_REDACTS = False + class Redactor: - """ Class to handle redaction of sensitive data. """ + """Class to handle redaction of sensitive data.""" # Data fields that must be redacted from the output REDACT_KEYS = [ - "Address", "City", "Latitude", "Longitude", "ZipCode", - "Pin", "SerialNo", "LogoBase64", - "Id", "CircuitId", "DeviceId", "InstallationId", "MID", "ChargerId", - "Name", "InstallationName", "SignedMeterValue", - "MacWiFi", "LteImsi", "LteIccid", "LteImei", + "Address", + "City", + "Latitude", + "Longitude", + "ZipCode", + "Pin", + "SerialNo", + "LogoBase64", + "Id", + "CircuitId", + "DeviceId", + "InstallationId", + "MID", + "ChargerId", + "Name", + "InstallationName", + "SignedMeterValue", + "MacWiFi", + "LteImsi", + "LteIccid", + "LteImei", "NewChargeCard", ] # Keys that will be looked up into the observer id dict - OBS_KEYS = [ - "SettingId", "StateId" - ] + OBS_KEYS = ["SettingId", "StateId"] # Key names that will be redacted if they the dict has a OBS_KEY entry # and it is in the REDACT_KEYS list. VALUES = [ - "ValueAsString", "Value", + "ValueAsString", + "Value", ] def __init__(self, redacted, acc): @@ -52,9 +68,9 @@ def __init__(self, redacted, acc): self.redact_info = {} def redact(self, text: str, make_new=None, ctx=None): - ''' Redact the text if it is present in the redacted dict. - A new redaction is created if make_new is True - ''' + """Redact the text if it is present in the redacted dict. + A new redaction is created if make_new is True + """ if not self.redacted: return text elif text in self.redacts: @@ -63,8 +79,8 @@ def redact(self, text: str, make_new=None, ctx=None): red = f"<--Redact #{len(self.redacts) + 1}-->" self.redacts[text] = red self.redact_info[red] = { # For statistics only - 'text': text, - 'from': f"{make_new} in {ctx}", + "text": text, + "from": f"{make_new} in {ctx}", } return red if isinstance(text, str): @@ -74,9 +90,9 @@ def redact(self, text: str, make_new=None, ctx=None): return text def redact_obj_inplace(self, obj, ctx=None): - ''' Iterate over obj and redact the fields. NOTE! This function - modifies the argument object in-place. - ''' + """Iterate over obj and redact the fields. NOTE! This function + modifies the argument object in-place. + """ if isinstance(obj, list): for k in obj: self.redact_obj_inplace(k, ctx=ctx) @@ -87,16 +103,20 @@ def redact_obj_inplace(self, obj, ctx=None): if isinstance(v, (list, dict)): self.redact_obj_inplace(v, ctx=ctx) continue - obj[k] = self.redact(v, make_new=k if k in self.REDACT_KEYS else None, ctx=ctx) + obj[k] = self.redact( + v, make_new=k if k in self.REDACT_KEYS else None, ctx=ctx + ) return obj def redact_statelist(self, objs, ctx=None): - '''Redact the special state list objects.''' + """Redact the special state list objects.""" for obj in objs: for key in self.OBS_KEYS: if key not in obj: continue - keyv = self.acc._obs_ids.get(obj[key]) # FIXME: Access to private member + keyv = self.acc._obs_ids.get( + obj[key] + ) # FIXME: Access to private member if keyv is not None: obj[key] = f"{obj[key]} ({keyv})" if keyv not in self.REDACT_KEYS: @@ -117,7 +137,7 @@ async def async_get_device_diagnostics( acc: Account = coordinator.account out = {} - api = out.setdefault('api', {}) + api = out.setdefault("api", {}) # Helper to redact the output data red = Redactor(DO_REDACT, acc) @@ -137,7 +157,7 @@ def gen(url, obj, ctx=None): # data = await req(url := "installation") - installation_ids = [inst['Id'] for inst in data.get('Data',[])] + installation_ids = [inst["Id"] for inst in data.get("Data", [])] gen(url, data, ctx="installation") circuit_ids = [] @@ -145,10 +165,10 @@ def gen(url, obj, ctx=None): for inst_id in installation_ids: data = await req(url := f"installation/{inst_id}/hierarchy") - for circuit in data.get('Circuits', []): - circuit_ids.append(circuit['Id']) - for charger in circuit.get('Chargers', []): - charger_in_circuits_ids.append(charger['Id']) + for circuit in data.get("Circuits", []): + circuit_ids.append(circuit["Id"]) + for charger in circuit.get("Chargers", []): + charger_in_circuits_ids.append(charger["Id"]) gen(url, data, ctx="hierarchy") @@ -160,7 +180,7 @@ def gen(url, obj, ctx=None): gen(url, data, ctx="circuit") data = await req(url := "chargers") - charger_ids = [charger['Id'] for charger in data.get('Data',[])] + charger_ids = [charger["Id"] for charger in data.get("Data", [])] gen(url, data, ctx="chargers") for charger_id in set([*charger_ids, *charger_in_circuits_ids]): @@ -179,21 +199,24 @@ def gen(url, obj, ctx=None): # MAPPINGS # - out.setdefault('maps', [ - red.redact_obj_inplace(deepcopy(obj._attrs), ctx='maps') for obj in acc.map.values() - ]) + out.setdefault( + "maps", + [ + red.redact_obj_inplace(deepcopy(obj._attrs), ctx="maps") + for obj in acc.map.values() + ], + ) # # REDACTED DATA # if INCLUDE_REDACTS: - out.setdefault('redacts', red.redact_info) + out.setdefault("redacts", red.redact_info) return out if __name__ == "__main__": - # Just to execute the script manually. Must be run using # python -m custom_components.zaptec.diagnostics import asyncio @@ -208,13 +231,10 @@ async def gogo(): acc = Account( username, password, - client=aiohttp.ClientSession( - connector=aiohttp.TCPConnector(ssl=False) - ), + client=aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)), ) try: - # # Mocking to pretend to be a hass instance # @@ -231,7 +251,7 @@ class FakeCoordinator: account: Account coordinator = FakeCoordinator(account=acc) - config = FakeConfig(entry_id='') + config = FakeConfig(entry_id="") hass = FakeHass(data={DOMAIN: {config.entry_id: coordinator}}) # Get the diagnostics info diff --git a/custom_components/zaptec/misc.py b/custom_components/zaptec/misc.py index ae83814..db41070 100644 --- a/custom_components/zaptec/misc.py +++ b/custom_components/zaptec/misc.py @@ -24,7 +24,7 @@ def data_producer(data: bytes): while data: block = data[:count] data = data[count:] - count = (yield block) + count = yield block prod = data_producer(msg) next(prod) @@ -33,7 +33,7 @@ def data_producer(data: bytes): SHORT_XMLNS_ATTRIBUTE = 0x08 SHORT_ELEMENT = 0x40 CHARS8TEXT = 0x98 - CHARS16TEXT = 0x9a + CHARS16TEXT = 0x9A def read_string(bits16=False): """Read a string.""" @@ -53,7 +53,10 @@ def frame_decoder(): return if record_type in ( - SHORT_ELEMENT, SHORT_XMLNS_ATTRIBUTE, CHARS8TEXT, CHARS16TEXT, + SHORT_ELEMENT, + SHORT_XMLNS_ATTRIBUTE, + CHARS8TEXT, + CHARS16TEXT, ): yield record_type, read_string(bits16=(record_type == CHARS16TEXT)) elif record_type == END_ELEMENT: diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index ea7e5ae..4ce9ecd 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from homeassistant import const -from homeassistant.components.number import (NumberDeviceClass, NumberEntity, - NumberEntityDescription) +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -22,7 +25,6 @@ class ZaptecNumber(ZaptecBaseEntity, NumberEntity): - @callback def _update_from_zaptec(self) -> None: try: @@ -35,20 +37,23 @@ def _update_from_zaptec(self) -> None: class ZaptecAvailableCurrentNumber(ZaptecNumber): - zaptec_obj: Installation def _post_init(self): # Get the max current rating from the reported max current - self.entity_description.native_max_value = self.zaptec_obj.get('max_current', 32) + self.entity_description.native_max_value = self.zaptec_obj.get( + "max_current", 32 + ) async def async_set_native_value(self, value: float) -> None: """Update to Zaptec.""" _LOGGER.debug( "Setting %s.%s to <%s> %s (in %s)", - self.__class__.__qualname__, self.key, - type(value).__qualname__, value, - self.zaptec_obj.id + self.__class__.__qualname__, + self.key, + type(value).__qualname__, + value, + self.zaptec_obj.id, ) try: @@ -60,37 +65,39 @@ async def async_set_native_value(self, value: float) -> None: class ZaptecSettingNumber(ZaptecNumber): - zaptec_obj: Installation def _post_init(self): # Get the max current rating from the reported max current - self.entity_description.native_max_value = self.zaptec_obj.get('charge_current_installation_max_limit', 32) + self.entity_description.native_max_value = self.zaptec_obj.get( + "charge_current_installation_max_limit", 32 + ) async def async_set_native_value(self, value: float) -> None: """Update to Zaptec.""" _LOGGER.debug( "Setting %s.%s to <%s> %s (in %s)", - self.__class__.__qualname__, self.key, - type(value).__qualname__, value, - self.zaptec_obj.id + self.__class__.__qualname__, + self.key, + type(value).__qualname__, + value, + self.zaptec_obj.id, ) try: - await self.zaptec_obj.set_settings({ - self.entity_description.setting: value - }) + await self.zaptec_obj.set_settings({self.entity_description.setting: value}) except Exception as exc: - raise HomeAssistantError(f"Setting {self.entity_description.setting} to {value} failed") from exc + raise HomeAssistantError( + f"Setting {self.entity_description.setting} to {value} failed" + ) from exc await self.coordinator.async_request_refresh() @dataclass class ZapNumberEntityDescription(NumberEntityDescription): - - cls: type|None = None - setting: str|None = None + cls: type | None = None + setting: str | None = None INSTALLATION_ENTITIES: list[EntityDescription] = [ @@ -106,8 +113,7 @@ class ZapNumberEntityDescription(NumberEntityDescription): ), ] -CIRCUIT_ENTITIES: list[EntityDescription] = [ -] +CIRCUIT_ENTITIES: list[EntityDescription] = [] CHARGER_ENTITIES: list[EntityDescription] = [ ZapNumberEntityDescription( diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index 1efdaf5..d15203b 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from homeassistant import const -from homeassistant.components.sensor import (SensorDeviceClass, SensorEntity, - SensorEntityDescription) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -23,7 +26,6 @@ class ZaptecSensor(ZaptecBaseEntity, SensorEntity): - @callback def _update_from_zaptec(self) -> None: try: @@ -36,7 +38,6 @@ def _update_from_zaptec(self) -> None: class ZaptecChargeSensor(ZaptecSensor): - @callback def _update_from_zaptec(self) -> None: try: @@ -55,7 +56,7 @@ def _update_from_zaptec(self) -> None: class ZapSensorEntityDescription(SensorEntityDescription): """Provide a description of a Zaptec sensor.""" - cls: type|None = None + cls: type | None = None INSTALLATION_ENTITIES: list[EntityDescription] = [ @@ -166,7 +167,7 @@ class ZapSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, ), ZapSensorEntityDescription( - key='completed_session.Energy', + key="completed_session.Energy", translation_key="completed_session_energy", device_class=SensorDeviceClass.ENERGY, icon="mdi:counter", diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index e6beab4..d801567 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -18,21 +18,26 @@ # ==================== has_id_schema = vol.Schema({vol.Required("charger_id"): str}) -has_limit_current_schema = vol.Schema(vol.SomeOf( - min_valid=1, max_valid=1, msg="Must specify either only available_current or all " - "three available_current_phaseX (where X is 1-3). They are mutually exclusive", - validators=[ - { - vol.Required("installation_id"): str, - vol.Required("available_current"): int, - }, - { - vol.Required("installation_id"): str, - vol.Required("available_current_phase1"): int, - vol.Required("available_current_phase2"): int, - vol.Required("available_current_phase3"): int, - }, -])) +has_limit_current_schema = vol.Schema( + vol.SomeOf( + min_valid=1, + max_valid=1, + msg="Must specify either only available_current or all " + "three available_current_phaseX (where X is 1-3). They are mutually exclusive", + validators=[ + { + vol.Required("installation_id"): str, + vol.Required("available_current"): int, + }, + { + vol.Required("installation_id"): str, + vol.Required("available_current_phase1"): int, + vol.Required("available_current_phase2"): int, + vol.Required("available_current_phase3"): int, + }, + ], + ) +) async def async_setup_services(hass: HomeAssistant) -> None: @@ -94,13 +99,13 @@ async def service_handle_limit_current(service_call: ServiceCall) -> None: # LIST OF SERVICES services: list[tuple[str, vol.Schema, TServiceHandler]] = [ - ("stop_charging", has_id_schema, service_handle_stop_charging), - ("resume_charging", has_id_schema, service_handle_resume_charging), - ("authorize_charging", has_id_schema, service_handle_authorize_charging), + ("stop_charging", has_id_schema, service_handle_stop_charging), + ("resume_charging", has_id_schema, service_handle_resume_charging), + ("authorize_charging", has_id_schema, service_handle_authorize_charging), ("deauthorize_charging", has_id_schema, service_handle_deauthorize_charging), - ("restart_charger", has_id_schema, service_handle_restart_charger), - ("update_firmware", has_id_schema, service_handle_update_firmware), - ("limit_current", has_limit_current_schema, service_handle_limit_current), + ("restart_charger", has_id_schema, service_handle_restart_charger), + ("update_firmware", has_id_schema, service_handle_update_firmware), + ("limit_current", has_limit_current_schema, service_handle_limit_current), ] # Register the services diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index c233c2f..1fb2199 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -4,8 +4,11 @@ import logging from dataclasses import dataclass -from homeassistant.components.switch import (SwitchDeviceClass, SwitchEntity, - SwitchEntityDescription) +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -20,7 +23,6 @@ class ZaptecSwitch(ZaptecBaseEntity, SwitchEntity): - @callback def _update_from_zaptec(self) -> None: try: @@ -33,7 +35,6 @@ def _update_from_zaptec(self) -> None: class ZaptecChargeSwitch(ZaptecSwitch): - zaptec_obj: Charger @callback @@ -51,7 +52,8 @@ async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument """Turn on the switch.""" _LOGGER.debug( "Turn on %s.%s (in %s)", - self.__class__.__qualname__, self.key, + self.__class__.__qualname__, + self.key, self.zaptec_obj.id, ) @@ -66,7 +68,8 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument """Turn off the switch.""" _LOGGER.debug( "Turn off %s.%s (in %s)", - self.__class__.__qualname__, self.key, + self.__class__.__qualname__, + self.key, self.zaptec_obj.id, ) @@ -79,14 +82,14 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument class ZaptecAuthorizationRequiredSwitch(ZaptecSwitch): - zaptec_obj: Installation async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument """Turn on the switch.""" _LOGGER.debug( "Turn on %s.%s (in %s)", - self.__class__.__qualname__, self.key, + self.__class__.__qualname__, + self.key, self.zaptec_obj.id, ) @@ -101,7 +104,8 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument """Turn off the switch.""" _LOGGER.debug( "Turn off %s.%s (in %s)", - self.__class__.__qualname__, self.key, + self.__class__.__qualname__, + self.key, self.zaptec_obj.id, ) @@ -115,8 +119,7 @@ async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument @dataclass class ZapSwitchEntityDescription(SwitchEntityDescription): - - cls: type|None = None + cls: type | None = None INSTALLATION_SWITCH_TYPES: list[EntityDescription] = [ @@ -129,8 +132,7 @@ class ZapSwitchEntityDescription(SwitchEntityDescription): ), ] -CIRCUIT_SWITCH_TYPES: list[EntityDescription] = [ -] +CIRCUIT_SWITCH_TYPES: list[EntityDescription] = [] CHARGER_SWITCH_TYPES: list[EntityDescription] = [ ZapSwitchEntityDescription( diff --git a/custom_components/zaptec/update.py b/custom_components/zaptec/update.py index 51d949b..185b5c0 100644 --- a/custom_components/zaptec/update.py +++ b/custom_components/zaptec/update.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from homeassistant import const -from homeassistant.components.update import (UpdateDeviceClass, UpdateEntity, - UpdateEntityDescription) +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -21,14 +24,17 @@ class ZaptecUpdate(ZaptecBaseEntity, UpdateEntity): - zaptec_obj: Charger @callback def _update_from_zaptec(self) -> None: try: - self._attr_installed_version = self._get_zaptec_value(key="current_firmware_version") - self._attr_latest_version = self._get_zaptec_value(key="available_firmware_version") + self._attr_installed_version = self._get_zaptec_value( + key="current_firmware_version" + ) + self._attr_latest_version = self._get_zaptec_value( + key="available_firmware_version" + ) self._attr_available = True self._log_value(self._attr_installed_version) except (KeyError, AttributeError): @@ -38,8 +44,9 @@ def _update_from_zaptec(self) -> None: async def async_install(self, version, backup): _LOGGER.debug( "Updating firmware %s.%s (in %s)", - self.__class__.__qualname__, self.key, - self.zaptec_obj.id + self.__class__.__qualname__, + self.key, + self.zaptec_obj.id, ) try: @@ -52,15 +59,12 @@ async def async_install(self, version, backup): @dataclass class ZapUpdateEntityDescription(UpdateEntityDescription): - - cls: type|None = None + cls: type | None = None -INSTALLATION_ENTITIES: list[EntityDescription] = [ -] +INSTALLATION_ENTITIES: list[EntityDescription] = [] -CIRCUIT_ENTITIES: list[EntityDescription] = [ -] +CIRCUIT_ENTITIES: list[EntityDescription] = [] CHARGER_ENTITIES: list[EntityDescription] = [ ZapUpdateEntityDescription( diff --git a/custom_components/zaptec/validate.py b/custom_components/zaptec/validate.py index 70c28e9..2366bf8 100644 --- a/custom_components/zaptec/validate.py +++ b/custom_components/zaptec/validate.py @@ -47,7 +47,7 @@ class ChargerStates(TypeWrapper, BaseModel): class ChargerSetting(BaseModel): model_config = ConfigDict(extra="allow") SettingsId: int - Value: str = '' + Value: str = "" # pydantic v2 @@ -108,41 +108,38 @@ class InstallationConnectionDetails(BaseModel): # Mapping of URL to pydantic model URLS = { - 'installation': Installations, - 'chargers': Chargers, - 'constants': None, - r'installation/[0-9a-f\-]+': Installation, - r'installation/[0-9a-f\-]+/hierarchy': Hierarchy, - r'installation/[0-9a-f\-]+/update': None, - r'installation/[0-9a-f\-]+/messagingConnectionDetails': InstallationConnectionDetails, - r'circuits/[0-9a-f\-]+': Circuit, - r'chargers/[0-9a-f\-]+': Charger, - r'chargers/[0-9a-f\-]+/state': ChargerStates, - r'chargers/[0-9a-f\-]+/settings': ChargerSettings, - r'chargers/[0-9a-f\-]+/authorizecharge': None, - r'chargers/[0-9a-f\-]+/SendCommand/[0-9]+': None, - r'chargerFirmware/installation/[0-9a-f\-]+': ChargerFirmwares, + "installation": Installations, + "chargers": Chargers, + "constants": None, + r"installation/[0-9a-f\-]+": Installation, + r"installation/[0-9a-f\-]+/hierarchy": Hierarchy, + r"installation/[0-9a-f\-]+/update": None, + r"installation/[0-9a-f\-]+/messagingConnectionDetails": InstallationConnectionDetails, + r"circuits/[0-9a-f\-]+": Circuit, + r"chargers/[0-9a-f\-]+": Charger, + r"chargers/[0-9a-f\-]+/state": ChargerStates, + r"chargers/[0-9a-f\-]+/settings": ChargerSettings, + r"chargers/[0-9a-f\-]+/authorizecharge": None, + r"chargers/[0-9a-f\-]+/SendCommand/[0-9]+": None, + r"chargerFirmware/installation/[0-9a-f\-]+": ChargerFirmwares, } -_URLS = [ - (k, re.compile(k), v) for k, v in URLS.items() -] +_URLS = [(k, re.compile(k), v) for k, v in URLS.items()] + def validate(data, url): """Validate the data.""" for pat, re_pat, model in _URLS: - # Mathes either the exact string or its regexp if url == pat or re_pat.fullmatch(url): - try: d = data # pydantic v1 if isinstance(model, TypeWrapper): - d = {'_data': data} - + d = {"_data": data} + if isinstance(model, BaseModel): # pydantic v1 model.parse_obj(d) @@ -153,7 +150,7 @@ def validate(data, url): # pydantic v2 # elif isinstance(model, TypeAdapter): # model.validate_python(data, strict=True) - + except ValidationError as err: _LOGGER.error("Failed to validate %s (pattern %s): %s", url, pat, err) raise From 95f9186f7adfb26b1e98d2e7edd00dd1f0ba8e98 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 18 Sep 2023 11:57:50 +0200 Subject: [PATCH 06/17] Fixed OCMF decoding on attributes and improve diagnostics redaction --- LICENSE | 3 +- custom_components/zaptec/api.py | 81 +++++++++++------------ custom_components/zaptec/diagnostics.py | 87 +++++++++++++------------ 3 files changed, 86 insertions(+), 85 deletions(-) diff --git a/LICENSE b/LICENSE index fa76e13..5941a35 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ MIT License -Copyright (c) 2021 Hellowlol and contributers -Copyright (c) 2023 Svein Seldal, @sveinse +Copyright (c) 2023 Hellowlol, Svein Seldal and contributers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index 1525b30..026c668 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -38,7 +38,7 @@ # to Support running this as a script. if __name__ == "__main__": - from const import API_RETRIES, API_URL, FALSY, MISSING, TOKEN_URL, TRUTHY + from const import API_RETRIES, API_URL, MISSING, TOKEN_URL, TRUTHY from misc import mc_nbfx_decoder, to_under from validate import validate @@ -50,7 +50,6 @@ from .const import ( API_RETRIES, API_URL, - FALSY, MISSING, TOKEN_URL, TRUTHY, @@ -83,6 +82,27 @@ class RequestRetryError(ZaptecApiError): """Retries too many times""" +# +# Attribute type converters +# +def type_ocmf(data): + """Open Charge Metering Format (OCMF) type""" + # https://github.com/SAFE-eV/OCMF-Open-Charge-Metering-Format/blob/master/OCMF-en.md + sects = data.split("|") + if len(sects) not in (2, 3) or sects[0] != "OCMF": + raise ValueError(f"Invalid OCMF data: {data}") + data = json.loads(sects[1]) + return data + + +def type_completed_session(data): + """Convert the CompletedSession to a dict""" + data = json.loads(data) + if "SignedSession" in data: + data["SignedSession"] = type_ocmf(data["SignedSession"]) + return data + + class ZaptecBase(ABC): """Base class for Zaptec objects""" @@ -104,7 +124,18 @@ def set_attributes(self, data: TDict) -> bool: for k, v in data.items(): # Cast the value to the correct type new_key = to_under(k) - new_v = self.ATTR_TYPES.get(new_key, lambda x: x)(v) + try: + new_v = self.ATTR_TYPES.get(new_key, lambda x: x)(v) + except Exception as err: + _LOGGER.error( + "Failed to convert attribute %s (%s) value <%s> %s: %s", + k, + new_key, + type(v).__qualname__, + v, + err, + ) + new_v = v new_vt = type(new_v).__qualname__ if new_key not in self._attrs: qn = self.__class__.__qualname__ @@ -456,16 +487,17 @@ class Charger(ZaptecBase): # Type conversions for the named attributes ATTR_TYPES = { "active": bool, - "is_authorization_required": lambda x: x in TRUTHY, - "is_online": lambda x: x in TRUTHY, "charge_current_installation_max_limit": float, "charger_max_current": float, "charger_min_current": float, - "completed_session": json.loads, + "completed_session": type_completed_session, "current_phase1": float, "current_phase2": float, "current_phase3": float, + "is_authorization_required": lambda x: x in TRUTHY, + "is_online": lambda x: x in TRUTHY, "permanent_cable_lock": lambda x: x in TRUTHY, + "signed_meter_value": type_ocmf, "total_charge_power": float, "voltage_phase1": float, "voltage_phase2": float, @@ -570,43 +602,6 @@ async def live(self): # FIXME: Missing validator (see validate) return data - async def update(self, data): - # FIXME: Is this in use or an experiment? Should it be removed from production code? - - # https://api.zaptec.com/help/index.html#/Charger/post_api_chargers__id__update - # Not really sure this should be added as ppl might use it wrong - cmd = f"chargers/{self.id}/update" - default = { - # nullable: true - # Adjustable between 0 and 32A. If charge current is below the charger minimum charge current (usually 6A), no charge current will be allocated. - "maxChargeCurrent": 0, - # MaxPhaseinteger($int32) # Enum 1,3 - "maxChargePhases": "", - # The minimum allocated charge current. If there is not enough current available to provide the - # chargers minimum current it will not be able to charge. - # Usually set to match the vehicle minimum current for charging (defaults to 6A) - "minChargeCurrent": None, - # Adjustable between 0 and 32A. If offline charge current is below the charger minimum charge current (usually 6A), - # no charge current will be allocated when offline. - # Offline current override should only be done in special cases where charging stations should not automatically optimize offline current. - # In most cases this setting should be set to -1 to allow ZapCloud to optimise offline current. If -1, offline current will be automatically allocated. - "offlineChargeCurrent": None, - # Phasesinteger($int32) ENUM - # 0 = None - # 1 = Phase_1 - # 2 = Phase_2 - # 4 = Phase_3 - # 7 = All - "offlineChargePhase": None, - # nullable - # The interval in seconds for a charger to report meter values. Defaults to 900 seconds for Pro and 3600 seconds for Go - "meterValueInterval": None, - } - - pass - - # return await self._account.request(cmd, data=data, method="post") - def type_operation_mode(self, v): modes = { str(v): k for k, v in self._account._const["ChargerOperationModes"].items() diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py index f7dd2ad..fa64084 100644 --- a/custom_components/zaptec/diagnostics.py +++ b/custom_components/zaptec/diagnostics.py @@ -28,27 +28,32 @@ class Redactor: # Data fields that must be redacted from the output REDACT_KEYS = [ "Address", - "City", - "Latitude", - "Longitude", - "ZipCode", - "Pin", - "SerialNo", - "LogoBase64", - "Id", + "ChargerId", + "ChargerCurrentUserUuid", "CircuitId", + "City", "DeviceId", + "Id", + "ID", "InstallationId", - "MID", - "ChargerId", - "Name", "InstallationName", - "SignedMeterValue", - "MacWiFi", - "LteImsi", + "Latitude", + "LogoBase64", + "Longitude", "LteIccid", "LteImei", + "LteImsi", + "MacWiFi", + "MacMain", + "MacPlcModuleGrid", + "MID", + "Name", "NewChargeCard", + "PilotTestResults", + "Pin", + "ProductionTestResults", + "SerialNo", + "ZipCode", ] # Keys that will be looked up into the observer id dict @@ -57,21 +62,22 @@ class Redactor: # Key names that will be redacted if they the dict has a OBS_KEY entry # and it is in the REDACT_KEYS list. VALUES = [ - "ValueAsString", "Value", + "ValueAsString", ] - def __init__(self, redacted, acc): - self.redacted = redacted - self.acc = acc + def __init__(self, do_redact: bool, obs_ids: dict[str, str]): + self.do_redact = do_redact + self.obs_ids = obs_ids self.redacts = {} self.redact_info = {} def redact(self, text: str, make_new=None, ctx=None): """Redact the text if it is present in the redacted dict. - A new redaction is created if make_new is True + A new redaction is created if make_new is not None. ctx is only + for logging output. """ - if not self.redacted: + if not self.do_redact: return text elif text in self.redacts: return self.redacts[text] @@ -89,22 +95,24 @@ def redact(self, text: str, make_new=None, ctx=None): text = text.replace(k, v) return text - def redact_obj_inplace(self, obj, ctx=None): + def redact_obj_inplace(self, obj, ctx=None, secondpass=False): """Iterate over obj and redact the fields. NOTE! This function modifies the argument object in-place. """ if isinstance(obj, list): for k in obj: - self.redact_obj_inplace(k, ctx=ctx) + self.redact_obj_inplace(k, ctx=ctx, secondpass=secondpass) return obj elif not isinstance(obj, dict): - return obj + return self.redact(obj, ctx=ctx) for k, v in obj.items(): if isinstance(v, (list, dict)): - self.redact_obj_inplace(v, ctx=ctx) + self.redact_obj_inplace(v, ctx=ctx, secondpass=secondpass) continue obj[k] = self.redact( - v, make_new=k if k in self.REDACT_KEYS else None, ctx=ctx + v, + make_new=k if not secondpass and k in self.REDACT_KEYS else None, + ctx=ctx, ) return obj @@ -114,9 +122,7 @@ def redact_statelist(self, objs, ctx=None): for key in self.OBS_KEYS: if key not in obj: continue - keyv = self.acc._obs_ids.get( - obj[key] - ) # FIXME: Access to private member + keyv = self.obs_ids.get(obj[key]) if keyv is not None: obj[key] = f"{obj[key]} ({keyv})" if keyv not in self.REDACT_KEYS: @@ -140,7 +146,7 @@ async def async_get_device_diagnostics( api = out.setdefault("api", {}) # Helper to redact the output data - red = Redactor(DO_REDACT, acc) + red = Redactor(DO_REDACT, acc._obs_ids) # FIXME: Access to private member async def req(url): try: @@ -148,17 +154,16 @@ async def req(url): except Exception as err: return {"failed": str(err)} - def gen(url, obj, ctx=None): + def add(url, obj, ctx=None): red.redact_obj_inplace(obj, ctx=ctx) api[red.redact(url)] = obj # # API FETCHING # - data = await req(url := "installation") installation_ids = [inst["Id"] for inst in data.get("Data", [])] - gen(url, data, ctx="installation") + add(url, data, ctx="installation") circuit_ids = [] charger_in_circuits_ids = [] @@ -170,35 +175,34 @@ def gen(url, obj, ctx=None): for charger in circuit.get("Chargers", []): charger_in_circuits_ids.append(charger["Id"]) - gen(url, data, ctx="hierarchy") + add(url, data, ctx="hierarchy") data = await req(url := f"installation/{inst_id}") - gen(url, data, ctx="installation") + add(url, data, ctx="installation") for circ_id in circuit_ids: data = await req(url := f"circuits/{circ_id}") - gen(url, data, ctx="circuit") + add(url, data, ctx="circuit") data = await req(url := "chargers") charger_ids = [charger["Id"] for charger in data.get("Data", [])] - gen(url, data, ctx="chargers") + add(url, data, ctx="chargers") for charger_id in set([*charger_ids, *charger_in_circuits_ids]): data = await req(url := f"chargers/{charger_id}") - gen(url, data, ctx="charger") + add(url, data, ctx="charger") data = await req(url := f"chargers/{charger_id}/state") red.redact_statelist(data, ctx="state") - gen(url, data, ctx="state") + add(url, data, ctx="state") data = await req(url := f"chargers/{charger_id}/settings") red.redact_statelist(data.values(), ctx="settings") - gen(url, data, ctx="settings") + add(url, data, ctx="settings") # # MAPPINGS # - out.setdefault( "maps", [ @@ -207,6 +211,9 @@ def gen(url, obj, ctx=None): ], ) + # 2nd pass to replace any newer redacted text within the output. + red.redact_obj_inplace(out, secondpass=True) + # # REDACTED DATA # From 49742177ce71f5f74175d794eb54bca00afe60b5 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Sun, 24 Sep 2023 23:47:27 +0200 Subject: [PATCH 07/17] Fixed issues * Fix timeout issues, sveinse/zaptec#19 * Fix sensor stateclass, making it possible to use kwh from zaptec in HA energy, sveinse/zaptec#16 --- custom_components/zaptec/__init__.py | 5 ++++- custom_components/zaptec/api.py | 12 +++--------- custom_components/zaptec/const.py | 2 ++ custom_components/zaptec/diagnostics.py | 5 +++-- custom_components/zaptec/number.py | 2 +- custom_components/zaptec/sensor.py | 2 ++ 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index 859aafb..ae46e84 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -28,6 +28,7 @@ from .api import Account, ZaptecApiError, ZaptecBase from .const import ( + API_TIMEOUT, DEFAULT_SCAN_INTERVAL, DOMAIN, MANUFACTURER, @@ -146,7 +147,9 @@ async def _async_update_data(self) -> None: """Fetch data from Zaptec.""" try: - async with async_timeout.timeout(10): + # This timeout is only a safeguard against the API methods locking + # up. The API methods themselves have their own timeouts. + async with async_timeout.timeout(10 * API_TIMEOUT): if not self.account.is_built: # Build the Zaptec hierarchy await self.account.build() diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index 026c668..002c4f5 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -38,7 +38,7 @@ # to Support running this as a script. if __name__ == "__main__": - from const import API_RETRIES, API_URL, MISSING, TOKEN_URL, TRUTHY + from const import API_RETRIES, API_TIMEOUT, API_URL, MISSING, TOKEN_URL, TRUTHY from misc import mc_nbfx_decoder, to_under from validate import validate @@ -47,13 +47,7 @@ logging.getLogger("azure").setLevel(logging.WARNING) else: - from .const import ( - API_RETRIES, - API_URL, - MISSING, - TOKEN_URL, - TRUTHY, - ) + from .const import API_RETRIES, API_TIMEOUT, API_URL, MISSING, TOKEN_URL, TRUTHY from .misc import mc_nbfx_decoder, to_under from .validate import validate @@ -780,7 +774,7 @@ async def log_response(resp: aiohttp.ClientResponse): } full_url = API_URL + url try: - async with async_timeout.timeout(30): + async with async_timeout.timeout(API_TIMEOUT): if DEBUG_API_CALLS: log_request() diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index e200c09..e188bfc 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -34,6 +34,8 @@ DEFAULT_SCAN_INTERVAL = 60 +API_TIMEOUT = 30 + REQUEST_REFRESH_DELAY = 0.3 diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py index fa64084..47ae93d 100644 --- a/custom_components/zaptec/diagnostics.py +++ b/custom_components/zaptec/diagnostics.py @@ -227,10 +227,11 @@ def add(url, obj, ctx=None): # Just to execute the script manually. Must be run using # python -m custom_components.zaptec.diagnostics import asyncio - import aiohttp import os - from pprint import pprint from dataclasses import dataclass + from pprint import pprint + + import aiohttp async def gogo(): username = os.environ.get("zaptec_username") diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index 4ce9ecd..fa58274 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -11,11 +11,11 @@ NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.const import EntityCategory from . import ZaptecBaseEntity, ZaptecUpdateCoordinator from .api import Installation diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index d15203b..a83c604 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -9,6 +9,7 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -165,6 +166,7 @@ class ZapSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, icon="mdi:counter", native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, ), ZapSensorEntityDescription( key="completed_session.Energy", From 0d1dde6e48781b6054e90567c46a0fa809dc22ae Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Sun, 1 Oct 2023 19:27:12 +0200 Subject: [PATCH 08/17] Improvements * Add support for selecting which chargers to add, sveinse/zaptec#5 * Add support for adding optional prefix, sveinse/zaptec#4 * Adding API timeout to 60 secs, sveinse/zaptec#19 * Updated UI messages * Fixup general error handling * Change API to allow for supporting selecting which chargers to use --- custom_components/zaptec/__init__.py | 141 +++++++++---- custom_components/zaptec/api.py | 199 +++++++++++------- custom_components/zaptec/binary_sensor.py | 1 + custom_components/zaptec/button.py | 1 + custom_components/zaptec/config_flow.py | 151 ++++++++++--- custom_components/zaptec/const.py | 5 +- custom_components/zaptec/number.py | 1 + custom_components/zaptec/sensor.py | 1 + custom_components/zaptec/switch.py | 1 + custom_components/zaptec/translations/en.json | 42 ++-- custom_components/zaptec/update.py | 1 + 11 files changed, 380 insertions(+), 164 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index ae46e84..a35a92d 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -9,6 +9,7 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -29,6 +30,8 @@ from .api import Account, ZaptecApiError, ZaptecBase from .const import ( API_TIMEOUT, + CONF_CHARGERS, + CONF_MANUAL_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN, MANUFACTURER, @@ -111,6 +114,8 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: _LOGGER.debug("Setting up coordinator") + self._config = entry.data + self.account = Account( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], @@ -134,7 +139,7 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: ) async def cancel_streams(self): - await asyncio.gather(*(i.cancel_stream() for i in self.account.installs)) + await asyncio.gather(*(i.cancel_stream() for i in self.account.installations)) @callback async def _stream_update(self, event): @@ -154,16 +159,55 @@ async def _async_update_data(self) -> None: # Build the Zaptec hierarchy await self.account.build() + # Get the list if chargers to include + chargers = None + if self._config.get(CONF_MANUAL_SELECT, False): + chargers = self._config.get(CONF_CHARGERS) + + # Selected chargers to add + if chargers is not None: + _LOGGER.debug("Configured chargers: %s", chargers) + want = set(chargers) + all_objects = set(self.account.map.keys()) + + # Log if there are any objects listed not found in Zaptec + not_present = want - all_objects + if not_present: + _LOGGER.error("Charger objects %s not found", not_present) + + # Calculate the objects to keep. From the list of chargers + # we want to keep, we also want to keep the circuit and + # installation objects. + keep = set() + for charger in self.account.get_chargers(): + if charger.id in want: + keep.add(charger.id) + if charger.circuit: + keep.add(charger.circuit.id) + if charger.circuit.installation: + keep.add(charger.circuit.installation.id) + + if not keep: + _LOGGER.error("No zaptec objects will be added") + + # Unregister all discovered objects not in the keep list + for objid in all_objects: + if objid not in keep: + _LOGGER.debug("Unregistering: %s", objid) + self.account.unregister(objid) + # Setup the stream subscription - for install in self.account.installs: - await install.stream(cb=self._stream_update) - return + for install in self.account.installations: + if install.id in self.account.map: + await install.stream(cb=self._stream_update) # Fetch updates - for k, v in self.account.map.items(): - await v.state() + await self.account.update_states() + except ZaptecApiError as err: - _LOGGER.exception(f"{err}") + _LOGGER.exception( + "Fetching data failed: %s: %s", type(err).__qualname__, err + ) raise UpdateFailed(err) from err @@ -267,8 +311,9 @@ def _log_unavailable(self): @classmethod def create_from( cls, - sensors: list[EntityDescription], + descriptions: list[EntityDescription], coordinator: ZaptecUpdateCoordinator, + entry: ConfigEntry, zaptec_obj: ZaptecBase, device_info: DeviceInfo, ) -> list[ZaptecBaseEntity]: @@ -276,16 +321,21 @@ def create_from( EntityDescription objects. """ + # Calculate the prefix to use for the entity name + prefix = entry.data.get(CONF_NAME, "").rstrip() + if prefix: + prefix = prefix + " " + # Start with the common device info and append the provided device info dev_info = DeviceInfo( manufacturer=MANUFACTURER, identifiers={(DOMAIN, zaptec_obj.id)}, - name=zaptec_obj.name, + name=prefix + zaptec_obj.name, ) dev_info.update(device_info) entities = [] - for description in sensors: + for description in descriptions: # Use provided class if it exists, otherwise use the class this # function was called from klass = getattr(description, "cls", cls) or cls @@ -298,6 +348,7 @@ def create_from_zaptec( cls, account: Account, coordinator: ZaptecUpdateCoordinator, + entry: ConfigEntry, installation_entities: list[EntityDescription], circuit_entities: list[EntityDescription], charger_entities: list[EntityDescription], @@ -308,55 +359,63 @@ def create_from_zaptec( """ entities = [] - for zap_install in account.installs: - entities.extend( - cls.create_from( - installation_entities, - coordinator, - zap_install, - DeviceInfo( - model=f"{zap_install.name} Installation", - ), - ) - ) - - for zap_circuit in zap_install.circuits: + for zap_install in account.installations: + if zap_install.id in account.map: entities.extend( cls.create_from( - circuit_entities, + installation_entities, coordinator, - zap_circuit, + entry, + zap_install, DeviceInfo( - model=f"{zap_circuit.name} Circuit", - via_device=(DOMAIN, zap_install.id), + model=f"{zap_install.name} Installation", ), ) ) - for zap_charger in zap_circuit.chargers: + for zap_circuit in zap_install.circuits: + if zap_circuit.id in account.map: entities.extend( cls.create_from( - charger_entities, + circuit_entities, coordinator, - zap_charger, + entry, + zap_circuit, DeviceInfo( - model=f"{zap_charger.name} Charger", - via_device=(DOMAIN, zap_circuit.id), + model=f"{zap_circuit.name} Circuit", + via_device=(DOMAIN, zap_install.id), ), ) ) + for zap_charger in zap_circuit.chargers: + if zap_charger.id in account.map: + entities.extend( + cls.create_from( + charger_entities, + coordinator, + entry, + zap_charger, + DeviceInfo( + model=f"{zap_charger.name} Charger", + via_device=(DOMAIN, zap_circuit.id), + ), + ) + ) + for zap_charger in account.stand_alone_chargers: - entities.extend( - cls.create_from( - charger_entities, - coordinator, - zap_charger, - DeviceInfo( - model=f"{zap_charger.name} Charger", - ), + if zap_charger.id in account.map: + entities.extend( + cls.create_from( + charger_entities, + coordinator, + entry, + zap_charger, + DeviceInfo( + model=f"{zap_charger.name} Charger", + ), + ) ) - ) return entities diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index 002c4f5..390ede3 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -11,7 +11,7 @@ from typing import Any, Callable import aiohttp -import async_timeout +import pydantic # pylint: disable=missing-function-docstring @@ -56,7 +56,7 @@ class ZaptecApiError(Exception): """Base exception for all Zaptec API errors""" -class AuthorizationError(ZaptecApiError): +class AuthenticationError(ZaptecApiError): """Authenatication failed""" @@ -68,6 +68,10 @@ def __init__(self, message, error_code): self.error_code = error_code +class RequestConnectionError(ZaptecApiError): + """Failed to make the request to the API""" + + class RequestTimeoutError(ZaptecApiError): """Failed to get the results from the API""" @@ -76,6 +80,10 @@ class RequestRetryError(ZaptecApiError): """Retries too many times""" +class RequestDataError(ZaptecApiError): + """Data is not valid""" + + # # Attribute type converters # @@ -195,9 +203,6 @@ def __init__(self, data, account): async def build(self): """Build the installation object hierarchy.""" - # Get state to ensure we have the full and updated data - await self.state() - # Get the hierarchy of circurits and chargers try: hierarchy = await self._account._request( @@ -216,7 +221,7 @@ async def build(self): circuits = [] for item in hierarchy["Circuits"]: _LOGGER.debug(" Circuit %s", item["Id"]) - circ = Circuit(item, self._account) + circ = Circuit(item, self._account, installation=self) self._account.register(item["Id"], circ) await circ.build() circuits.append(circ) @@ -438,20 +443,20 @@ class Circuit(ZaptecBase): "is_authorisation_required": bool, } - def __init__(self, data, account): + def __init__( + self, data: TDict, account: Account, installation: Installation | None = None + ): super().__init__(data, account) self.chargers = [] + self.installation = installation async def build(self): """Build the python interface.""" - # Get state to ensure we have the full and updated data - await self.state() - chargers = [] for item in self._attrs["chargers"]: _LOGGER.debug(" Charger %s", item["Id"]) - chg = Charger(item, self._account) + chg = Charger(item, self._account, circuit=self) self._account.register(item["Id"], chg) await chg.build() chargers.append(chg) @@ -498,9 +503,13 @@ class Charger(ZaptecBase): "voltage_phase3": float, } - def __init__(self, data: TDict, account: "Account") -> None: + def __init__( + self, data: TDict, account: "Account", circuit: Circuit | None = None + ) -> None: super().__init__(data, account) + self.circuit = circuit + # Append the attr types that depends on self attr_types = self.ATTR_TYPES.copy() attr_types.update( @@ -675,14 +684,16 @@ async def set_current_in_maxium(self, value): class Account: """This class represent an zaptec account""" - def __init__(self, username: str, password: str, client=None) -> None: + def __init__( + self, username: str, password: str, client: aiohttp.ClientSession | None = None + ) -> None: _LOGGER.debug("Account init") self._username = username self._password = password - self._client = client + self._client = client or aiohttp.ClientSession() self._token_info = {} self._access_token = None - self.installs: list[Installation] = [] + self.installations: list[Installation] = [] self.stand_alone_chargers: list[Charger] = [] self.map: dict[str, ZaptecBase] = {} self._const = {} @@ -690,14 +701,16 @@ def __init__(self, username: str, password: str, client=None) -> None: self._set_ids = {} self._cmd_ids = {} self.is_built = False - - if client is None: - self._client = aiohttp.ClientSession() + self._timeout = aiohttp.ClientTimeout(total=API_TIMEOUT) def register(self, id: str, data: ZaptecBase): """Register an object data with id""" self.map[id] = data + def unregister(self, id: str): + """Unregister an object data with id""" + del self.map[id] + # ======================================================================= # API METHODS @@ -709,17 +722,23 @@ async def check_login(username: str, password: str) -> bool: "grant_type": "password", } try: - async with aiohttp.request("POST", TOKEN_URL, data=p) as resp: + timeout = aiohttp.ClientTimeout(total=API_TIMEOUT) + async with aiohttp.request( + "POST", TOKEN_URL, data=p, timeout=timeout + ) as resp: if resp.status == 200: data = await resp.json() return True else: - raise AuthorizationError( + raise AuthenticationError( f"Failed to authenticate. Got status {resp.status}" ) - except aiohttp.ClientConnectorError as err: - _LOGGER.exception("Bad things happend while trying to authenticate :(") - raise + except asyncio.TimeoutError as err: + _LOGGER.error("Authentication timeout") + raise RequestTimeoutError("Authenticaton timed out") from err + except aiohttp.ClientConnectionError as err: + _LOGGER.error("Authentication request failed: %s", err) + raise RequestConnectionError("Authentication request failed") from err async def _refresh_token(self): # So for some reason they used grant_type password.. @@ -729,7 +748,7 @@ async def _refresh_token(self): "password": self._password, "grant_type": "password", } - async with aiohttp.request("POST", TOKEN_URL, data=p) as resp: + async with self._client.post(TOKEN_URL, data=p, timeout=self._timeout) as resp: if resp.status == 200: data = await resp.json() # The data includes the time the access token expires @@ -737,12 +756,14 @@ async def _refresh_token(self): self._token_info.update(data) self._access_token = data.get("access_token") else: - raise AuthorizationError( + _LOGGER.error("Failed to authenticate. Got code %s", resp.status) + raise AuthenticationError( "Failed to refresh token, check your credentials." ) async def _request(self, url: str, method="get", data=None, iteration=1): def log_request(): + """Log the request""" try: _LOGGER.debug( f"@@@ REQUEST {method.upper()} to '{full_url}' length {len(data or '')}" @@ -753,6 +774,7 @@ def log_request(): _LOGGER.exception("Failed to log response") async def log_response(resp: aiohttp.ClientResponse): + """Log the response""" try: contents = await resp.read() _LOGGER.debug(f"@@@ RESPONSE {resp.status} length {len(contents)}") @@ -768,60 +790,89 @@ async def log_response(resp: aiohttp.ClientResponse): except Exception as err: _LOGGER.exception("Failed to log response") + async def log_error(resp: aiohttp.ClientResponse, msg, *args): + """Log the error""" + if not DEBUG_API_ERRORS: + return + _LOGGER.error(msg, *args) + if not DEBUG_API_CALLS: + log_request() + await log_response(resp) + header = { "Authorization": f"Bearer {self._access_token}", "Accept": "application/json", } full_url = API_URL + url + + if DEBUG_API_CALLS: + log_request() + + request_fn = getattr(self._client, method) + if request_fn is None: + raise ValueError(f"Unknown method {method}") + if data is not None and method in ("post", "put"): + request_fn = partial(request_fn, json=data) + try: - async with async_timeout.timeout(API_TIMEOUT): + resp: aiohttp.ClientResponse + async with request_fn( + full_url, headers=header, timeout=self._timeout + ) as resp: if DEBUG_API_CALLS: - log_request() - - call = getattr(self._client, method) - if data is not None and method in ("post", "put"): - call = partial(call, json=data) - - resp: aiohttp.ClientResponse - async with call(full_url, headers=header) as resp: - if DEBUG_API_CALLS: - await log_response(resp) - - if resp.status == 401: # Unauthorized - await self._refresh_token() - if iteration > API_RETRIES: - raise RequestRetryError( - f"Request to {full_url} failed after {iteration} retries" - ) - return await self._request(url, iteration=iteration + 1) + await log_response(resp) + + if resp.status == 401: # Unauthorized + await self._refresh_token() + if iteration > API_RETRIES: + await log_error(resp, "Failed reauthentication too many times") + raise RequestRetryError( + f"Request to {full_url} failed after {iteration} retries" + ) + return await self._request(url, iteration=iteration + 1) - elif resp.status == 204: # No content - content = await resp.read() - return content + elif resp.status == 204: # No content + content = await resp.read() + return content - elif resp.status == 200: # OK - # FIXME: This will raise json error if the json is invalid. How to handle this? + elif resp.status == 200: # OK + # Read the JSON payload + try: json_result = await resp.json(content_type=None) - - # Validate the incoming json data - # FIXME: This raise pydantic.ValidationError if the json is unexpected. How to handle this? + except json.JSONDecodeError as err: + await log_error(resp, "Failed to decode json: %s", err) + raise RequestDataError( + f"Failed to decode json: {err}", resp.status + ) from err + + # Validate the incoming json data + try: validate(json_result, url=url) + except pydantic.ValidationError as err: + await log_error(resp, "Failed to validate data: %s", err) + raise RequestDataError( + f"Failed to validate data: {err}", resp.status + ) from err - return json_result + return json_result - else: - if DEBUG_API_ERRORS and not DEBUG_API_CALLS: - _LOGGER.debug("Failing request:") - log_request() - await log_response(resp) - - raise RequestError( - f"{method} request to {full_url} failed with status {resp.status}: {resp}", - resp.status, - ) + else: + await log_error(resp, "Failing request to %s", full_url) + raise RequestError( + f"{method} request to {full_url} failed with status {resp.status}: {resp}", + resp.status, + ) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - raise RequestTimeoutError(f"Request to {full_url} failed: {err}") from err + except asyncio.TimeoutError as err: + if DEBUG_API_ERRORS: + _LOGGER.error(f"Request to %s timed out", full_url) + raise RequestTimeoutError(f"Request to {full_url} timed out") from err + except aiohttp.ClientConnectionError as err: + if DEBUG_API_ERRORS: + _LOGGER.error("Request to %s failed: %s", full_url, err) + raise RequestConnectionError( + f"Request to {full_url} failed: {err}" + ) from err # API METHODS DONE # ======================================================================= @@ -841,7 +892,7 @@ async def build(self): await inst.build() installs.append(inst) - self.installs = installs + self.installations = installs # Get list of chargers # Will also report chargers listed in installation hierarchy above @@ -885,13 +936,14 @@ async def build(self): {to_under(k): v for k, v in self._cmd_ids.items() if isinstance(k, str)} ) - # Update the state on all chargers + self.is_built = True + + async def update_states(self, id: str | None = None): + """Update the state for the given id. If id is None, all""" for data in self.map.values(): - if isinstance(data, Charger): + if id is None or data.id == id: await data.state() - self.is_built = True - def update(self, data: TDict): """update for the stream. Note build has to called first.""" @@ -906,6 +958,10 @@ def update(self, data: TDict): else: _LOGGER.warning("Unknown update message %s", data) + def get_chargers(self): + """Return a list of all chargers""" + return [v for v in self.map.values() if isinstance(v, Charger)] + @staticmethod def _get_remap(const, wanted, device_types=None) -> dict: """Parse the given zaptec constants record `const` and generate @@ -969,7 +1025,6 @@ async def gogo(): acc = Account( username, password, - client=aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)), ) try: @@ -992,7 +1047,7 @@ async def gogo(): # outfile.write(json.dumps(data, indent=2) + '\n') # outfile.flush() - # for ins in acc.installs: + # for ins in acc.installations: # await ins._stream(cb=cb) finally: diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py index 8087589..ae97585 100644 --- a/custom_components/zaptec/binary_sensor.py +++ b/custom_components/zaptec/binary_sensor.py @@ -120,6 +120,7 @@ async def async_setup_entry( entities = ZaptecBinarySensor.create_from_zaptec( acc, coordinator, + entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index f2d73e2..8f85ced 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -99,6 +99,7 @@ async def async_setup_entry( entities = ZaptecButton.create_from_zaptec( acc, coordinator, + entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/config_flow.py b/custom_components/zaptec/config_flow.py index e4c6d3f..f5f050c 100644 --- a/custom_components/zaptec/config_flow.py +++ b/custom_components/zaptec/config_flow.py @@ -2,13 +2,30 @@ from __future__ import annotations import logging +from typing import Any -import aiohttp +import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant import config_entries - -from .api import Account, AuthorizationError -from .const import DOMAIN +from homeassistant.config_entries import data_entry_flow +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .api import ( + Account, + AuthenticationError, + Charger, + RequestConnectionError, + RequestDataError, + RequestRetryError, + RequestTimeoutError, +) +from .const import CONF_CHARGERS, CONF_MANUAL_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -17,58 +34,124 @@ class ZaptecFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Blueprint.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize.""" - self._errors = {} + self._account: Account | None = None + self._input: dict[str, Any] = {} async def async_step_user( - self, user_input=None - ): # pylint: disable=dangerous-default-value - """Handle a flow initialized by the user.""" - errors = {} - - entries = self.hass.config_entries.async_entries(DOMAIN) - if entries: - return self.async_abort(reason="already_setup") - - data_schema = { - vol.Required("username", default=""): str, - vol.Required("password", default=""): str, - vol.Optional("scan_interval", default=30): vol.Coerce(int), - } + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle a flow initiated by the user.""" + errors: dict[str, str] = {} - placeholders = { - "username": "firstname.lastname@gmail.com", - "password": "your password", - "scan_interval": 30, - } + if self._async_current_entries(): + return self.async_abort(reason="already_exists") if user_input is not None: valid_login = False try: valid_login = await Account.check_login( - user_input["username"], user_input["password"] + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], ) - except aiohttp.ClientConnectorError: - errors["base"] = "connection_failure" - except AuthorizationError: - errors["base"] = "auth_failure" + except (RequestConnectionError, RequestTimeoutError): + errors["base"] = "cannot_connect" + except AuthenticationError: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" if valid_login: - unique_id = user_input["username"].lower() + unique_id = user_input[CONF_USERNAME].lower() await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() + if user_input.get(CONF_MANUAL_SELECT, False): + self._input = user_input + return await self.async_step_chargers() + return self.async_create_entry( title=DOMAIN.capitalize(), data=user_input ) + data = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_NAME): str, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.Coerce( + int + ), + vol.Optional(CONF_MANUAL_SELECT): bool, + } + return self.async_show_form( step_id="user", - data_schema=vol.Schema(data_schema), - description_placeholders=placeholders, + data_schema=vol.Schema(data), + errors=errors, + ) + + async def async_step_chargers( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle login steps""" + errors: dict[str, str] = {} + + if user_input is not None: + if user_input.get(CONF_CHARGERS): + self._input.update(user_input) + + return self.async_create_entry( + title=DOMAIN.capitalize(), data=self._input + ) + + errors["base"] = "no_chargers_selected" + + try: + if not self._account: + self._account = Account( + username=self._input[CONF_USERNAME], + password=self._input[CONF_PASSWORD], + client=async_get_clientsession(self.hass), + ) + + # Build the hierarchy, but don't fetch any detailed data yet + if not self._account.is_built: + await self._account.build() + + # Get all chargers + chargers = self._account.get_chargers() + except (RequestConnectionError, RequestTimeoutError, RequestDataError): + errors["base"] = "cannot_connect" + chargers = [] + except (AuthenticationError, RequestRetryError): + errors["base"] = "invalid_auth" + chargers = [] + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + chargers = [] + + def charger_text(charger: Charger): + text = f"{charger.name} ({getattr(charger, 'device_id', '-')})" + circuit = charger.circuit + if circuit: + text += f" in {circuit.name} circuit" + if circuit.installation: + text += f" of {circuit.installation.name} installation" + return text + + data = { + vol.Required(CONF_CHARGERS): cv.multi_select( + {charger.id: charger_text(charger) for charger in chargers}, + ), + } + + return self.async_show_form( + step_id="chargers", + data_schema=vol.Schema(data), errors=errors, ) @@ -77,4 +160,4 @@ async def async_step_import(self, user_input): # pylint: disable=unused-argumen Special type of import, we're not actually going to store any data. Instead, we're going to rely on the values that are in config file. """ - return self.async_create_entry(title="configuration.yaml", data={}) + return await self.async_step_user(user_input) diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index e188bfc..e4c50fb 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -34,10 +34,13 @@ DEFAULT_SCAN_INTERVAL = 60 -API_TIMEOUT = 30 +API_TIMEOUT = 60 REQUEST_REFRESH_DELAY = 0.3 +CONF_MANUAL_SELECT = "manual_select" +CONF_CHARGERS = "chargers" + class Missing: """Singleton class representing a missing value.""" diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index fa58274..46bc73f 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -154,6 +154,7 @@ async def async_setup_entry( entities = ZaptecNumber.create_from_zaptec( acc, coordinator, + entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index a83c604..d75718a 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -189,6 +189,7 @@ async def async_setup_entry( entities = ZaptecSensor.create_from_zaptec( acc, coordinator, + entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index 1fb2199..6b5b67b 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -155,6 +155,7 @@ async def async_setup_entry( switches = ZaptecSwitch.create_from_zaptec( acc, coordinator, + entry, INSTALLATION_SWITCH_TYPES, CIRCUIT_SWITCH_TYPES, CHARGER_SWITCH_TYPES, diff --git a/custom_components/zaptec/translations/en.json b/custom_components/zaptec/translations/en.json index 62d0e25..c3518e4 100644 --- a/custom_components/zaptec/translations/en.json +++ b/custom_components/zaptec/translations/en.json @@ -1,23 +1,33 @@ { - "config":{ - "error":{ - "auth_failure":"Unable to connect to Zaptec. Check your username and password.", - "connection_failure":"Unable to connect to Zaptec. Test again later.", - "refused_failure":"Connection refused to Zaptec. Test again later." - }, - "step":{ - "user":{ - "title":"Zaptec", - "description":"If you need help with this integration: https://github.com/custom-components/zaptec", - "data":{ - "username":"Username", - "password":"Password", - "scan_interval":"Scan interval" + "config": { + "step": { + "user": { + "title": "Zaptec setup", + "description": "Add your Zaptec Portal login details.\n\nThe optional prefix will add the prefix to all devices. Note that it is generally better to rename the device names in HA than adding a prefix.", + "data": { + "username": "Username", + "password": "Password", + "name": "Optional prefix", + "scan_interval": "Scan interval", + "manual_select": "Manually select chargers. Next screen will select the chargers to add." + } + }, + "chargers": { + "title": "Zaptec charger selection", + "description": "Select the chargers you want to add to Home Assistant.", + "data": { + "chargers": "Chargers" } } }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_chargers_selected": "No chargers selected. Please select at least one charger." + }, "abort":{ - "single_instance_allowed":"Only a single instance is allowed." + "already_exists": "One instance of Zaptec already exists." } }, "entity": { @@ -45,7 +55,7 @@ "charger_max_current": { "name": "Charger max current" } }, "binary_sensor": { - "is_authorization_required": { + "is_authorization_required": { "name": "Authorization required", "state": { "off": "Not required", diff --git a/custom_components/zaptec/update.py b/custom_components/zaptec/update.py index 185b5c0..7c539f3 100644 --- a/custom_components/zaptec/update.py +++ b/custom_components/zaptec/update.py @@ -88,6 +88,7 @@ async def async_setup_entry( entities = ZaptecUpdate.create_from_zaptec( acc, coordinator, + entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, From 8e9132777b5b88b3a8183aa32e4bda7b716a39f2 Mon Sep 17 00:00:00 2001 From: Gustav Ahlberg Date: Fri, 6 Oct 2023 12:15:40 +0200 Subject: [PATCH 09/17] chore: Update to use latest devcontainer Based on the latest version of https://github.com/ludeeus/integration_blueprint --- .devcontainer.json | 42 ++++++++++++++ .devcontainer/README.md | 60 -------------------- .devcontainer/devcontainer.json | 30 ---------- .gitignore | 4 ++ .vscode/launch.json | 35 ------------ .vscode/settings.json | 12 ---- .vscode/tasks.json | 24 +------- {.devcontainer => config}/configuration.yaml | 0 requirements.txt | 4 ++ scripts/develop | 20 +++++++ scripts/lint | 7 +++ scripts/setup | 7 +++ 12 files changed, 87 insertions(+), 158 deletions(-) create mode 100644 .devcontainer.json delete mode 100644 .devcontainer/README.md delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json rename {.devcontainer => config}/configuration.yaml (100%) create mode 100644 requirements.txt create mode 100755 scripts/develop create mode 100755 scripts/lint create mode 100755 scripts/setup diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..0e84100 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,42 @@ +{ + "name": "Zaptec integration development", + "image": "mcr.microsoft.com/devcontainers/python:0-3.11-bullseye", + "postCreateCommand": "scripts/setup", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "github.vscode-pull-request-github", + "ryanluker.vscode-coverage-gutters", + "ms-python.vscode-pylance" + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "python.pythonPath": "/usr/bin/python3", + "python.analysis.autoSearchPaths": false, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } + } + }, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/rust:1": {} + } +} diff --git a/.devcontainer/README.md b/.devcontainer/README.md deleted file mode 100644 index e304a9a..0000000 --- a/.devcontainer/README.md +++ /dev/null @@ -1,60 +0,0 @@ -## Developing with Visual Studio Code + devcontainer - -The easiest way to get started with custom integration development is to use Visual Studio Code with devcontainers. This approach will create a preconfigured development environment with all the tools you need. - -In the container you will have a dedicated Home Assistant core instance running with your custom component code. You can configure this instance by updating the `./devcontainer/configuration.yaml` file. - -**Prerequisites** - -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- Docker - - For Linux, macOS, or Windows 10 Pro/Enterprise/Education use the [current release version of Docker](https://docs.docker.com/install/) - - Windows 10 Home requires [WSL 2](https://docs.microsoft.com/windows/wsl/wsl2-install) and the current Edge version of Docker Desktop (see instructions [here](https://docs.docker.com/docker-for-windows/wsl-tech-preview/)). This can also be used for Windows Pro/Enterprise/Education. -- [Visual Studio code](https://code.visualstudio.com/) -- [Remote - Containers (VSC Extension)][extension-link] - -[More info about requirements and devcontainer in general](https://code.visualstudio.com/docs/remote/containers#_getting-started) - -[extension-link]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers - -**Getting started:** - -1. Fork the repository. -2. Clone the repository to your computer. -3. Open the repository using Visual Studio code. - -When you open this repository with Visual Studio code you are asked to "Reopen in Container", this will start the build of the container. - -_If you don't see this notification, open the command palette and select `Remote-Containers: Reopen Folder in Container`._ - -### Tasks - -The devcontainer comes with some useful tasks to help you with development, you can start these tasks by opening the command palette and select `Tasks: Run Task` then select the task you want to run. - -When a task is currently running (like `Run Home Assistant on port 9123` for the docs), it can be restarted by opening the command palette and selecting `Tasks: Restart Running Task`, then select the task you want to restart. - -The available tasks are: - -Task | Description --- | -- -Run Home Assistant on port 9123 | Launch Home Assistant with your custom component code and the configuration defined in `.devcontainer/configuration.yaml`. -Run Home Assistant configuration against /config | Check the configuration. -Upgrade Home Assistant to latest dev | Upgrade the Home Assistant core version in the container to the latest version of the `dev` branch. -Install a specific version of Home Assistant | Install a specific version of Home Assistant core in the container. - -### Step by Step debugging - -With the development container, -you can test your custom component in Home Assistant with step by step debugging. - -You need to modify the `configuration.yaml` file in `.devcontainer` folder -by uncommenting the line: - -```yaml -# debugpy: -``` - -Then launch the task `Run Home Assistant on port 9123`, and launch the debugger -with the existing debugging configuration `Python: Attach Local`. - -For more information, look at [the Remote Python Debugger integration documentation](https://www.home-assistant.io/integrations/debugpy/). diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 345babf..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,30 +0,0 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. -{ - "image": "ghcr.io/ludeeus/devcontainer/integration:stable", - "name": "Zaptec integration development", - "context": "..", - "appPort": [ - "9123:8123" - ], - "postCreateCommand": "container install", - "extensions": [ - "ms-python.python", - "github.vscode-pull-request-github", - "ryanluker.vscode-coverage-gutters", - "ms-python.vscode-pylance" - ], - "settings": { - "files.eol": "\n", - "editor.tabSize": 4, - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/bin/python3", - "python.analysis.autoSearchPaths": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2b21fe9..5c35464 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ __pycache__ # Python virtual environments venv* + +# Home Assistant configuration +config/* +!config/configuration.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 555a62b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - // Example of attaching to local debug server - "name": "Python: Attach Local", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost", - "pathMappings": [ - { - "localRoot": "${workspaceFolder}", - "remoteRoot": "." - } - ] - }, - { - // Example of attaching to my production server - "name": "Python: Attach Remote", - "type": "python", - "request": "attach", - "port": 5678, - "host": "homeassistant.local", - "pathMappings": [ - { - "localRoot": "${workspaceFolder}", - "remoteRoot": "/usr/src/homeassistant" - } - ] - } - ] - } - \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f7f6619..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.pythonPath": "/usr/local/bin/python", - "files.associations": { - "*.yaml": "home-assistant" - }, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "python.formatting.provider": "none" -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7ab4ba8..c6ee32f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,28 +2,10 @@ "version": "2.0.0", "tasks": [ { - "label": "Run Home Assistant on port 9123", + "label": "Run Home Assistant on port 8123", "type": "shell", - "command": "container start", - "problemMatcher": [] - }, - { - "label": "Run Home Assistant configuration against /config", - "type": "shell", - "command": "container check", - "problemMatcher": [] - }, - { - "label": "Upgrade Home Assistant to latest dev", - "type": "shell", - "command": "container install", - "problemMatcher": [] - }, - { - "label": "Install a specific version of Home Assistant", - "type": "shell", - "command": "container set-version", + "command": "scripts/develop", "problemMatcher": [] } ] -} \ No newline at end of file +} diff --git a/.devcontainer/configuration.yaml b/config/configuration.yaml similarity index 100% rename from .devcontainer/configuration.yaml rename to config/configuration.yaml diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ad6f856 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +colorlog==6.7.0 +homeassistant==2023.8.0 +pip>=21.0,<23.2 +ruff==0.0.267 diff --git a/scripts/develop b/scripts/develop new file mode 100755 index 0000000..89eda50 --- /dev/null +++ b/scripts/develop @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/integration_blueprint +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..9b5b1df --- /dev/null +++ b/scripts/lint @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +ruff check . --fix diff --git a/scripts/setup b/scripts/setup new file mode 100755 index 0000000..141d19f --- /dev/null +++ b/scripts/setup @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip install --requirement requirements.txt From 8fe9879567d19bb3cf778fb092ab5a772c0be391 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 9 Oct 2023 16:14:36 +0000 Subject: [PATCH 10/17] Updated devcontainer --- .devcontainer.json | 20 ++++++++++++++++---- requirements.txt | 2 +- scripts/setup | 12 +++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.devcontainer.json b/.devcontainer.json index 0e84100..b2389df 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "Zaptec integration development", - "image": "mcr.microsoft.com/devcontainers/python:0-3.11-bullseye", + "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye", "postCreateCommand": "scripts/setup", "forwardPorts": [ 8123 @@ -9,6 +9,14 @@ "8123": { "label": "Home Assistant", "onAutoForward": "notify" + }, + "0-8122": { + "label": "Auto-Forwarded - Other", + "onAutoForward": "ignore" + }, + "8124-999999": { + "label": "Auto-Forwarded - Other", + "onAutoForward": "ignore" } }, "customizations": { @@ -17,12 +25,16 @@ "ms-python.python", "github.vscode-pull-request-github", "ryanluker.vscode-coverage-gutters", - "ms-python.vscode-pylance" + "ms-python.vscode-pylance", + "visualstudioexptteam.vscodeintellicode", + "redhat.vscode-yaml", + "esbenp.prettier-vscode" ], "settings": { "files.eol": "\n", "editor.tabSize": 4, - "python.pythonPath": "/usr/bin/python3", + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.pythonPath": "/usr/local/python/bin/python", "python.analysis.autoSearchPaths": false, "python.linting.pylintEnabled": true, "python.linting.enabled": true, @@ -37,6 +49,6 @@ }, "remoteUser": "vscode", "features": { - "ghcr.io/devcontainers/features/rust:1": {} + "rust": "latest" } } diff --git a/requirements.txt b/requirements.txt index ad6f856..b5178cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ colorlog==6.7.0 -homeassistant==2023.8.0 +homeassistant==2023.6.0 pip>=21.0,<23.2 ruff==0.0.267 diff --git a/scripts/setup b/scripts/setup index 141d19f..550c150 100755 --- a/scripts/setup +++ b/scripts/setup @@ -4,4 +4,14 @@ set -e cd "$(dirname "$0")/.." -python3 -m pip install --requirement requirements.txt +pip_packages() { + python3 -m pip \ + install \ + --upgrade \ + --disable-pip-version-check \ + "${@}" +} + +pip_packages "pip<23.2,>=21.3.1" +pip_packages setuptools wheel +pip_packages --requirement requirements.txt From c2de9e27d39199b6f20b6da219221e10232d2905 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 9 Oct 2023 16:43:09 +0000 Subject: [PATCH 11/17] Minor cleanups and simplifications --- custom_components/zaptec/__init__.py | 145 ++++++++++------------ custom_components/zaptec/binary_sensor.py | 3 - custom_components/zaptec/button.py | 3 - custom_components/zaptec/diagnostics.py | 4 +- custom_components/zaptec/number.py | 3 - custom_components/zaptec/sensor.py | 3 - custom_components/zaptec/switch.py | 7 +- custom_components/zaptec/update.py | 3 - 8 files changed, 73 insertions(+), 98 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index a35a92d..fe39630 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -27,7 +27,7 @@ UpdateFailed, ) -from .api import Account, ZaptecApiError, ZaptecBase +from .api import Account, Charger, Circuit, Installation, ZaptecApiError, ZaptecBase from .const import ( API_TIMEOUT, CONF_CHARGERS, @@ -55,9 +55,6 @@ Platform.UPDATE, ] -# FIXME: Informing users that the interface is considerable different -# FIXME: Setting that allows users to continue with old naming scheme? - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up integration.""" @@ -83,6 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setup all platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True @@ -102,27 +100,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reload config entry.""" - await async_unload_entry(hass, entry) - await async_setup_entry(hass, entry) + _LOGGER.debug("Reloading entry %s", entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) class ZaptecUpdateCoordinator(DataUpdateCoordinator[None]): account: Account + config_entry: ConfigEntry def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: """Initialize account-wide Zaptec data updater.""" _LOGGER.debug("Setting up coordinator") - self._config = entry.data - self.account = Account( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], client=async_get_clientsession(hass), ) - self._entry = entry scan_interval = entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) super().__init__( @@ -161,8 +157,8 @@ async def _async_update_data(self) -> None: # Get the list if chargers to include chargers = None - if self._config.get(CONF_MANUAL_SELECT, False): - chargers = self._config.get(CONF_CHARGERS) + if self.config_entry.data.get(CONF_MANUAL_SELECT, False): + chargers = self.config_entry.data.get(CONF_CHARGERS) # Selected chargers to add if chargers is not None: @@ -290,30 +286,36 @@ def _log_value(self, value, force=False): self._prev_value = value # Only logs when the value changes _LOGGER.debug( - " %s.%s = <%s> %s (in %s)", + " %s.%s = <%s> %s (in %s %s)", self.__class__.__qualname__, self.key, type(value).__qualname__, value, - self.zaptec_obj.id, + type(self.zaptec_obj).__qualname__, + self.zaptec_obj.name, ) @callback def _log_unavailable(self): """Helper to log when unavailable.""" _LOGGER.debug( - " %s.%s = UNAVAILABLE (in %s)", + " %s.%s = UNAVAILABLE (in %s %s)", self.__class__.__qualname__, self.key, - self.zaptec_obj.id, + type(self.zaptec_obj).__qualname__, + self.zaptec_obj.name, ) + @property + def key(self): + """Helper to retrieve the key from the entity description.""" + return self.entity_description.key + @classmethod - def create_from( + def create_from_descriptions( cls, descriptions: list[EntityDescription], coordinator: ZaptecUpdateCoordinator, - entry: ConfigEntry, zaptec_obj: ZaptecBase, device_info: DeviceInfo, ) -> list[ZaptecBaseEntity]: @@ -322,7 +324,7 @@ def create_from( """ # Calculate the prefix to use for the entity name - prefix = entry.data.get(CONF_NAME, "").rstrip() + prefix = coordinator.config_entry.data.get(CONF_NAME, "").rstrip() if prefix: prefix = prefix + " " @@ -334,24 +336,32 @@ def create_from( ) dev_info.update(device_info) - entities = [] + entities: list[ZaptecBaseEntity] = [] for description in descriptions: # Use provided class if it exists, otherwise use the class this # function was called from - klass = getattr(description, "cls", cls) or cls + klass: type[ZaptecBaseEntity] = getattr(description, "cls", cls) or cls + + # Create the entity object + _LOGGER.debug( + "Adding %s.%s (in %s %s)", + klass.__qualname__, + description.key, + type(zaptec_obj).__qualname__, + zaptec_obj.name, + ) entity = klass(coordinator, zaptec_obj, description, dev_info) entities.append(entity) + return entities @classmethod def create_from_zaptec( cls, - account: Account, coordinator: ZaptecUpdateCoordinator, - entry: ConfigEntry, - installation_entities: list[EntityDescription], - circuit_entities: list[EntityDescription], - charger_entities: list[EntityDescription], + installation_descriptions: list[EntityDescription], + circuit_descriptions: list[EntityDescription], + charger_descriptions: list[EntityDescription], ) -> list[ZaptecBaseEntity]: """Helper factory to populate the listed entities for the detected Zaptec devices. It sets the proper device info on the installation, @@ -359,67 +369,50 @@ def create_from_zaptec( """ entities = [] - for zap_install in account.installations: - if zap_install.id in account.map: + # Iterate over every zaptec object in the account mapping and add + # the listed entities for each object type + for obj in coordinator.account.map.values(): + if isinstance(obj, Installation): + info = DeviceInfo(model=f"{obj.name} Installation") + entities.extend( - cls.create_from( - installation_entities, + cls.create_from_descriptions( + installation_descriptions, coordinator, - entry, - zap_install, - DeviceInfo( - model=f"{zap_install.name} Installation", - ), + obj, + info, ) ) - for zap_circuit in zap_install.circuits: - if zap_circuit.id in account.map: - entities.extend( - cls.create_from( - circuit_entities, - coordinator, - entry, - zap_circuit, - DeviceInfo( - model=f"{zap_circuit.name} Circuit", - via_device=(DOMAIN, zap_install.id), - ), - ) + elif isinstance(obj, Circuit): + info = DeviceInfo(model=f"{obj.name} Circuit") + if obj.installation: + info["via_device"] = (DOMAIN, obj.installation.id) + + entities.extend( + cls.create_from_descriptions( + circuit_descriptions, + coordinator, + obj, + info, ) + ) + + elif isinstance(obj, Charger): + info = DeviceInfo(model=f"{obj.name} Charger") + if obj.circuit: + info["via_device"] = (DOMAIN, obj.circuit.id) - for zap_charger in zap_circuit.chargers: - if zap_charger.id in account.map: - entities.extend( - cls.create_from( - charger_entities, - coordinator, - entry, - zap_charger, - DeviceInfo( - model=f"{zap_charger.name} Charger", - via_device=(DOMAIN, zap_circuit.id), - ), - ) - ) - - for zap_charger in account.stand_alone_chargers: - if zap_charger.id in account.map: entities.extend( - cls.create_from( - charger_entities, + cls.create_from_descriptions( + charger_descriptions, coordinator, - entry, - zap_charger, - DeviceInfo( - model=f"{zap_charger.name} Charger", - ), + obj, + info, ) ) - return entities + else: + _LOGGER.error("Unknown zaptec object type: %s", type(obj).__qualname__) - @property - def key(self): - """Helper to retrieve the key from the entity description.""" - return self.entity_description.key + return entities diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py index ae97585..18df95b 100644 --- a/custom_components/zaptec/binary_sensor.py +++ b/custom_components/zaptec/binary_sensor.py @@ -115,12 +115,9 @@ async def async_setup_entry( _LOGGER.debug("Setup binary sensors") coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - acc = coordinator.account entities = ZaptecBinarySensor.create_from_zaptec( - acc, coordinator, - entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index 8f85ced..64168ee 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -94,12 +94,9 @@ async def async_setup_entry( _LOGGER.debug("Setup buttons") coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - acc = coordinator.account entities = ZaptecButton.create_from_zaptec( - acc, coordinator, - entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py index 47ae93d..c1fce36 100644 --- a/custom_components/zaptec/diagnostics.py +++ b/custom_components/zaptec/diagnostics.py @@ -146,11 +146,11 @@ async def async_get_device_diagnostics( api = out.setdefault("api", {}) # Helper to redact the output data - red = Redactor(DO_REDACT, acc._obs_ids) # FIXME: Access to private member + red = Redactor(DO_REDACT, acc._obs_ids) async def req(url): try: - return await acc._request(url) # FIXME: Access to private member + return await acc._request(url) except Exception as err: return {"failed": str(err)} diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index 46bc73f..0b1dd7b 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -149,12 +149,9 @@ async def async_setup_entry( _LOGGER.debug("Setup numbers") coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - acc = coordinator.account entities = ZaptecNumber.create_from_zaptec( - acc, coordinator, - entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index d75718a..30e0f76 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -184,12 +184,9 @@ async def async_setup_entry( _LOGGER.debug("Setup sensors") coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - acc: Account = coordinator.account entities = ZaptecSensor.create_from_zaptec( - acc, coordinator, - entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index 6b5b67b..73dcdf8 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -150,14 +150,11 @@ async def async_setup_entry( _LOGGER.debug("Setup switches") coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - acc = coordinator.account - switches = ZaptecSwitch.create_from_zaptec( - acc, + entities = ZaptecSwitch.create_from_zaptec( coordinator, - entry, INSTALLATION_SWITCH_TYPES, CIRCUIT_SWITCH_TYPES, CHARGER_SWITCH_TYPES, ) - async_add_entities(switches, True) + async_add_entities(entities, True) diff --git a/custom_components/zaptec/update.py b/custom_components/zaptec/update.py index 7c539f3..44467d8 100644 --- a/custom_components/zaptec/update.py +++ b/custom_components/zaptec/update.py @@ -83,12 +83,9 @@ async def async_setup_entry( _LOGGER.debug("Setup binary sensors") coordinator: ZaptecUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - acc = coordinator.account entities = ZaptecUpdate.create_from_zaptec( - acc, coordinator, - entry, INSTALLATION_ENTITIES, CIRCUIT_ENTITIES, CHARGER_ENTITIES, From 14573b77e6f5ca39fb82e777cb1e423bb45b4ea7 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 9 Oct 2023 17:32:55 +0000 Subject: [PATCH 12/17] Rename CONF_NAME to CONF_PREFIX --- custom_components/zaptec/__init__.py | 4 ++-- custom_components/zaptec/config_flow.py | 5 ++--- custom_components/zaptec/const.py | 1 + custom_components/zaptec/translations/en.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index fe39630..5ba1170 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -9,7 +9,6 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -32,6 +31,7 @@ API_TIMEOUT, CONF_CHARGERS, CONF_MANUAL_SELECT, + CONF_PREFIX, DEFAULT_SCAN_INTERVAL, DOMAIN, MANUFACTURER, @@ -324,7 +324,7 @@ def create_from_descriptions( """ # Calculate the prefix to use for the entity name - prefix = coordinator.config_entry.data.get(CONF_NAME, "").rstrip() + prefix = coordinator.config_entry.data.get(CONF_PREFIX, "").rstrip() if prefix: prefix = prefix + " " diff --git a/custom_components/zaptec/config_flow.py b/custom_components/zaptec/config_flow.py index f5f050c..09dd654 100644 --- a/custom_components/zaptec/config_flow.py +++ b/custom_components/zaptec/config_flow.py @@ -9,7 +9,6 @@ from homeassistant import config_entries from homeassistant.config_entries import data_entry_flow from homeassistant.const import ( - CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -25,7 +24,7 @@ RequestRetryError, RequestTimeoutError, ) -from .const import CONF_CHARGERS, CONF_MANUAL_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import CONF_CHARGERS, CONF_MANUAL_SELECT, CONF_PREFIX, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -80,7 +79,7 @@ async def async_step_user( data = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_NAME): str, + vol.Optional(CONF_PREFIX): str, vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.Coerce( int ), diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index e4c50fb..35a126e 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -40,6 +40,7 @@ CONF_MANUAL_SELECT = "manual_select" CONF_CHARGERS = "chargers" +CONF_PREFIX = "prefix" class Missing: diff --git a/custom_components/zaptec/translations/en.json b/custom_components/zaptec/translations/en.json index c3518e4..e0a6866 100644 --- a/custom_components/zaptec/translations/en.json +++ b/custom_components/zaptec/translations/en.json @@ -7,7 +7,7 @@ "data": { "username": "Username", "password": "Password", - "name": "Optional prefix", + "prefix": "Optional prefix", "scan_interval": "Scan interval", "manual_select": "Manually select chargers. Next screen will select the chargers to add." } From aebc427fdcfcb67c5071d9477e8db4505db5ba6f Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Wed, 11 Oct 2023 11:39:26 +0200 Subject: [PATCH 13/17] Fixes * Add TOTAL_INCREASING to totl_charger_power_session, sveinse/zaptec#23 * Fix regression of missing services, sveinse/zaptec#24 * Refactor services to make them easier to control from UI --- custom_components/zaptec/__init__.py | 6 + custom_components/zaptec/sensor.py | 3 +- custom_components/zaptec/services.py | 273 +++++++++++++++++++------ custom_components/zaptec/services.yaml | 139 +++++++++---- 4 files changed, 327 insertions(+), 94 deletions(-) diff --git a/custom_components/zaptec/__init__.py b/custom_components/zaptec/__init__.py index 5ba1170..59b00de 100644 --- a/custom_components/zaptec/__init__.py +++ b/custom_components/zaptec/__init__.py @@ -39,6 +39,7 @@ REQUEST_REFRESH_DELAY, STARTUP, ) +from .services import async_setup_services, async_unload_services _LOGGER = logging.getLogger(__name__) @@ -78,6 +79,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + # Setup services + await async_setup_services(hass) + # Setup all platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) @@ -95,6 +99,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.cancel_streams() + await async_unload_services(hass) + return unload_ok diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index 30e0f76..0c5a23c 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -159,6 +159,7 @@ class ZapSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, icon="mdi:counter", native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, ), ZapSensorEntityDescription( key="signed_meter_value_kwh", @@ -166,7 +167,7 @@ class ZapSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, icon="mdi:counter", native_unit_of_measurement=const.UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), ZapSensorEntityDescription( key="completed_session.Energy", diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index d801567..0c5f539 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -2,112 +2,271 @@ from __future__ import annotations import logging -from typing import Awaitable, Callable +from collections.abc import Generator +from typing import TYPE_CHECKING, Awaitable, Callable, TypeVar +import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry, entity_registry from .api import Account, Charger, Installation from .const import DOMAIN +if TYPE_CHECKING: + from . import ZaptecUpdateCoordinator + _LOGGER = logging.getLogger(__name__) TServiceHandler = Callable[[ServiceCall], Awaitable[None]] +T = TypeVar("T") -# SCHEMAS for services -# ==================== -has_id_schema = vol.Schema({vol.Required("charger_id"): str}) - -has_limit_current_schema = vol.Schema( - vol.SomeOf( - min_valid=1, - max_valid=1, - msg="Must specify either only available_current or all " - "three available_current_phaseX (where X is 1-3). They are mutually exclusive", - validators=[ +CHARGER_ID_SCHEMA = vol.Schema( + vol.All( + vol.Schema( { - vol.Required("installation_id"): str, - vol.Required("available_current"): int, + vol.Required( + vol.Any("charger_id", "device_id", "entity_id"), + msg=( + "At leas one of 'charger_id', 'device_id' or " + "'entity_id' must be specified" + ), + ): object, }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Optional("charger_id"): str, + vol.Optional("device_id"): vol.All(cv.ensure_list, [str]), + vol.Optional("entity_id"): vol.All(cv.ensure_list, [str]), + } + ), + ) +) + +LIMIT_CURRENT_SCHEMA = vol.Schema( + vol.All( + vol.Schema( { - vol.Required("installation_id"): str, - vol.Required("available_current_phase1"): int, - vol.Required("available_current_phase2"): int, - vol.Required("available_current_phase3"): int, + vol.Required( + vol.Any("installation_id", "device_id", "entity_id"), + msg=( + "At least one of 'installation_id', 'device_id' or " + "'entity_id' must be specified" + ), + ): object, }, - ], + extra=vol.ALLOW_EXTRA, + ), + vol.Any( + vol.Schema( + { + vol.Optional("installation_id"): str, + vol.Optional("device_id"): vol.All(cv.ensure_list, [str]), + vol.Optional("entity_id"): vol.All(cv.ensure_list, [str]), + vol.Required("available_current"): int, + }, + ), + vol.Schema( + { + vol.Optional("installation_id"): str, + vol.Optional("device_id"): vol.All(cv.ensure_list, [str]), + vol.Optional("entity_id"): vol.All(cv.ensure_list, [str]), + vol.Required("available_current_phase1"): int, + vol.Required("available_current_phase2"): int, + vol.Required("available_current_phase3"): int, + }, + ), + msg=( + "Missing either 'available_current' or all three of " + "'available_current_phase1' " + "'available_current_phase2' and 'available_current_phase3'." + ), + ), ) ) async def async_setup_services(hass: HomeAssistant) -> None: - """Set up services for the Plex component.""" - + """Set up services for zaptec.""" _LOGGER.debug("Set up services") - acc: Account = hass.data[DOMAIN]["api"] + + def get_as_set(service_call: ServiceCall, key: str) -> set[str]: + data = service_call.data.get(key, []) + if not isinstance(data, list): + data = [data] + return set(data) + + def iter_objects( + service_call: ServiceCall, mustbe: type[T] + ) -> Generator[tuple[Account, T], None, None]: + ent_reg = entity_registry.async_get(hass) + dev_reg = device_registry.async_get(hass) + + device_ids = get_as_set(service_call, "device_id") + lookup: dict[str, str] = {} + + # Parse all entities and find their device ids which is appended to the + # list of devices. + for entity_id in get_as_set(service_call, "entity_id"): + entity_entry = ent_reg.async_get(entity_id) + if entity_entry is None: + raise HomeAssistantError(f"Unable to find entity '{entity_id}'") + if not entity_entry.device_id: + raise HomeAssistantError(f"Entity '{entity_id}' doesn't have a device") + device_ids.add(entity_entry.device_id) + lookup[entity_entry.device_id] = f"entity '{entity_id}'" + + # Parse all device ids and find the uid for each device + uids: set[str] = set() + for device_id in device_ids: + device_entry = dev_reg.async_get(device_id) + err_device = lookup.get(device_id, f"device '{device_id}'") + if device_entry is None: + raise HomeAssistantError(f"Unable to find device {err_device}") + err_device = lookup.get(device_id, f"device {device_entry.name}") + if not device_entry.identifiers: + raise HomeAssistantError(f"Unable to find identifiers for {err_device}") + for domain, uid in device_entry.identifiers: + if domain != DOMAIN: + raise HomeAssistantError( + f"Non-zaptec device specified {err_device}" + ) + uids.add(uid) + lookup[uid] = err_device + + # Append any legacy charger_id or installation_id that might be specified + field = None + if mustbe is Charger: + field = "charger_id" + elif mustbe is Installation: + field = "installation_id" + if field: + uids.update(get_as_set(service_call, field)) + + # Any uid specified at all? + if not uids: + suffix = f". Missing field '{field}'" if field else "" + raise HomeAssistantError(f"No zaptec devices specified{suffix}") + + # Loop through every uid and find the object + for uid in uids: + # Set the human readable identifier for the error message + if uid in lookup: + err_device = f"{lookup[uid]} ({uid})" + else: + err_device = f"id {uid}" + + # Get all account and objects that matches the uid from all coordinators/account objects + matches = set( + (coord.account, obj) + for coord in hass.data[DOMAIN].values() + for obj in coord.account.map.values() + if obj.id == uid + ) + # Filter out the objects that doesn't match the expected type + want: set[tuple[Account, T]] = set( + (a, o) for a, o in matches if isinstance(o, mustbe) + ) + + def cap_first(s): + return s[0].upper() + s[1:] + + if not matches: + raise HomeAssistantError( + f"Unable to find zaptec object for {err_device}" + ) + if want != matches: + raise HomeAssistantError( + f"{cap_first(err_device)} is not a {mustbe.__name__}" + ) + if len(want) > 1: + _LOGGER.warning( + "Unexpected multiple matches for '%s' (%s): %s", + err_device, + uid, + [o.id for _, o in want], + ) + + # Send to caller + yield from want async def service_handle_stop_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called stop charging") - charger_id = service_call.data["charger_id"] - charger: Charger = acc.map[charger_id] - await charger.stop_charging_final() + for acc, obj in iter_objects(service_call, Charger): + _LOGGER.debug(" >> to %s", obj.id) + await obj.stop_charging_final() async def service_handle_resume_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called resume charging") - charger_id = service_call.data["charger_id"] - charger: Charger = acc.map[charger_id] - await charger.resume_charging() + for acc, obj in iter_objects(service_call, mustbe=Charger): + _LOGGER.debug(" >> to %s", obj.id) + await obj.resume_charging() async def service_handle_authorize_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called authorize charging") - charger_id = service_call.data["charger_id"] - charger: Charger = acc.map[charger_id] - await charger.authorize_charge() + for acc, obj in iter_objects(service_call, mustbe=Charger): + _LOGGER.debug(" >> to %s", obj.id) + await obj.authorize_charge() async def service_handle_deauthorize_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called deauthorize charging and stop") - charger_id = service_call.data["charger_id"] - charger: Charger = acc.map[charger_id] - await charger.deauthorize_and_stop() + for acc, obj in iter_objects(service_call, mustbe=Charger): + _LOGGER.debug(" >> to %s", obj.id) + await obj.deauthorize_and_stop() async def service_handle_restart_charger(service_call: ServiceCall) -> None: _LOGGER.debug("Called restart charger") - charger_id = service_call.data["charger_id"] - charger: Charger = acc.map[charger_id] - await charger.restart_charger() + for acc, obj in iter_objects(service_call, mustbe=Charger): + _LOGGER.debug(" >> to %s", obj.id) + await obj.restart_charger() - async def service_handle_update_firmware(service_call: ServiceCall) -> None: + async def service_handle_upgrade_firmware(service_call: ServiceCall) -> None: _LOGGER.debug("Called update firmware") - charger_id = service_call.data["charger_id"] - charger: Charger = acc.map[charger_id] - await charger.upgrade_firmware() + for acc, obj in iter_objects(service_call, mustbe=Charger): + _LOGGER.debug(" >> to %s", obj.id) + await obj.upgrade_firmware() async def service_handle_limit_current(service_call: ServiceCall) -> None: _LOGGER.debug("Called set current limit") - installation_id = service_call.data["installation_id"] available_current = service_call.data.get("available_current") available_current_phase1 = service_call.data.get("available_current_phase1") available_current_phase2 = service_call.data.get("available_current_phase2") available_current_phase3 = service_call.data.get("available_current_phase3") - installation: Installation = acc.map[installation_id] - await installation.set_limit_current( - availableCurrent=available_current, - availableCurrentPhase1=available_current_phase1, - availableCurrentPhase2=available_current_phase2, - availableCurrentPhase3=available_current_phase3, - ) + for acc, obj in iter_objects(service_call, mustbe=Installation): + _LOGGER.debug(" >> to %s", obj.id) + await obj.set_limit_current( + availableCurrent=available_current, + availableCurrentPhase1=available_current_phase1, + availableCurrentPhase2=available_current_phase2, + availableCurrentPhase3=available_current_phase3, + ) # LIST OF SERVICES services: list[tuple[str, vol.Schema, TServiceHandler]] = [ - ("stop_charging", has_id_schema, service_handle_stop_charging), - ("resume_charging", has_id_schema, service_handle_resume_charging), - ("authorize_charging", has_id_schema, service_handle_authorize_charging), - ("deauthorize_charging", has_id_schema, service_handle_deauthorize_charging), - ("restart_charger", has_id_schema, service_handle_restart_charger), - ("update_firmware", has_id_schema, service_handle_update_firmware), - ("limit_current", has_limit_current_schema, service_handle_limit_current), + ("stop_charging", CHARGER_ID_SCHEMA, service_handle_stop_charging), + ("resume_charging", CHARGER_ID_SCHEMA, service_handle_resume_charging), + ("authorize_charging", CHARGER_ID_SCHEMA, service_handle_authorize_charging), + ( + "deauthorize_charging", + CHARGER_ID_SCHEMA, + service_handle_deauthorize_charging, + ), + ("restart_charger", CHARGER_ID_SCHEMA, service_handle_restart_charger), + ("upgrade_firmware", CHARGER_ID_SCHEMA, service_handle_upgrade_firmware), + ("limit_current", LIMIT_CURRENT_SCHEMA, service_handle_limit_current), ] # Register the services for name, schema, handler in services: - hass.services.async_register(DOMAIN, name, handler, schema=schema) + if not hass.services.has_service(DOMAIN, name): + hass.services.async_register(DOMAIN, name, handler, schema=schema) + + +async def async_unload_services(hass: HomeAssistant) -> None: + """Unload zaptec services.""" + _LOGGER.debug("Unload services") + for service in hass.services.async_services().get(DOMAIN, {}): + hass.services.async_remove(DOMAIN, service) diff --git a/custom_components/zaptec/services.yaml b/custom_components/zaptec/services.yaml index afc9b51..a3a8f6b 100644 --- a/custom_components/zaptec/services.yaml +++ b/custom_components/zaptec/services.yaml @@ -1,67 +1,134 @@ stop_charging: - description: "Stop/pause charging" + name: Stop charging + description: >- + Stop or pauses the active charging. Select either charger device(s) + or specify the charger_id directly fields: + device_id: + description: Select charger device + selector: + device: + integration: zaptec charger_id: - required: true - description: "The charger id" - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Charger identifier + example: 00000000-1111-2222-3333-444444444444 resume_charging: - description: "Resume/start charging" + name: Resume charging + description: >- + Resume or start charging. Select either charger device(s) or + specify the charger_id directly fields: + device_id: + description: Select charger device + selector: + device: + integration: zaptec charger_id: - required: true - description: "The charger id" - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Charger identifier + example: 00000000-1111-2222-3333-444444444444 authorize_charging: - description: "Authorize charging" + name: Authorize charging + description: >- + Authorize the charge session and resume charging. Select charger + device(s) or specify the charger_id directly. fields: + device_id: + description: Select charger device + selector: + device: + integration: zaptec charger_id: - required: true - description: "The charger id" - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Charger identifier + example: 00000000-1111-2222-3333-444444444444 deauthorize_charging: - description: "Deauthorize charging and stop" + name: Deauthorize charging + description: >- + Deauthorize the session and stop charging. Select charger device(s) + or specify the charger_id directly. fields: + device_id: + description: Select charger device + selector: + device: + integration: zaptec charger_id: - required: true - description: "The charger id" - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Charger identifier + example: 00000000-1111-2222-3333-444444444444 restart_charger: - description: "Restart charger." + name: Restart charger + description: >- + Restart the charger. Select charger device(s) or specify the + charger_id directly. fields: + device_id: + description: Select charger device + selector: + device: + integration: zaptec charger_id: - required: true - description: "The charger id" - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Charger identifier + example: 00000000-1111-2222-3333-444444444444 update_firmware: - description: "New resume command." + name: Update firmware + description: >- + Send update firmware request to the charger. Select charger + device(s) or specify the charger_id directly. fields: + device_id: + description: Select charger device + selector: + device: + integration: zaptec charger_id: - required: true - description: "Update the firmware." - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Charger identifier + example: 00000000-1111-2222-3333-444444444444 limit_current: - description: "Update available current for the installation." + name: Set current limit + description: >- + Set the available current for the installation. Select installation + device(s) or specify the installation_id directly. The available + current can either be using the available_current or the + available_current_phasex fields. All three phases must be present. fields: + device_id: + description: Select installation device + selector: + device: + integration: zaptec installation_id: - required: true - description: "The installation id" - example: "yyyyyyyy-9999-99xx-xxxx-yyyyyyyyyyyy" + description: Installer identifier + example: 00000000-1111-2222-3333-444444444444 available_current: - description: "The available current for all phases. Cannot be used together with any available_current_phase* fields" - example: "16" + description: The available current for all phases. Cannot be used together with any available_current_phase* fields + example: 16 + selector: + number: + min: 0 + max: 32 available_current_phase1: - description: "The available current for phase 1" - example: "16" + description: The available current for phase 1 + example: 16 + selector: + number: + min: 0 + max: 32 available_current_phase2: - description: "The available current for phase 2" - example: "16" + description: The available current for phase 2 + example: 16 + selector: + number: + min: 0 + max: 32 available_current_phase3: - description: "The available current for phase 3" - example: "16" + description: The available current for phase 3 + example: 16 + selector: + number: + min: 0 + max: 32 From 842b825d739e2deb1b85c57726767c992df5a657 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 16 Oct 2023 00:50:33 +0200 Subject: [PATCH 14/17] Fixes API retry robustness and set_limit_current * API: Make requests more robust by introducing a retry mechanism, sveinse/zaptec#19 * API: Fix set_limit_current bug, sveinse/zaptec#27 * Fix coordinator update when calling services --- custom_components/zaptec/api.py | 357 ++++++++++++++---------- custom_components/zaptec/config_flow.py | 1 + custom_components/zaptec/const.py | 3 +- custom_components/zaptec/services.py | 31 +- 4 files changed, 232 insertions(+), 160 deletions(-) diff --git a/custom_components/zaptec/api.py b/custom_components/zaptec/api.py index 390ede3..4991678 100644 --- a/custom_components/zaptec/api.py +++ b/custom_components/zaptec/api.py @@ -7,18 +7,38 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from concurrent.futures import CancelledError -from functools import partial -from typing import Any, Callable +from typing import Any, AsyncGenerator, Callable, Protocol import aiohttp import pydantic +from .const import API_RETRIES, API_TIMEOUT, API_URL, MISSING, TOKEN_URL, TRUTHY +from .misc import mc_nbfx_decoder, to_under +from .validate import validate + +""" +stuff are missing from the api docs compared to what the portal uses. +circuits/{self.id}/live +circuits/{self.id}/ +https://api.zaptec.com/api/dashboard/activechargersforowner?limit=250 +/dashbord +signalr is used by the website. +""" + # pylint: disable=missing-function-docstring # Type definitions TValue = str | int | float | bool TDict = dict[str, TValue] + +class TLogExc(Protocol): + """Protocol for logging exceptions""" + + def __call__(self, exc: Exception) -> Exception: + ... + + _LOGGER = logging.getLogger(__name__) # Set to True to debug log all API calls @@ -27,30 +47,6 @@ # Set to True to debug log all API errors DEBUG_API_ERRORS = True -""" -stuff are missing from the api docs compared to what the portal uses. -circuits/{self.id}/live -circuits/{self.id}/ -https://api.zaptec.com/api/dashboard/activechargersforowner?limit=250 -/dashbord -signalr is used by the website. -""" - -# to Support running this as a script. -if __name__ == "__main__": - from const import API_RETRIES, API_TIMEOUT, API_URL, MISSING, TOKEN_URL, TRUTHY - from misc import mc_nbfx_decoder, to_under - from validate import validate - - # remove me later - logging.basicConfig(level=logging.DEBUG) - logging.getLogger("azure").setLevel(logging.WARNING) - -else: - from .const import API_RETRIES, API_TIMEOUT, API_URL, MISSING, TOKEN_URL, TRUTHY - from .misc import mc_nbfx_decoder, to_under - from .validate import validate - class ZaptecApiError(Exception): """Base exception for all Zaptec API errors""" @@ -396,9 +392,9 @@ async def set_limit_current(self, **kwargs): Use availableCurrent for setting all phases at once. Use availableCurrentPhase* to set each phase individually. """ - has_availablecurrent = "availableCurrent" in kwargs + has_availablecurrent = kwargs.get("availableCurrent") is not None has_availablecurrentphases = all( - k in kwargs + kwargs.get(k) is not None for k in ( "availableCurrentPhase1", "availableCurrentPhase2", @@ -715,7 +711,46 @@ def unregister(self, id: str): # API METHODS @staticmethod - async def check_login(username: str, password: str) -> bool: + def _request_log(url, method, iteration, **kwargs): + """Helper that yields request log entries.""" + try: + data = kwargs.get("data", "") + jdata = kwargs.get("json", "") + dlength = f" data length {len(data)}" if "data" in kwargs else "" + jlength = f" json length {len(jdata)}" if "json" in kwargs else "" + attempt = f" (attempt {iteration})" if iteration > 1 else "" + yield f"@@@ REQUEST {method.upper()} to '{url}'{dlength}{jlength}{attempt}" + if "headers" in kwargs: + yield f" headers {dict((k, v) for k, v in kwargs['headers'].items())}" + if "data" in kwargs: + yield f" data '{kwargs['data']}'" + if "json" in kwargs: + yield f" json '{kwargs['json']}'" + except Exception: + _LOGGER.exception("Failed to log request (ignored exception)") + + @staticmethod + async def _response_log(resp: aiohttp.ClientResponse): + """Helper that yield response log entries.""" + try: + contents = await resp.read() + yield f"@@@ RESPONSE {resp.status} length {len(contents)}" + yield f" headers {dict((k, v) for k, v in resp.headers.items())}" + if not contents: + return + if resp.status != 200: + yield f" data '{await resp.text()}'" + else: + yield f" json '{await resp.json(content_type=None)}'" + except Exception: + _LOGGER.exception("Failed to log response (ignored exception)") + + @staticmethod + async def check_login( + username: str, password: str, client: aiohttp.ClientSession | None = None + ) -> bool: + """Check if the login is valid.""" + client = client or aiohttp.ClientSession() p = { "username": username, "password": password, @@ -723,9 +758,7 @@ async def check_login(username: str, password: str) -> bool: } try: timeout = aiohttp.ClientTimeout(total=API_TIMEOUT) - async with aiohttp.request( - "POST", TOKEN_URL, data=p, timeout=timeout - ) as resp: + async with client.post(TOKEN_URL, data=p, timeout=timeout) as resp: if resp.status == 200: data = await resp.json() return True @@ -740,6 +773,76 @@ async def check_login(username: str, password: str) -> bool: _LOGGER.error("Authentication request failed: %s", err) raise RequestConnectionError("Authentication request failed") from err + async def _retry_request( + self, url: str, method="get", **kwargs + ) -> AsyncGenerator[tuple[aiohttp.ClientResponse, TLogExc], None]: + """API request generator that handles retries. This function handles + logging and error handling. The generator will yield responses. If the + request needs to be retried, the caller must call __next__.""" + + error: Exception | None = None + iteration = 0 + for iteration in range(1, API_RETRIES + 1): + try: + # Log the request + log_req = list(self._request_log(url, method, iteration, **kwargs)) + if DEBUG_API_CALLS: + for msg in log_req: + _LOGGER.debug(msg) + + # Make the request + async with self._client.request( + method=method, url=url, **kwargs + ) as resp: + # Log the response + log_resp = [m async for m in self._response_log(resp)] + if DEBUG_API_CALLS: + for msg in log_resp: + _LOGGER.debug(msg) + + # Prepare the exception handler + def log_exc(exc: Exception) -> Exception: + """Log the exception and return it.""" + if DEBUG_API_ERRORS: + if not DEBUG_API_CALLS: + for msg in log_req + log_resp: + _LOGGER.debug(msg) + _LOGGER.error(exc) + return exc + + # Let the caller handle the response. If the caller + # calls __next__ on the generator the request will be + # retried. + yield resp, log_exc + + # Exceptions that can be retried + except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as err: + error = err # Capture tha last error + if DEBUG_API_ERRORS: + _LOGGER.error( + "Request to %s failed (attempt %s): %s: %s", + url, + iteration, + type(err).__qualname__, + err, + ) + + # Arriving after retrying too many times. + + if isinstance(error, asyncio.TimeoutError): + raise RequestTimeoutError( + f"Request to {url} timed out after {iteration} retries" + ) from error + + if isinstance(error, aiohttp.ClientConnectionError): + raise RequestConnectionError( + f"Request to {url} failed after {iteration} retries: {error}" + ) from error + + raise RequestRetryError( + f"Request to {url} failed after {iteration} retries" + ) from error + async def _refresh_token(self): # So for some reason they used grant_type password.. # what the point with oauth then? Anyway this is valid for 24 hour @@ -748,131 +851,90 @@ async def _refresh_token(self): "password": self._password, "grant_type": "password", } - async with self._client.post(TOKEN_URL, data=p, timeout=self._timeout) as resp: + if DEBUG_API_CALLS: + _LOGGER.debug("@@@ REFRESH TOKEN") + + async for resp, log_exc in self._retry_request( + TOKEN_URL, method="post", data=p, timeout=self._timeout + ): if resp.status == 200: data = await resp.json() # The data includes the time the access token expires # atm we just ignore it and refresh token when needed. self._token_info.update(data) self._access_token = data.get("access_token") - else: - _LOGGER.error("Failed to authenticate. Got code %s", resp.status) - raise AuthenticationError( - "Failed to refresh token, check your credentials." - ) + if DEBUG_API_CALLS: + _LOGGER.debug(" TOKEN OK") + return - async def _request(self, url: str, method="get", data=None, iteration=1): - def log_request(): - """Log the request""" - try: - _LOGGER.debug( - f"@@@ REQUEST {method.upper()} to '{full_url}' length {len(data or '')}" + elif resp.status == 400: + data = await resp.json() + raise log_exc( + AuthenticationError( + f"Failed to authenticate. {data.get('error_description', '')}" + ) ) - if data: - _LOGGER.debug(f" content {data}") - except Exception as err: - _LOGGER.exception("Failed to log response") - async def log_response(resp: aiohttp.ClientResponse): - """Log the response""" - try: - contents = await resp.read() - _LOGGER.debug(f"@@@ RESPONSE {resp.status} length {len(contents)}") - _LOGGER.debug( - f" header {dict((k, v) for k, v in resp.headers.items())}" + raise log_exc( + RequestError( + f"POST request to {TOKEN_URL} failed with status {resp.status}: {resp}", + resp.status, ) - if not contents: - return - if resp.status != 200: - _LOGGER.debug(f" content {await resp.text()}") - else: - _LOGGER.debug(f" json '{await resp.json(content_type=None)}'") - except Exception as err: - _LOGGER.exception("Failed to log response") - - async def log_error(resp: aiohttp.ClientResponse, msg, *args): - """Log the error""" - if not DEBUG_API_ERRORS: - return - _LOGGER.error(msg, *args) - if not DEBUG_API_CALLS: - log_request() - await log_response(resp) - - header = { - "Authorization": f"Bearer {self._access_token}", - "Accept": "application/json", - } - full_url = API_URL + url - - if DEBUG_API_CALLS: - log_request() - - request_fn = getattr(self._client, method) - if request_fn is None: - raise ValueError(f"Unknown method {method}") - if data is not None and method in ("post", "put"): - request_fn = partial(request_fn, json=data) - - try: - resp: aiohttp.ClientResponse - async with request_fn( - full_url, headers=header, timeout=self._timeout - ) as resp: - if DEBUG_API_CALLS: - await log_response(resp) - - if resp.status == 401: # Unauthorized - await self._refresh_token() - if iteration > API_RETRIES: - await log_error(resp, "Failed reauthentication too many times") - raise RequestRetryError( - f"Request to {full_url} failed after {iteration} retries" - ) - return await self._request(url, iteration=iteration + 1) - - elif resp.status == 204: # No content - content = await resp.read() - return content - - elif resp.status == 200: # OK - # Read the JSON payload - try: - json_result = await resp.json(content_type=None) - except json.JSONDecodeError as err: - await log_error(resp, "Failed to decode json: %s", err) - raise RequestDataError( - f"Failed to decode json: {err}", resp.status - ) from err - - # Validate the incoming json data - try: - validate(json_result, url=url) - except pydantic.ValidationError as err: - await log_error(resp, "Failed to validate data: %s", err) - raise RequestDataError( - f"Failed to validate data: {err}", resp.status - ) from err - - return json_result + ) - else: - await log_error(resp, "Failing request to %s", full_url) - raise RequestError( - f"{method} request to {full_url} failed with status {resp.status}: {resp}", - resp.status, - ) + async def _request(self, url: str, method="get", data=None): + """Make a request to the API.""" - except asyncio.TimeoutError as err: - if DEBUG_API_ERRORS: - _LOGGER.error(f"Request to %s timed out", full_url) - raise RequestTimeoutError(f"Request to {full_url} timed out") from err - except aiohttp.ClientConnectionError as err: - if DEBUG_API_ERRORS: - _LOGGER.error("Request to %s failed: %s", full_url, err) - raise RequestConnectionError( - f"Request to {full_url} failed: {err}" - ) from err + full_url = API_URL + url + kwargs = { + "timeout": self._timeout, + "headers": { + "Authorization": f"Bearer {self._access_token}", + "Accept": "application/json", + }, + } + if data is not None: + kwargs["json"] = data + + async for resp, log_exc in self._retry_request( + full_url, method=method, **kwargs + ): + # The log_exc callback is a helper that will log the failing request + + if resp.status == 401: # Unauthorized + await self._refresh_token() + kwargs["headers"]["Authorization"] = f"Bearer {self._access_token}" + continue # Retry request + + elif resp.status == 204: # No content + content = await resp.read() + return content + + elif resp.status == 200: # OK + # Read the JSON payload + try: + json_result = await resp.json(content_type=None) + except json.JSONDecodeError as err: + raise log_exc( + RequestDataError(f"Failed to decode json: {err}"), + ) from err + + # Validate the incoming json data + try: + validate(json_result, url=url) + except pydantic.ValidationError as err: + raise log_exc( + RequestDataError(f"Failed to validate data: {err}"), + ) from err + + return json_result + + raise log_exc( + RequestError( + f"{method.upper()} request to {full_url} failed with status {resp.status}: {resp}", + resp.status, + ) + ) # API METHODS DONE # ======================================================================= @@ -1015,10 +1077,13 @@ def _state_to_attrs( if __name__ == "__main__": - # Just to execute the script manually. + # Just to execute the script manually with "python -m custom_components.zaptec.api" import os from pprint import pprint + logging.basicConfig(level=logging.DEBUG) + logging.getLogger("azure").setLevel(logging.WARNING) + async def gogo(): username = os.environ.get("zaptec_username") password = os.environ.get("zaptec_password") diff --git a/custom_components/zaptec/config_flow.py b/custom_components/zaptec/config_flow.py index 09dd654..b83992f 100644 --- a/custom_components/zaptec/config_flow.py +++ b/custom_components/zaptec/config_flow.py @@ -54,6 +54,7 @@ async def async_step_user( valid_login = await Account.check_login( username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], + client=async_get_clientsession(self.hass), ) except (RequestConnectionError, RequestTimeoutError): errors["base"] = "cannot_connect" diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index 35a126e..33fb9dc 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -31,11 +31,10 @@ CONST_URL = "https://api.zaptec.com/api/constants" API_RETRIES = 5 +API_TIMEOUT = 10 DEFAULT_SCAN_INTERVAL = 60 -API_TIMEOUT = 60 - REQUEST_REFRESH_DELAY = 0.3 CONF_MANUAL_SELECT = "manual_select" diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index 0c5f539..faa21ba 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity_registry -from .api import Account, Charger, Installation +from .api import Charger, Installation from .const import DOMAIN if TYPE_CHECKING: @@ -101,7 +101,7 @@ def get_as_set(service_call: ServiceCall, key: str) -> set[str]: def iter_objects( service_call: ServiceCall, mustbe: type[T] - ) -> Generator[tuple[Account, T], None, None]: + ) -> Generator[tuple[ZaptecUpdateCoordinator, T], None, None]: ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) @@ -159,15 +159,15 @@ def iter_objects( else: err_device = f"id {uid}" - # Get all account and objects that matches the uid from all coordinators/account objects + # Get all coordinators and objects that matches the uid from all coordinator objects matches = set( - (coord.account, obj) + (coord, obj) for coord in hass.data[DOMAIN].values() for obj in coord.account.map.values() if obj.id == uid ) # Filter out the objects that doesn't match the expected type - want: set[tuple[Account, T]] = set( + want: set[tuple[ZaptecUpdateCoordinator, T]] = set( (a, o) for a, o in matches if isinstance(o, mustbe) ) @@ -195,39 +195,45 @@ def cap_first(s): async def service_handle_stop_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called stop charging") - for acc, obj in iter_objects(service_call, Charger): + for coordinator, obj in iter_objects(service_call, Charger): _LOGGER.debug(" >> to %s", obj.id) await obj.stop_charging_final() + await coordinator.async_request_refresh() async def service_handle_resume_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called resume charging") - for acc, obj in iter_objects(service_call, mustbe=Charger): + for coordinator, obj in iter_objects(service_call, mustbe=Charger): _LOGGER.debug(" >> to %s", obj.id) await obj.resume_charging() + await coordinator.async_request_refresh() async def service_handle_authorize_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called authorize charging") - for acc, obj in iter_objects(service_call, mustbe=Charger): + for coordinator, obj in iter_objects(service_call, mustbe=Charger): _LOGGER.debug(" >> to %s", obj.id) await obj.authorize_charge() + await coordinator.async_request_refresh() async def service_handle_deauthorize_charging(service_call: ServiceCall) -> None: _LOGGER.debug("Called deauthorize charging and stop") - for acc, obj in iter_objects(service_call, mustbe=Charger): + for coordinator, obj in iter_objects(service_call, mustbe=Charger): _LOGGER.debug(" >> to %s", obj.id) await obj.deauthorize_and_stop() + await coordinator.async_request_refresh() async def service_handle_restart_charger(service_call: ServiceCall) -> None: _LOGGER.debug("Called restart charger") - for acc, obj in iter_objects(service_call, mustbe=Charger): + for coordinator, obj in iter_objects(service_call, mustbe=Charger): _LOGGER.debug(" >> to %s", obj.id) await obj.restart_charger() + await coordinator.async_request_refresh() async def service_handle_upgrade_firmware(service_call: ServiceCall) -> None: _LOGGER.debug("Called update firmware") - for acc, obj in iter_objects(service_call, mustbe=Charger): + for coordinator, obj in iter_objects(service_call, mustbe=Charger): _LOGGER.debug(" >> to %s", obj.id) await obj.upgrade_firmware() + await coordinator.async_request_refresh() async def service_handle_limit_current(service_call: ServiceCall) -> None: _LOGGER.debug("Called set current limit") @@ -235,7 +241,7 @@ async def service_handle_limit_current(service_call: ServiceCall) -> None: available_current_phase1 = service_call.data.get("available_current_phase1") available_current_phase2 = service_call.data.get("available_current_phase2") available_current_phase3 = service_call.data.get("available_current_phase3") - for acc, obj in iter_objects(service_call, mustbe=Installation): + for coordinator, obj in iter_objects(service_call, mustbe=Installation): _LOGGER.debug(" >> to %s", obj.id) await obj.set_limit_current( availableCurrent=available_current, @@ -243,6 +249,7 @@ async def service_handle_limit_current(service_call: ServiceCall) -> None: availableCurrentPhase2=available_current_phase2, availableCurrentPhase3=available_current_phase3, ) + await coordinator.async_request_refresh() # LIST OF SERVICES services: list[tuple[str, vol.Schema, TServiceHandler]] = [ From b02efeaf828f0345073646315ad6069dcb1c103b Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Mon, 6 Nov 2023 21:59:32 +0100 Subject: [PATCH 15/17] Adding cloud flag, sveinse/zaptec#30 --- custom_components/zaptec/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/zaptec/manifest.json b/custom_components/zaptec/manifest.json index 9aae47c..c0aeaa6 100644 --- a/custom_components/zaptec/manifest.json +++ b/custom_components/zaptec/manifest.json @@ -8,6 +8,7 @@ "hellowlol", "sveinse" ], + "iot_class": "cloud_polling", "requirements": ["azure-servicebus", "pydantic"], "version": "0.0.6b2" } From 7b0087f2d61f3d25aafb22bd465d5a8469f68b31 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Sat, 11 Nov 2023 14:54:04 +0100 Subject: [PATCH 16/17] Update documentation and manifest --- INFO.md | 37 - README.md | 231 ++- custom_components/zaptec/manifest.json | 2 +- hacs.json | 4 + lovelace_example/README.md | 393 ----- lovelace_example/car_charger_advanced.PNG | Bin 119447 -> 0 bytes lovelace_example/car_charger_attributes_.png | Bin 32746 -> 0 bytes lovelace_example/car_charger_sensor_.png | Bin 11669 -> 0 bytes lovelace_example/car_charger_simple.PNG | Bin 29469 -> 0 bytes lovelace_example/zaptec-home.png | Bin 1419510 -> 0 bytes lovelace_example/zaptec_home_api_data.txt | 1347 ------------------ lovelace_example/zh-1.png | Bin 40660 -> 0 bytes lovelace_example/zh-2.png | Bin 40660 -> 0 bytes lovelace_example/zh-3.png | Bin 40660 -> 0 bytes lovelace_example/zh-5.png | Bin 40660 -> 0 bytes 15 files changed, 211 insertions(+), 1803 deletions(-) delete mode 100644 INFO.md create mode 100644 hacs.json delete mode 100644 lovelace_example/README.md delete mode 100644 lovelace_example/car_charger_advanced.PNG delete mode 100644 lovelace_example/car_charger_attributes_.png delete mode 100644 lovelace_example/car_charger_sensor_.png delete mode 100644 lovelace_example/car_charger_simple.PNG delete mode 100644 lovelace_example/zaptec-home.png delete mode 100644 lovelace_example/zaptec_home_api_data.txt delete mode 100644 lovelace_example/zh-1.png delete mode 100644 lovelace_example/zh-2.png delete mode 100644 lovelace_example/zh-3.png delete mode 100644 lovelace_example/zh-5.png diff --git a/INFO.md b/INFO.md deleted file mode 100644 index 971c75f..0000000 --- a/INFO.md +++ /dev/null @@ -1,37 +0,0 @@ -## zaptec charger custom component for home assistant - -[![GitHub Release][releases-shield]][releases] -[![GitHub Activity][commits-shield]][commits] -[![License][license-shield]][license] - -[![hacs][hacsbadge]][hacs] -[![Project Maintenance][maintenance-shield]][user_profile] -[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] - -[![Discord][discord-shield]][discord] -[![Community Forum][forum-shield]][forum] - -### Usage -Use hacs to install the package, add the config example for more usage see the `lovelace_example` - -Setup the integration using the integrations page. - - -[zaptec]: https://github.com/custom-components/zaptec -[buymecoffee]: https://www.buymeacoffee.com/hellowlol1 -[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge -[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-components/zaptec.svg?style=for-the-badge -[commits]: https://github.com/custom-components/zaptec/commits/master -[hacs]: https://hacs.xyz -[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge -[discord]: https://discord.gg/Qa5fW2R -[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge -[exampleimg]: example.png -[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge -[forum]: https://community.home-assistant.io/ -[license]: https://github.com/custom-components/zaptec/blob/main/LICENSE -[license-shield]: https://img.shields.io/github/license/custom-components/zaptec.svg?style=for-the-badge -[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge -[releases-shield]: https://img.shields.io/github/release/custom-components/integration_blueprint.svg?style=for-the-badge -[releases]: https://github.com/custom-components/zaptec/releases -[user_profile]: https://github.com/hellowlol \ No newline at end of file diff --git a/README.md b/README.md index 0daf9e6..bf066f5 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,218 @@ -## zaptec charger custom component for home assistant +## Zaptec EV charger component for Home Assistant +[![hacs][hacsbadge]][hacs] [![GitHub Release][releases-shield]][releases] -[![GitHub Activity][commits-shield]][commits] [![License][license-shield]][license] +![Downloads][downloads-shield] -[![hacs][hacsbadge]][hacs] -[![Project Maintenance][maintenance-shield]][user_profile] -[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] +[![Project Maintenance][hellowlol-maintenance-shield]][hellowlol-profile] +[![BuyMeCoffee][buymecoffeebadge]][hellowlol-buymecoffee] + +[![Project Maintenance][sveinse-maintenance-shield]][sveinse-profile] +[![BuyMeCoffee][buymecoffeebadge]][sveinse-buymecoffee] + + +# Features + +* Integration for Home assistant for Zaptec Chargers through the Zaptec + portal/cloud API +* Provides start & stop of charging the EV +* Supports basic authentication (*native* authentication) +* Sensors for status, current, energy +* Adjustable charging currents, all or individual three phase + +To use this component, a user with access to +[Zaptec Portal](https://portal.zaptec.com/) is needed. + +### Compatibility + +Confirmed to work with + +* Zaptec Go + +> :information_source: Please reach out if you have been able to make this component work with +other Zaptec chargers. + + +## :bangbang: Breaking change + +> **:warning: This release will BREAK your current automations** + +The Zaptec integration has been completely refactored. The way to interact +with you Zaptec charger from Home Assistant has been changed. The Zaptec data +is now represented as proper entities (like sensors, numbers, buttons, etc). +This makes logging and interactions much simpler and it needs no additional +templates. + +The integration is set up as one devices for each of the detected Zaptec +devices. Most users will have three devices: An installation device, a circuit +and a charger and each provide different functionality. + +The previous zaptec entities were named `zaptec_charger_`, +`zaptec_installation_` and `zaptec_circute_`. The full data were +available as attributes in these objects, and they could be retried with +the aid of manual templates. The same objects exists, but under the names +` Installer`, ` Charger` and ` Circuit`. + + +# Installation + +This integration is available in HACS (Home Assistant Community Store). + +Just search for Zaptec in the HACS list or click the badge below: + +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=custom-components&repository=zaptec) + + +# Usage + +## Zaptec device concept + +The Zaptec cloud API use three levels of abstractions in their EVCP setup. These are +represented as three devices in HA + +* **Installation** - This is the top-level entity and represents the entire + site. This is where the current limit for the entire installation is set. + +* **Circuit** - An installation can have one or more (electrical) circuits. One + circuit have one common circuit breaker. This device isn't directly used in + HA. + +* **Charger** - This is the actual EV charge point connected to a circuit. Each + circuit might have more than one charger. This is where the start & stop + interaction is done and information about the charging and sessions. + + +## Start & stop charging + +Starting and stopping charging can be done by several methods. If the charger +is configured to no require authentication, connecting the charger to the +EV will by default start charging. + +To start the charging from HA, this can be done in several ways: + +- Press the _"Resume charging"_ button, or +- Toggle the _"Charging"_ switch, or +- Send `zaptec.restart_charger` service call + +Similarly, pausing the charging can be done by: + +- Pressing the _"Stop charging"_ button, or +- Turn off the _"Charging"_ switch, or +- Send `zaptec.stop_pause_charging` service call + +**:information_source: NOTE:** Zaptec will unlocks the cable when charging +is paused unless it is permanently locked. + + +## Prevent charging auto start + +Zaptec will by default start charging as soon as everything is ready +under the following conditions; (1) Cable connected to car, (2) Car is ready to +charge, (3) authentication is given (optional). + +If auto start is not wanted, e.g. for delayed start or energy control, one +of the following will prevent auto start: + +* Delay authorization of the charger +* Set the available charge current to `0 A`. There are two ways to do it + * _"Available current"_ in the installation object + * _"Charger max current"_ in the charger object + +**:information_source: NOTE!** The _"Available current"_ is the official +way to control the charge current. However, it will affect __all__ chargers +connected to the installation. + + +## Setting charging current + +The _"Available current"_ number entity in the installation device will set +the maximum current the EV can use. This slider will set all 3 phases at +the same time. + +**:information_source: NOTE!** This entity is adjusting the available current +for the entire installation. If the installation has several chargers installed, +changing this value will affect all. + +**:information_source: NOTE!** Many EVs doesn't like getting too frequent +changes to the available charge current. Zaptec recommends not changing the +values more often than 15 minutes. + +#### 3 phase current adjustment + +The service call `limit_current` can be used with the arguments +`available_current_phase1`, `available_current_phase2` and +`available_current_phase3` to set the available current on individual phases. + + +## Require charging authorization + +Many users wants to setup their charger to require authorization before giving +power to charge any EV. This integration does not offer any options to configure +authorization. Please use the official +[Zaptec portal](https://portal.zaptec.com/) or app. + +If the charger has been setup with authorization required, the car will go +into _Waiting_ mode when the cable is inserted. Authentication must be +presented before being able to charge. This can be RFID tags, the Zaptec app +and more. + +If the installation is configured for _native authentication_ it is possible +to authorize charging from Home Assistant using the _"Authorize charging"_ +button. It stays authorized until either the cable is removed or the button +_"Deauthorize charging"_ is pressed. + +**:information_source: INFO:** Please note that Zaptec unlocks the cable when +charging is paused unless it is permanently locked. + + +## Templates + +The special diagnostics entities named _"x Installation"_, _"x Circuit"_ and +_"x Charger"_ contains all attributes from the Zaptec API for each of these +devices. This corresponds to the old `zaptec_installation_*`, `zaptec_circuit_*` +and `zaptec_charger_*` objects. These attributes can be used with template +sensors to retrieve additional or missing information. + +Example: Add the following to your `configuration.yaml` + +```yaml +template: + - sensor: + - name: Charger Humidity + unique_id: charger_humidity + unit_of_measurement: '%Humidity' + state: > + {{ state_attr('binary_sensor.X_charger', 'humidity') | round(0) }} + # Replace "X_charger" with actual entity name +``` + +The list of attributes can be found by looking at the attributes for the +entities. Note that the names cannot contain spaces. Replace captal letters +with small case and spaces with underscore (_). E.g. The attribute +_"Charger max current"_ is `charger_max_current` in the template. -[![Discord][discord-shield]][discord] -[![Community Forum][forum-shield]][forum] -### Usage -Use hacs to install the package, add the config example for more usage see the `lovelace_example` +## Diagnostics -Setup the integration using the integrations page. +The integration supports downloading of diagnostics data. This can be reached +by `Settings -> Devices & Services -> ` and then +press the "Download diagnostics". The file downloaded is anonymized and should +not contain any personal information. Please double check that the file +doesn't contain any personal information before sharing. -[zaptec]: https://github.com/custom-components/zaptec -[buymecoffee]: https://www.buymeacoffee.com/hellowlol1 -[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge -[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-components/zaptec.svg?style=for-the-badge -[commits]: https://github.com/custom-components/zaptec/commits/master +[hellowlol-buymecoffee]: https://www.buymeacoffee.com/hellowlol1 +[sveinse-buymecoffee]: https://www.buymeacoffee.com/sveinse +[buymecoffeebadge]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png [hacs]: https://hacs.xyz -[hacsbadge]: https://img.shields.io/badge/HACS-Default-blue.svg?style=for-the-badge -[discord]: https://discord.gg/Qa5fW2R -[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge -[exampleimg]: example.png -[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge -[forum]: https://community.home-assistant.io/ +[hacsbadge]: https://img.shields.io/badge/HACS-Default-blue.svg [license]: https://github.com/custom-components/zaptec/blob/master/LICENSE -[license-shield]: https://img.shields.io/github/license/custom-components/zaptec.svg?style=for-the-badge -[maintenance-shield]: https://img.shields.io/badge/maintainer-Hellowlol-blue.svg?style=for-the-badge -[releases-shield]: https://img.shields.io/github/release/custom-components/zaptec.svg?style=for-the-badge +[license-shield]: https://img.shields.io/github/license/custom-components/zaptec.svg +[hellowlol-maintenance-shield]: https://img.shields.io/badge/maintainer-Hellowlol-blue.svg +[sveinse-maintenance-shield]: https://img.shields.io/badge/maintainer-sveinse-blue.svg +[releases-shield]: https://img.shields.io/github/release/custom-components/zaptec.svg [releases]: https://github.com/custom-components/zaptec/releases -[user_profile]: https://github.com/hellowlol +[downloads-shield]: https://img.shields.io/github/downloads/custom-components/zaptec/total.svg +[hellowlol-profile]: https://github.com/hellowlol +[sveinse-profile]: https://github.com/sveinse diff --git a/custom_components/zaptec/manifest.json b/custom_components/zaptec/manifest.json index c0aeaa6..a29145e 100644 --- a/custom_components/zaptec/manifest.json +++ b/custom_components/zaptec/manifest.json @@ -1,6 +1,6 @@ { "domain": "zaptec", - "name": "zaptec", + "name": "Zaptec EV charger", "documentation": "https://github.com/custom-components/zaptec", "dependencies": [], "config_flow": true, diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..4a7d4b2 --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "Zaptec EV charger", + "render_readme": true +} diff --git a/lovelace_example/README.md b/lovelace_example/README.md deleted file mode 100644 index dd64365..0000000 --- a/lovelace_example/README.md +++ /dev/null @@ -1,393 +0,0 @@ -The most common wanted_attributes are: - -```` --2: is online, 0=offline, 1=online -201: internal temperature 5, °C -202: internal temperature 6, °C -270: humidity, % -501: voltage phase 1, V -502: voltage phase 2, V -503: voltage phase 3, V -507: current phase 1, A -508: current phase 2, A -509: current phase 3, A -513: total charge power, W -553: total charge power session, kWh -708: charge current set, A -710: charger operation mode, 0=unknown, 1=disconnected, 2=connected requesting, 3=connected charging, 5=connected finished -804: warnings, see "zaptec_home_api_data.txt" -809: communication signal strength, dBm -911: smart computer software application version (a.k.a. firmware) -```` - -For a complete list of attributes see `zaptec_home_api_data.txt`. - -Here are a couple of configuration examples for a single phase installation. -Add the following to your `sensors.yaml` file to make some sensors from the attributes. - -#### TEMPLATE SENSORS (change "zch000000" to your charger's id) -```` -- platform: template - sensors: - zaptec_home_allocated: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger allocated current" - icon_template: mdi:waves - unit_of_measurement: "A" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'charge_current_set') | round(0) }}" - - zaptec_home_current: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger current" - icon_template: mdi:current-ac - unit_of_measurement: "A" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'current_phase1') | round(0) }}" - - zaptec_home_energy: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger energy" - icon_template: mdi:counter - unit_of_measurement: "kWh" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'total_charge_power_session') | round(0) }}" - - zaptec_home_firmware: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger firmware version" - icon_template: mdi:label - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'smart_computer_software_application_version') }}" - - zaptec_home_humidity: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger humidity" - icon_template: mdi:water-percent - unit_of_measurement: "%" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'humidity') | round(0) }}" - - zaptec_home_mode: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger mode" - icon_template: mdi:alpha-z-circle-outline - value_template: >- - {% if is_state_attr('sensor.zaptec_zch000000', 'charger_operation_mode', '1') %} - Disconnected - {% elif is_state_attr('sensor.zaptec_zch000000', 'charger_operation_mode', '2') %} - Waiting - {% elif is_state_attr('sensor.zaptec_zch000000', 'charger_operation_mode', '3') %} - Charging - {% elif is_state_attr('sensor.zaptec_zch000000', 'charger_operation_mode', '5') %} - Finished - {% else %} - Unknown - {% endif %} - - zaptec_home_power: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger power" - icon_template: mdi:power-plug - unit_of_measurement: "W" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'total_charge_power') | round(0) }}" - - zaptec_home_signal: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger signal strength" - icon_template: >- - {% if state_attr('sensor.zaptec_zch000000', 'communication_signal_strength') | float >= -67 %} - mdi:wifi-strength-4 - {% elif state_attr('sensor.zaptec_zch000000', 'communication_signal_strength') | float >= -72 %} - mdi:wifi-strength-3 - {% elif state_attr('sensor.zaptec_zch000000', 'communication_signal_strength') | float >= -80 %} - mdi:wifi-strength-2 - {% elif state_attr('sensor.zaptec_zch000000', 'communication_signal_strength') | float >= -90 %} - mdi:wifi-strength-1 - {% else %} - mdi:wifi-strength-outline - {% endif %} - unit_of_measurement: "dBm" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'communication_signal_strength') }}" - - zaptec_home_state: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger state" - icon_template: >- - {% if is_state_attr('sensor.zaptec_zch000000', 'is_online', '1') %} - mdi:access-point-network - {% else %} - mdi:access-point-network-off - {% endif %} - value_template: >- - {% if is_state_attr('sensor.zaptec_zch000000', 'is_online', '1') %} - Online - {% else %} - Offline - {% endif %} - - zaptec_home_temperature: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger temperature" - icon_template: mdi:thermometer - unit_of_measurement: '°C' - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'temperature_internal5') | round(0) }}" - - zaptec_home_voltage: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger voltage" - icon_template: mdi:flash - unit_of_measurement: "V" - value_template: "{{ state_attr('sensor.zaptec_zch000000', 'voltage_phase1') | round(0) }}" - - zaptec_home_warnings: - entity_id: sensor.zaptec_zch000000 - friendly_name: "Car charger warnings" - icon_template: mdi:alert-circle-outline - value_template: >- - {% if is_state_attr('sensor.zaptec_zch000000', 'warnings', '0') %} - No warnings - {% else %} - Failure - {% endif %} -```` - -Then add the following to your lovelace configuration to make a simple card in Home Assistant. -```` - - title: Car charger entity card - icon: mdi:ev-station - background: white - cards: - - type: vertical-stack - cards: - - type: entities - title: Car charger - show_header_toggle: false - entities: - - sensor.zaptec_home_mode - - sensor.zaptec_home_state - - sensor.zaptec_home_signal - - sensor.zaptec_home_allocated - - sensor.zaptec_home_temperature - - sensor.zaptec_home_humidity - - sensor.zaptec_home_current - - sensor.zaptec_home_voltage - - sensor.zaptec_home_power - - sensor.zaptec_home_energy - - sensor.zaptec_home_warnings - - sensor.zaptec_home_firmware -```` - -And it will look like this: - -![Simple](/lovelace_example/car_charger_simple.PNG) - -This is an example of a more sophisticated card using the picture-elements card, the circle-sensor custom card and some other custom elements. -For this configuration the images are located in the /www/images directory. -``` - - title: Car charger picture elements - icon: mdi:ev-station - cards: - - type: vertical-stack - cards: - - type: picture-elements - image: /local/images/zaptec-home.png - title: Car charger - elements: - - type: state-label - entity: sensor.zaptec_home_state - style: - left: 48.5% - top: 10% - transform: 'translate(-50%,-50%)' - - type: state-icon - entity: sensor.zaptec_home_signal - style: - left: 48.7% - top: 13% - "--paper-item-icon-color": black - transform: 'translate(-50%,-50%)' - - type: state-label - entity: sensor.zaptec_home_signal - style: - left: 48.5% - top: 16% - transform: 'translate(-50%,-50%)' - - type: custom:circle-sensor-card - entity: sensor.zaptec_home_temperature - max: 90 - min: -10 - stroke_width: 15 - stroke_color: '#00aaff' - gradient: true - fill: rgba(255,255,255,0.6) - name: Temp. - units: '°C' - color_stops: - 0: '#00aaff' - 10: '#aaff00' - 50: '#ffff00' - 60: '#ffaa00' - 90: '#ff0055' - font_style: - font-size: 1em - font-color: black - style: - top: 20% - left: 28% - width: 6em - height: 6em - transform: translate(-50%,-50%) - - type: custom:circle-sensor-card - entity: sensor.zaptec_home_humidity - max: 100 - min: 0 - stroke_width: 15 - stroke_color: '#00aaff' - gradient: true - fill: rgba(255,255,255,0.6) - name: Humidity - units: '%' - color_stops: - 0: '#ffff00' - 50: '#aaff00' - 90: '#00aaff' - 100: '#aa00ff' - font_style: - font-size: 1em - font-color: black - style: - top: 20% - left: 72% - width: 6em - height: 6em - transform: translate(-50%,-50%) - - type: custom:circle-sensor-card - entity: sensor.zaptec_home_current - max: 32 - min: 0 - stroke_width: 15 - stroke_color: '#00aaff' - gradient: true - fill: rgba(255,255,255,0.6) - name: Current - units: 'A' - color_stops: - 0: '#ffff00' - 16: '#aaff00' - 32: '#ffaa00' - font_style: - font-size: 1em - font-color: black - style: - top: 47.5% - left: 25% - width: 6em - height: 6em - transform: translate(-50%,-50%) - - type: custom:circle-sensor-card - entity: sensor.zaptec_home_voltage - max: 460 - min: 0 - stroke_width: 15 - stroke_color: '#00aaff' - gradient: true - fill: rgba(255,255,255,0.6) - name: Voltage - units: 'V' - color_stops: - 207: '#ffff00' - 230: '#aaff00' - 253: '#ff0055' - font_style: - font-size: 1em - font-color: black - style: - top: 47.5% - left: 75% - width: 6em - height: 6em - transform: translate(-50%,-50%) - - type: image - entity: sensor.zaptec_zch000000 - state_image: - unknown: /local/images/zh-1.png - disconnected: /local/images/zh-1.png - waiting: /local/images/zh-2.png - charging: /local/images/zh-3.png - charge_done: /local/images/zh-5.png - style: - top: 34.5% - left: 50% - width: 15% - transform: translate(-50%,-50%) - - type: state-label - entity: sensor.zaptec_home_mode - style: - top: 45.5% - left: 48.5% - transform: translate(-50%,-50%) - - type: custom:state-attribute-element - entity: sensor.zaptec_zch000000 - attribute: smart_computer_software_application_version - prefix: "Firmware: " - style: - top: 96% - left: 48.5% - transform: translate(-50%,-50%) - - type: custom:circle-sensor-card - entity: sensor.zaptec_home_power - max: 7350 - min: 0 - stroke_width: 15 - stroke_color: '#00aaff' - gradient: true - fill: rgba(255,255,255,0.6) - name: Power - units: 'W' - color_stops: - 0: '#00aaff' - 6440: '#aaff00' - 6900: '#ffff00' - 7130: '#ffaa00' - 7360: '#ff0055' - font_style: - font-size: 1em - font-color: black - style: - top: 72% - left: 34% - width: 6em - height: 6em - transform: 'translate(-50%,-50%)' - - type: custom:circle-sensor-card - entity: sensor.zaptec_home_energy - max: 35 - min: 0 - stroke_width: 15 - stroke_color: '#00aaff' - gradient: true - fill: rgba(255,255,255,0.6) - name: Energy - units: 'kWh' - color_stops: - 31: '#ffff00' - 33: '#ffaa00' - 35: '#ff0055' - font_style: - font-size: 1em - font-color: black - style: - top: 72% - left: 66% - width: 6em - height: 6em - transform: 'translate(-50%,-50%)' - - type: state-label - entity: sensor.zaptec_home_allocated - style: - top: 82.5% - left: 49% - font-size: 28px - color: '#ffffff' - transform: 'translate(-50%,-50%)' -```` - -And it will look like this: - -![Advanced](/lovelace_example/car_charger_advanced.PNG) diff --git a/lovelace_example/car_charger_advanced.PNG b/lovelace_example/car_charger_advanced.PNG deleted file mode 100644 index e1c090d5596603187f8e021d8895299ef1ebed4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119447 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 K%Le*P{H^f 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

vssNOtYMj8BArgDTna z6>lF-9ops?N5rW+{tXVWUl`ToL>_QzY5>h8-uJgFTl4rE4j!RTOkwD|x{5g0BXP@r z!iOXgHURv6YB7&DBK_Otg@u zL5fPH$<1N>FQM*tXG6F1x~wSVB%VXB0iH{fw1#YdXB-7XM30N+8JbotwpT^>+H$YF83s#MTIQL77iH zRYctYF?8dEdF*Y6NQ76b%Q$ewH)vw_ch@Y%iUxdUWm6o+Z#!5TDL@TSxHJD=7MHCi)&41o3>Gy*pY85l-X|^>_!iH3o=W9~uQe4^*_?+H)@z`| z{h7j(6K2g;cDfo*{^n1Wl5>v;6B&UTU z+`)T*tz75e1Av$n(88c~mkJUgU}oUE4vl}|z>!VUn3jES7R~wbWv+Xab6%I$PhKTk ze!3cXN0wUmUY!!17RS=<;A{2o0I_SnwxO?PX#3{Rj!7V2v@sU(b)qj^&$)3ISCDTq z&de*+OR~kZ;BL*s3RPQBj+dh|SxgMzT7vd=XM)~`RG>Ea_EsxncfN=UJrmEWvfhHL zOB`3|y0p5~nR^W#n-_%gi)bq!BSWG6ppR!KRIWPn_UA5yxzkN;40b1iu)di&pw8K| z<>z9{&1r@x5}1DJ#|FRYkfOn-1oBX7gY$8JG&cTrJ*fq>E!Ud|GP{S?52-f1%$;i7 zaI<_rAF3sIFT^FA6@IhsBd$`bl~Ak|XBGX=(8q*iJdvpsUPXCes0f@i!E2Ar`tg5K z3t$A8dVhm*0EpKlR4a;&O+fL+U5LWfu-uE6rh(dF?75$u1VRItQG6td?;}^xgfG>;&%VjIl_339p(KP27dw^nh1E zs^mnI``sb{HCTC8ugCe)rH0Mi{y={9;XNVOGyuGkvH4R2Kls45yqW+Whm#l7!wn9y ztaq%$Is#A*-oPCnlkp~thb#9BM;U67evbP(4obIRk{}{G{Nj$-JkmWBBII_5ItBt5 zdi5Ytg1_65fK^V%!+B>7h2I}ve^4w{r-D7EfjriDC$Laws=rxoyYLpEeMYY7$xfN{ z4s{!8ug*bSmk9N54FVEP^b6E|5DR_PL^~4Mq?!mfDN^|!ot}YqYRLO*v1t~|%9D-i zBe<40$TEd5bbVSX%XgU)(zsBiM_f{+jrQjk+2Ubea(ccB_EXNw31je|aUTgMM0chR zt`$_<0dk3#M!?Ne%fIwogy((Q2HDw$jh(M$b**IFtFHJ$qaTh=4`d>kYf&4-eI%+J z#M?aqe~S|=Cp)>PuTgJ^@9k5B{^4z{D&oo?DrM5Z?pVbrHll29apXzR8wk$Q6Po{h zU>vL@O;si{{~RgV%rfqP6&W)O#yjMUkNCjMdr6xISCk5(@CiZbcdc*rRV# zGHZEu_DQil`*mM*;@B$sdz2?<8{c+SLOt-^($ks|yQ6F{ou32ex#6fd2%pv0&#VS{ zyiTB@*rm$2i+39{4zVwvO>&KM%W{DIP(M^>_tz}Kj66xJU-a}O7jNzB^tQG@y8swB zSOrYR0A-u{adA`Ko>CT?@-`acBeU^9Xv{SJ8*6vfZOx#_3>jo>$zH-8oomD>p`o>! zYK`kpki{sC_W^9mH|it-MrW7wgIm}A7J31JH-37nNc+CT*)!OoE`fyQCWIt@w%S^8k6tw`q>ox24uRzg+D!`Q>St=4h>Xa&|wvfFeAiq%(%U zX`j+%AzU~=h{Zz-6W5<|RYKz8mLeTk@x7iLx|&XUTJU~-A3NB5G2B1Kj}HDq4r8O1 zGbCy^?Qg+C@jc60o~LqF9Mt``ClTZQZi7$?zbr=pJC?bXN5EP&k=h?ZljqX{Aj`8)5h1yZU=M(732=E7=`^J#A@B% zl_XMdYI;AH#@g(-NGxz$0Lh|!<4pl)Gh~=m;um6B z%PrNONMYFKc1l?}tpZM#WMCE)7yp^=7=E)1HFOcWA6z~myd6xK^~MrrJOZccj#EcZ z4x0++JoZfRr{`PpJ2bzOg)jZ4X3Ld|wf*0%J?e{fe13Ocao!Fs?z#Oy_0m4dkv;Pz zIB|#6?SHe5ECZR4sSoS^(0j+wJr%RxJIpd%Hoc%thVXG83JD?8l*rX3tRa3 z94hy-bMU^q(u`gQ-+HJ6g!Vk$VMJ9pFNoPAEY{a=lgVUUd`wxX%O$qVC$h?%x!?70 zm$Od4A371V+t0QPYT28JEA!@s(E)AT&S0x(2j(hC6}5G(`g+XkwYk$I?s+Ba*{6wUml1oOKb)B}dV;kDzdwvc6S`Sq#Z-!ltD9W zrm%QvWWJI5B=7=@=2(1$9B7ONQ?BMt$w>4A^^NkZ=ax4i0eE-C-IVUvHgE`$+*z|k z$f?sytUQHIxZmee0E#`2AywfY=$*_*69H#MUNgQ}nC2edjk}Uk0^sW*zL$rM)OV4dnjCGuC)0FC-*rYpE>&7Be>%s}%f+!ARQL zWSf=#SW==2FW6e9-fEPTYn4&2O&^MYkC9qG2;hOzwQeU5Lr%JS^zY_9LO)}RZN}O= zuwOTD{?GJ>YOR}^f~S>}_pK=HiSQ~QF1?xBG35u*J6AF1&E+;)QkJ3K-UzWo?s6z` z2*e7xn#lfZ+KVC`@tT1ses)$rWe4!kjg{O`2Ts9f=8%O&@VZ*#9|)yb_kNvKF+kjN z$2Rf{_Xl>Uv(Gsl(z^^K2oZ|KHV0cBQf+5k?l$%MCwl1HZ|dNC&apIWTWXRT$zDG( z_-?KX-JT=2HZT_xMonx9gh)kn>-OfXra+o)t%B)Nr7jzs@T1yB0H5H{=e)CXjA?dX zv4xws4C`S3D2zx!@&0^?f5ci{hl)vctEf$aPH<`C@Nd_<_2~1kbm#-!SS{>(qN$e` zcB*tEF2kS0-U%5!q>5iwWumf7CcAC{9UXA?COTLN@Vnv>Vkw~l~6U?5m+B{Js*#m-K^-~i5(hc)A)`x?)$DtXE$JB zz^emSV1QLJun!1II--Y%Qffj4s%RBPNr2Tg2*}^V2m9+Wh)jd1D!SjVyJhg@!`K~iJS9krqU}W$pB8<228OcMZaGu2q zl=p6yb=Je4TDy0L-1F{h55EbefPCTQK;9k$6Q^g<)BezQ^0z^PA&fK=v#(;|b1*gJIk-U7zcB0V~(q^fX917%T4N4y?@ zP%)PWrld-@W*S1`c6ShNLUN1}7rM#L*ARnEV{nDY-YMU~NQB&U(EbZu^8`azn*eJrc z&cR<)1Y(>NH}1#Cv}fYGEy~AG*?gbuwSq{!L<>srR%nhSv?fDI7@&~po8R4O#e zZpbh22ccli?u8(;8E=Kl=$1Ieud(tnhRep#aH#;^$2VFW7t{=GDMEkUQ2(kt*-7*6 z)C=`(e^rEf>fwH45RVDj)3X~{>t`JTab>p<1KzS!`JMwll`iJhh&3!Zp*GI&4Nzs~ z)-A>A;iU;RK`hewbZLU z^5c7tAOVFK@BR)0^J^6s%COpN{jTPsnnfsJ!}MQ=r*ML!qR=lW*(q7U>?HHJ_Z z`S;HIs>Fje77DXoIy!^3G4Wfshoka=@MFTl*hk%aWO{tJ1o_b7WMBq?R$2fj41?k( zZjTaCS>dVI$YGDwo~Z&RM^9K`D)MOkcU_4mDRXnX1^DUY@W z^O!xkI8!sQ$o;hf_e1f5o~>V-6LWv%a!6jl1augiJaQ1{MTeuB1C42$*h^^_K^VQn z-yXhtEd%?OW5SG6KivGa-SHu4QB(bxmy}rj(d~sa)`VO_Y6(qM;jAt$Ny!g7n~url z@*-^Eb<9)Jb6k#>;tCSq7G=zBlMuSiQ?xOS$0?f>TQj3Mk<<(KgP?5`-FtE!v2{xE z6h-D1=Uh;>Ws&LGZdx3ftWsC=a!R)>1y|e#xx(-(+$x9mL&&d<;%oogYRHzEGil<5 z*rf(az}(oeTJO~OEaC4Tymr&goaQt4k|kwJu~8HJTLLSffd(^z_dqf~cfuD3>)?Dm zrmy~cBAo?dqUm3et{eT-?NwuCM;mu?vzt9M44wqHfx!bbQDxL$ubV!QaN~f)G=tWu zl;dR~t+u}}(qRBYS$~7Tlwn9PthBI-n;Rw9kUIVIl(3VN0_bedf_0q+u!OPc&DX+3 zhDF6qzY##I6*##t4KfCZzZ|+%+6FG)C1vUB(^%*9?3!d#dBYce4m50+A&B8_qxEYq z3c{!B_E|{kbI0eD=cg6tBac6S_GZW2_IY@s&ahQ-OfYUaPGV z!VESNQRYeP21o>M1>|}!@l)Z``>JAw9vi&GpFNm@@!ZbS&8a@ZIl10wo$B;xM^Un6 zPXsg4E|sv(q&54TN#*{?215e;JgVesCw7EVEpSPhF3SBWtd@J{MUV*P~y@Vr_qG5 zvWs%mWZwjj+_Xn8|CPV9f?AyXW@;ORb0`+rZVBXXlws-`=}U=5oU|quZ{#Jux*cC*P!*14B)E87JqY*BuwEZ?+b?f&jIZpU#51XWcm36xv`I?&5(%QF7z;ez$$8C`qTb=4&q?NA@l}+@ z%HxIv7IMv#ZqsoE9g`Z})}|6IQ=>_bl{7o{`4<;0Y;V1LZlO9gGDSL>=wRk#pWAc#=7co2T0r-CNHC zCTK`C<#%)vAT;--TVO8#3^0b0@Fx{(@r#M<9~IkZ?h1>*t5&^WXvS2}<{*K8$;nZF zhbYRTcNgzYRn2fhqNj}h-+c=Is=9V5{je;{?eNLlTOQ=Ek{2;q^j6zzrGaB#0eu|f>Qm^nHAk1WQ^~)Wgf9f4*C$B z^PJlM?jNtk0lI#Nk0fgA!yW@u(yY#jq1QD@Yb9vei<#gC;Fh!BqQEJZN*Qo>o1I2b zkjjBv+&OqGL_U|4t-|mLM$s*K;&*OO2r+t4wp8GQfT z-7j;N%oD}6%p2%176Ho@k&G{BbKswj7A7b)sz;;xCxUEdPQ+k9k|aLwvWKlc+OOrS&mTx$AsN_yx_pQ3{a9t zPAwQ^eXl-C;W1W2%#~#^@(L>I@+Z9OdaRoh55Ec03$G3rI*_(> zJ?@1Od1I8rC(dG~Y;x4ks`@)!8?}$r&PyuWF~S@ZO_w?vU00eq#YHC-DIG* zahorP)`9~w`An*X(1rgDXOfzg_^M8)x;iI+Oty7D=J>{M`sxoayBWCFDc!7#4O=J= z^S`SPW|(-H7r5?HlNKr1enXA>Gt9wMCOhK}V3hasyDi5*l5st7 z(tJxYO>#Hd7sJAQ7NBw*A%R70bZa@IFP};AN*cMUHpA3+eU&gDm*T4ZdCpo zfGK}wCGkipK7<4Z+|6*&h2m(e^?V5aP7;kV79;`y08 zpY2TFx8^C{Um9OLf?v?UJj7r+NJAO61r+M=HzbE78nv-$#i`%VTRmTJd3$0~V)gDg zrSOPq&%{0T0c|!jzv?qajV=JX{_m`c3mOfGE6 z@=R#t6u{AZ9X%oDy5Men=092{dlya|Hh3yW=9U-?Z6h# zvJUSwp#%Fl!k6vAia^~X2TxI5QqaY5HYFv6LYFukzkdh_)3fXFu-Tid_~Q8Li2_9Q zYwVPh#($TMbQjSxU7fOu@5;4JAb!796oXqEHsE)4cMk1)yp$aj3+c{`hr9B*m9_2m zU6E=HW=TkM$nr0~4FqrhHRBBgB4o?e@_`f35lB(Z{q?<`rZx(tk+k8YDhlo#pX%s@ zk-OHguGKB982KoBU*z)VtAflW89RMaeJ+G}jiJX3^Ji6cfASgdT#`G&1V5Iuw3ELB zzVZ^2-nw3%x41?v2{yies8aFuO-(0FzOvZ;se0X%X|V%9p1#0>k6+PGyZFO@DeN*E zUcG99<8RoFU>=C@{R7L%XJI}xsk9mhuKO^$v9o8he{6T&T1v>bvW2G}TUU58?R}kV zpcZHhRbLb4O3n9io>A8tm>YUGc+Xmt1EeA5i~r%Gn~2J^DO6vi%1U~$@^DxA@%Olc z92xQ<^>54>hP(R6kr!L44BWTSKL8aJT zeXh!6xiyHc^BJ|#ZDRu2K&d4hpl5w-_=Y3c`vW^q(s2bVQg?)1*{y98rin7M$sJhgHE` zXCPP8$2aySd~W6rtn5y) z1)s4I?r3v-e(piI@<`F%}=dj7ny|K*xBG1TY`uBxKq(WApyoo?Pdrp%0Ky2yTcvYI*ixQ~G)1#(VJ#J+2Iqcl11ZF3`F+Gul?`8D?R-Y39=5{E>I zL5s~(oYGH>q;|K6e5Tt?-s>1+u~ds{Z+w}N@{%1vK;^IS|n-! z`wK$cGh)P4Z4t(e_dRj*WpL&SiKR|w_1a8xD3Sd4VImACOh*BfY?FIR@B>dAG1f6m z$Bh_U7Pu?@bw#-duD0}_Jp+0?mwK^oUgtgiA@POXs10KJAi2AyfwzMv-$nuF-nRA@ z>8O6Vl!21IY^KsELLbc8gBKjR)p6VMVv4Ka&YIm^1>8{*s#)N9B&CD|wyxA>re=~Z zVxH8X$;Z!!rN^zmb|p_}Kd#!q@FMOxJwc$8xqz>J-H})t>8%oZ%jaM(wZQ27WSpRE zbWsL4b*t=&=4{p;N=n>wjk#Rpl6vC_yLiXN{jihKsj%*E_9(&bV%d&(+R5Vkm7baI z%}SD!?Mx`i1LuRfikh{DNA)E=8;vOfgL|cPPr8mLQgwRSH?h$oj7OgS?56Lp2M8wE z>3RpfB#N`Hg31`Yjc<}ns=~Vi8J!{o2oQ_7kL2d%e` zWkpsPtPhxew9z(ywB!#m`>Y@0^lgQ%l>)MjPMUHdx1J-LaQ$+y-_GwdM6n$%Tm)~r zAnsUNBHlJ_0`8#Gzk($9S&3UxCmO)>eLOcL?=K29bdY>te(~C<$;ZU2i_GQ(;6>!n zQzwsqJ#k>ccG#<2qGQzF&Yy1{IvZ@yS68Mr`KIdEi*s^Th05HVt~!I{f5*=;@S5Sv z>5!!y&{|kaRv5W)me~@jqygUHoOL}S-2*l8fu4mq>+otU28m)lpi(~z2e|iIR56-} znWmq7J3#APro+32bQvEN#Oa#7SpG z;S-n#SqK%S;)k;}buW0b%clIvVnLjtFMsT=)mz*NGMAKi_H-*3^FuIu=cr&oJvOUB z&3cD{N=jY~9^wBayg!+b__uFl4@;@<5r&t)C+k+NZyyZeasS6UBK zJPwZCCDtdWF=Va%5&bneVa6Zk74Osi5=EcMKe=-X#Wk>Ijc2tRNBhJI=$ z4QNhKf`jdPPmb~nu*4`Jeu zjbbbNztrP-G3uhp9r2EVPK7A%gF)xxwVu`c-(q~->LmI2`s!=K{L5{yXgZzKC4R?v3C#%x z91b!M@om_y5gCLnb~ZDl5}v|5J|0BPg$B3=j32Qxz~$=|`H%3Lz8Kq#l-eRz5$*Oy z-_Oe{x1^4~&{TV~1obqYZqm?Wt2*_W zh3x)I8j=w9YXcirr{an@W@I6#)PZ!F4eBv+Fk5GSzUYJ*UV{;bt+w*ieRl0eH{2w+ zaTCC+wcR?!NtqiprEy}w?3@g_`nGG@x_ z11$yR39^IZ%m~LR!6^yak|Hjz37>NFaNm1rTv!t>mwV`wrRZw1`ld?_P1&`lW<1qC zYON(_x;Q~#9`6H~&(}|fE@XgSRrFfxIa*bE5Bx!WAArH9Cw6%VO}P&6U>*6bFb8+hkhOJyjRQ23L}PelEi3j=N^5JcGPl@881fVT@j0=ngrAHVI{ai%h7IJQwE?m8g0Xejr^E#v4|f3 z+ve<8rk(#7zSA4yc$XhXxB5qwvWp#gg|t4KE6VJwMb5Xn*m+ zv_@V!sc9juk%*n20I%Hy_vrq@f@6==<#rJ9^yuPEfnB!lw)f~{t@3X^e1HKE=b{8u!4^!Pd^k!NkHqN*gd2}lXFoe|E(g#W6f%?=jCuQ=x@5MWikR$Hj0_Bh zJ2JrN>a$t3f=BsG8a#U&ZhlH~oA<`pHcS{0j|W(?F0XY$I^oP9@W{)nrKrEhM@Q(F zJ8d!hhx49W=lgWNOebAy536S$hO(yp(UHToM}m7GRrsu&Ui-zYFHr#T5d9a-1@CZL zw~TE|R4hiB%y@i@BiZXSa{A<5s0HqndZlq~+Vwepc`%UGG|8iHnytB8SL)?vO>W<% zr_+HD@e^yh9PZOdsO4ER>gA4b_WA7LmC-a0?+i{kGM~eGO(L`Kv{(nrtO&z&iuXCg z2@j(4?bHaU+;bCV*`lGylr`tIXM@1J;uSPCz%+QTVN^XanW&;4eb4X@8MOhUS?qE^ z0PyGxXe^+$3I6_MN%vPL`fRE2jxLK%%~~Pl?#tu^Rb^2=l$LlXG`>fe%%A~w&4Z^N z#k=1an?WPsxd`v*lPyHuO5`@9r!fgSK3eMx8Y>MMltLn;rQg-95DKib+ za%HNYfC{eWcX-nDq==1zrBT2$O2?GUqMl%vFOs3I=0*#q<-TVv_xBA&#Z>YYjW=-b zr(;vU!RIP2#Imu%+PEM9jHM08vIc+@hySfO(Tfze_Sv5n8k;3zn~7d3`(ai{`u~2u zeEJ?4EB}uoE;CMAvJfa+bjf{e=ORt_1a#12!z7FG+|)!9-3wigkG;BT_AqKp) zZ^=6cg&z*9nH2x?%hL+>tTk{}^XHzL{A)Z*kGx`muo1wI*t zs)SD7VfSF}nJ|{?rN~m5=jAB!Q$<9Y>U>*xp`^=4rq`dJPK=h%mtI2Nj>qWDlns6x zr%`Bnuh0b_!+2=)zoNC{G~zRnY;Xmuw|B|BcvorxaJCm)c^h&X7~vgfQ;I*8CKUDg zKmJf;1(RUUdowSS?5l;T9O=L$^s!(wZ_UTx>hkERa6kO=oiFsO)#tbK6eecJ4-{Sj zYT(51Z3i?0G**1~U{i-z>9}b85}OxC1T+U`W|*Dw@Ksj7zwtWxt+#-Mp=-$Ljmduj zCYfH-ogzaa@Fz^8g&WS+NWrM;gY zHZGZC*8elQl*7>cKy-{NWSq8~>r(LqAooc zb7q%lU_qWdHCreBC1G|ZA`c{kl8mQFdrdsDHGTs&<0J#ZAl}RMlEV4EHP|{N@o|_% z6VU5`g5uyhi0`g8bN4B5>2`iMOC8sYKO!9$X-h|C+~it%Y6DNb)+uj z!x|_S5|J|t`VHsC8f3fu6Uj-Ek*NB@Pr*Y6J|Kd*t zUWm&B;wOOK!WUH@PSamDA|c$Q`XAmgvXZbHM{t`Z6r(DPGd#Bykm_YK|M^thdn{X2 z@A|kAwun0%e}xU1&N|=^ns&0rvaWI=B2O01D|jTcLXuHB z(Pb5*cZ+=`U1_jtNi^z}$3lo`AgIS^?`&l7v=uKF@i`6X24pt4?7hsVbrj2$6GY&) z8un67Mx@R)y=}^**3*MnLV{q#F6n%Wuw5?15=PiTNAQ)ZUzm;h@#`o@r_55gnxWpm zJs}3EdImPBgAacT*^K51jOxvlNR()l{O;6kg$EDXw6shc&nPKb z{Um=%o$AhugJR3R+c6_iyu>I+{|Yr6AcH|-ydS37^f*2KKU977LzGd|wzPyIARvgq z(kmsalytK+3(}#$ib!{NOT$u<0!t{}Eg&qglyo;shjgm%?ejd}58q#K-)GL8Idf*N zxvmG8IF|QeKz%Urq;zM~9+`1-cN*tU zxBLY697~Y8t8VlvkT&y6^gCT6&*W_Ro|+0_n5y}P3Ar3U%zR3}`f%eMJM%Rnl!kN< z3j!odbmn%K%P@IBg~zdh$|f41+Uiu#b&H3DMAvXS5{uv6>tUC;2KXqC*|VclX_!#l zB97rNyn}siO^BuxEh@{$e&oBR@LB|jcwOW1y?N!TiCp`LTZ*SVlBE#5hyj}+GlJW% z@d&>>VPTZ)O6a4cg^uRofu(~sBnKPwr|&^M0}4nK(Y`w9D=Z?>>0eu!A3P+)L~I7wUDUyZ(2`O`u6V!j(Kj6FXZT+cm%MZ;SpdPj#S5EULmX2wrMyNDEr zBopCgOxp1sa8@)M49xidO;BKn3iTQBz3i2^ji`?q$aV3}z2y16rgVPk|M&7n?cD_} z#uO-LP^FbI!^8mfMOx;|O^`P8Qzkf0DnyS^Vm{)73|Qei$|C9O7g#+Mt{9@igknyD zpi&ccR9yaD1m>ZlqOZ&CC&Rx7h`7tOAWi)+Zx+)I7F!+cZSO>x4?CkSf29iA#g9x! z22X2@y)~Xy@LznB0y$*gr|#Ast%V5)_0%uH*k~U2-j^qj?@V>N%j{PrXI#nYJmB|+ zqS|oG4ba}BT5Mo*y;@>#Y2s-=fkgY1G!UHU?QJHh@{cWj^Hd~;h4#rso)k#7n>VVv;%PLm7q!LuaAL2E(&N`<8=;u0#COJq>NH zbNqF_cN%zhHMgh!*VMj$ZNAIDb?5gQX*6dzOp0^7F1Kc!WF{yn)mj_N!u(B|jGp*J7wJc%BQyu9{QGqW{$`KTxbsIC1|JaIjN%4~zGWxp9 z_U_)WeD}#}5zVMaMecXRF)j9=sP9+AVss6=cVnsF>Aq7}P{abJ($;gQ?BG4R!W-qz z4&lIT7jPH-s%GMr?h&P`)V(@*q1!ww>)ynqk43IVbt67Oe;{u%aaAWMM@?rh}gC$8935||uw`&k~A5=wISfSru> zp1mHSgyl(OnB~nkuLOJT z3lRzrXV&}s9Wk)IX4|>p#O+?g*Se?m39atimyFwo?V5GH z6i%98nY&Cx+7O^QH2oPiTo{2k&IiLDvU~E@#rf>OkiEo2b2S<;}1Eh=$z3Zof?yUet5V-2_Ol+c%uOxdAWlDJ<|23+~FKp9zT>KD0ehfb@FWU-v9jpLvBFSjvOE&ab_r~od3uc$wvL;hV=`ap$x%zbA}jt}(a zm31mpvX!SxJQ;SOj&J;frk2hAqBKpT%r7fZd- zB5vm;!;81Yf}#bly)Q;BFSeIXZ-$=jX1N+pJUy}lxK$f8m1)OkE0HpB!Atz9&aB{D zU@zSsr3{D*=%DFqW7g^VK*_q%z<5=kzC>J7YwDbWcsl~=X1)C5*z_)7@S57F7#YP- z)?sI}zw~Ek#IGD^7snM}PM1yde{omOxl#YS^KzHd^!n=|tN9bZB{EARnW;M!n^^56 zvpX-;%%U#oq8AJK2CMMz{0TO6f3(wvxjnXLz#AQuW}Q$SRxqV5X1@P^X;usvniZYa;(fwY zC$>B!;#FIE8AGSdOhNq7#!EA1gsbFuHU!5bQ(rq@E9#?zkJlmYZBk-o+~Oa~%4|io_29D7pWf>T#P)3;!S!Vgx+s#ay-2SyZQg39Lt#48L_4JH=yRLcXqgR!l zOTI}I_UA;}kO^&m=?5|Kk^$PRU%MG!fhJUlBMlgnC8YzDaPU5)WG{(sKe%kaU1~WM z-4q>hws>!0fyzw3T(WHY>z(5}EPiYAQZVjU#9*|;z#X@s;N=TJiTcYw*Hy>&|q?VF_xC^(7T z&rBCNXa=ZInKY(C!r0Z(t?HgDlDv;fWpgh6JbseTt{ zB+GKonGoTfnAfPgjszgz)x6ktG4g}+oWHo2@yD<2q{*h^-sQbc-b-IULH?y9qT%8aZ2*MYqQWe| zM}hMKW**W=qbQ+uoy?+?Sk+SLhjdT1Bl%#ct&q-xh(t38rsXT*cOWAbOUWPfVdz{C z(uO=wDe=*?4s+*&b!PCqbP3)B)ofQhBH5~di@NRul0@;*WMhI8F5x$Ibo^3!6tRAv zeXDbXS52>{|IxEu%H=Oh$4r5}BAiqS+y$aKgLmR*+0bEP<&4F^{HruZ#>r_P@Uh}V9^a=uX^6PRLy&Q{A;?XBydFrwr7 zqx%hQVo^=#nYid3|5aWe5mWmB?+IaW3Ub$_XlcbiSi4Kl!Xn4AV4vrmKenZ}No+n%iihWW|tdeya*0 zW|#!Iz+|(67=%4zNjtnUy!sT>O<3M76=iexr){(}p1g}ggvuqJ>hbN}{t|>7{5X7d z{?pWNUGnlqB)mnYcf_rT$0+q%kQfMd@yiS8QCS?)hPVyk(%>S$*_N}B+Lz}m3))Fm zVL*5%ZM?ic84Oyx;ZxBrYK`t(Y$1>a1?PwRGfwv#iFnW1eme-d^aR`6EgXp#z~_m7 zP&|h-n?)IVW;0>@oPN&aDgn$_EBB@uFQl@65%R^J80WkKuiJ?jtXyUHy-Rq7_lj~d zD-~iPIfloQ0D3;UEfk5)#|B2nc`vt1h`D4k*Ie!flMgRWZn;+H2N+x$Ko&?{wA`Rc z7d;td^EW8?TKa;zoX54I-L=vCz&1i(06uMI7fSLRgmW`Y)+8qS;afC?Lr8fAmPZ>* zlmuwh^yi z3U>1mtC307rHDVlZFfYq`zgSbFWvuQK%vicRqCCtFo(SbjOOQ3`mQrWlEq zzZgAJewh2bNK~p%D2-S_ak*JRnoYAhULVcy>oiEQG1*DF zCxgX^kY=s$G9q4LWu)0P)sJNHHo)X>Ws8GtB=QdbJk3#Qa#mTeg#Ek9$)Xe(=i|IQ z+A392&TB|{=Eq+0+PjFXc!*hJ%t5aV`nj3!0+D9ci8TE#YjDfuusR5Cs!Kk4sk$*2 zEF!B9Ud~azP9Ry(QsdloOo7;4QsFm(ls-N25ftIsZ$}u~1U(28Fl^QRG_ME#mgXFo zRe~TkOsZ~^g+C~WeyW)H+P_T1=|nWg+GqRf`fzhGTt7u0Z8EsM2vI|kJ*qMb@rhz7 zl}1@&|K`gj2w0VTdG~gOGn6|XK|@|;J`XE&%Thu&x8tvzD;>X(jiq_26-jO{-v``` zPwdujj}xkq<)HP5T}0}M2JdxumA}e)d_Y1xi&(_ zy$wKI!!l~k>RI3>J9T=GpK_@@(im5y$)UveWK#MRbZZjHWv2W~tSBhOSplhXP}geU z)&G_j4Cu^(48ek-BAw%UeQ&pr(M#-JUwYL{H?4gDAI!4R z@;%k-d&N6w3bz>@5%aoHxH9=m`&+!(Z#9^VsS+V+l1tX7*o z=f78>8yoKDv@nyVgz$JNO%H+xobK;ES(bnSTl4+y&VQ1dJ1Y~0R+<-Bb!Tf_+_pda zXo5CrvSt2v6#?stYd}cZ=TeA+J}z7;iAKWYZ)cdxt~)-{C^aS6Gid*ES4R8H4xZ|^ zFtZQ4+A|0(Wi@#uZ4iMZ`kX9MaD-o~q3Q3>3_NKwAAdQ_HEKytOz3kMBa*S$60YVL zcik6Be1@||W%`qa+?f@Av!uef)oe>LQBD%ae(VL}+&OpnP2;AGK1(CL8B@Sfi!)o@ z_}{+%nl}AC<5W}KZpcBr99%L$ zLQ*KKIFy38=`4{q;Y!UdJT@!eOdX;v#m2YH68JSuMmWjhO;>|9N-RXW+``ASk@>Cu zuxZl|nBQ&Ny3eBRT|5Vj=NFzrYbMU_@4d*vp+pb;t`AziD(jxlSo6*mU%^l%_7}nl z>nN0Vka>)Wxpp6ZN)@?c)8Apu&m!Wf`=_)+-(Lt4SjpK$?xA03X2C$qMprNn^IvlV z%Rjk!O&9YJ!54(zD{}qKdFhy7OHZfUP``lr#SWxE6)YPdNAj_Iysrs+{BQrPU?%e( z@p7#-FcZwvjZ^@uu|o5iP42rwA0shSA(aXjW?(wCd7QfZ6a!fn^ZCT<=y^-e;+aDx zMJ2ptQgb9MgC?9+Q2AwDg#dZZGY`jy*gv77vJz%|?{)Ai@)Q_4Y!}HngitCAOD$JK z%*1MW%f7NrnZkHh?UqiK@G6W(VJEl#VXz;?= zmC#SFu$gLMfUAhf!t6pwq7lcUL>@Vh_z%5VX$fz)4DZ=iGXn>|xhA?xKnh(hw7CWy z7cGm%B1}ixcy~zg66CNMuU4l!P!VR z?3kZO)T45??HJ8K3@2IgIml3q9%$i$PfR&&@LwXfV16#a$&do=NKU;-D*YXSCq%+} zL6T;Kr}?p@_i3Jh^BW?ZcYP`qpqdWLoP0^yj@W@CS0PV6FvqFB7-^eSeQs74o!orC zT+NZ=(fePxF4I{+?P@A7`Ki`UaiE3 zqsbm^?J0r=^oA1d7N6+!dV zg$Spegfw6 z7xL)Ta^7QZs;V8@>flmF2})3b2aZ|ytGo!|b3{`r$#vn@%u!~%?@zTpmTe_86ved0 zv~C^EgX5@7PrcnHbpIg6J?;cg?n)=lj^aWH++fS6j~*+;GnS|>_v+tU0vh09eqiD^ ztq&DxW<(T||F=9}Fngo;pbk15Kniw<)h^T1%~G6|G@2jl?| zzXxI=2rGC!3exu@#fi- zmcL?BcoiDo{Y|tSW>>-*qB>1ovAF7-Ogv7tKs z>RCmV$EfqLbc#uJE-WP*LY(g1!(<*NVaiLXRRcjTjKsO%S`I@O?zUzB`^G= zwH>i2e!;mGG>vckq`6^2dPx(;5$)GG9)bqF_LvX3#_xn{%?=bk()C5 z>5jdWP9{uq})w%@`~RTO(H`Kxr*6_oFJ;g2Q}{l)R>^m`KL zgsm52f6EN@4blAe>;79N?H z7vG+0{vK8zII?UyFK;zZFcWgJa=Y9V&QzC!j1F#fcs@ZWzCzqK>hzFT+^}Gfzz}It zO~2nmj@q>5Tw|q#oCgsu`q&EUZ}qlH=nEKngK7{-5mhQ)L)Ki(Jbwwn=HDT)R5@9Q zKtF#FqAGDpu8p4C92xBq8A|e=8*a3-H+;f29l~Lcn`7R!&VmUiWpD{6y^GQ4PMMDi zR~lkgQeAilKHWRN zq&SUJy|$HoD*S8lXkoqpn^hV}cr$Y^RS^)vcWkhBr*9%&C)f<0zB_Z|-+7glkjl zfEE(MW$eqtJ%;GK=Y%ig)1Hl52dT2)-HsQ8liVO}h_kr^NGJzqmGxz9N-izSX(Gan`AA#V zz^;$(3ud)J7iNAq?cLN}b&Nk5$}rtA@!t9SZ@qsO{B5l1k3STsh+$mhD%xCL5|OAE zARJ#>XMRJ$3X=gg)%g}OJiG<2f=mmWPRsg8n5>QGLP0cUx{C$Rx*8!<{C3y`T@qmk z4(7tT-$X#Ji&6^a2r9vINycS|iI2cgfu=+PPX|VO+-m9$l~a^WcQvP(-qTQn$7@+E zkcXgei1QD7E|LKB(fOrq^ZP_at0sqWk^t3%iX=o8@7>y@+#&c|M1}s?^4w9GQQXRf z&3q-bfu(vgf^tJXO9BWVaWJ&9GC?y1#RUT=HSb-@L__Z3K|LS>=D>fB=rn0QA}6UYW)~3f}Wbd3DlpUtcN~ai>`_AxZ>RXEt}(n-VFhPdA`m zLDGGiop^+_qP`KYLLj*v`70*t&YKVnsb9_jS@7=dY$t;+*=&kIl5)&CIaahD^CYTJS#Qpj2@OcCz!kI!U>wX7oYCB9#8`ka=V^$)R5otFiKa z1eyS;Ee5AewlC=ZbK}b3jmY5YT57I7u!hsL5Y`eShUNj_7L{Fz&wN=k91|IGFJc<*jInd;=4ZGkxV94F$ zDC0G@#Ee4S@5QFGr985ZWuz}kfT%NLDZudEQw5uNUadj*3lT-h#{h?od4tO(Hl}r| zUVF~@PlXsCXC!;!4tfE zr1OL>i+g&YT-K&H<4Az@@xEaKWX{a9PCRV@7CG~XvMsF8<25Y7HtNU@U{7L$hT{^< zO1V&1aG%HtEeU%t+0(YSI;7$%9D$_JZ)(ThTSugKdwUBLrUeEJH4+J($MeN9SL>tX z`yTnWD@M-RWL^3@tLvvEc=tj3QcF^!amE}{i&IU!<484f_K$C9`$>VwtEPxBM2W4)og75(MUd9$-pPTIWBWcKcOT?m$bbAIu-74q3Wa^bY* z)Na}EA}+SQ+qO38i7rrCR$w^4?YD+S$pWDyD(!M?h~tH50>A;kk`>=)Q*o?hkhhFT=bsT>Wx>H_JZyO4YC9;fGf_6$2bCT%~Y+g)%H z^Oqn6GHc!ZMHoQJ_w_=tbh&+u0y*EgpKE4OQi%QX?*NoCuRbzUQ2(V+0HCU>2pev* zpsYIc(yyHpd7sjlNt>V=-69wzWdelH0j*}lCPttHN_-^ib%sOPSFxbG}K ztx3vdZgBJo73g0+la}x#&x6^{P(}1PT`%E%eY14g`QT)GUVwDLN#cESbS~^d>_^E9 z{fDRePX{5~>7Om*nnj4&9d3_7PpwF&d%3VW91L@6oXO-0nc7+)PU zvbwSW2Q#PZY)^{`yxHoK?kOSVF)|oD6V{{@)xGb0#6mqJZLG3q6>1z0$sf;j2}ppH z`}(d8!o{~P_j~Zj7$ZsJwvlEzZnHkd0J!Ly4!*-E$gld%tA!JAN}*b&AP6 z0YaGG;n~}is=`DGibA~Uqqa)_g@ZkSADh!O)^@c!?_;HLz%8h@^x;w9OE_THfH4mivwTU?26t$Qu_{Dp7 z0TAM47hZ|K{zP&j_zOmBUDu}hUX;W%ulOs1c2V^OHofx~9ec$CLMH~u===)D&|VYD zj#rac?@L$Om+4+b;k?HySqLW;W_o6L#O2wwH4Em)Vt^X|8mzl-}wi{SX^(^n!>EjCatK9l@Tqgmfh0;PjT87X<;#xuwiB^Xn2B*4 z-6t0+AVm{&u^hhh{>p!OHAD4?@ShdTL|{|279SDUe{Px;X>JqS5J_|w9hC$HMqUE~ z5!NoD4I3-JI@jT)8ov&j5zxd(FJ;!#NX1D9p7}3ham?V&js+Y=!ckibNRwmZ=Zwl? zE99Sjxq^&3h#HO(11LE^cpaojkBA9!O9fKW^MZq&QyoCD%!}`KUD2DXOp;CfX0wR0 z+f?NvJ+K)S*}(N+(7w#n?>;BTM~U=Wmx+PNkn*Wecqc~E5m9%t*`m;rYU|$<)5`;0`PMjp?+Zd@?d%D~z)#K&fEc&l^d`$R4_e#y+ zNlq^?{F4kh4a)AMq0);F0mm?PWmmuB{Cr7SBBT8sNaENxzm|V&I8PFa{l3AZe@N?PhWH`sD}nm^Ex89RENkN82CrYX~vmO-*O00s^Av9TbT)~LC_y$f4tlQe-HKqT(>tPrW#MXri!g1OhmMZR7 zrZ8@hNJ(34FH(VJx^5zV_X!;k61c3Ml?0u|R{hnx0M*F8iH>(9hYX}Oss#0%mx4B` zxw@`7-J4%hQ#YrY_-3S1k6+n06)k)cyo|@GMo_9aYuj4EOSfO|zSDt~OOmkB*C9IF zx*@8;1ql_!oW#VuA!duudt?9PA6~2-H$NQRQbAP9?U@u(Ix)k0CYGAnz6B6!{cvt8 zeElBTIt-{z|0}d?DCw(5d2PVLNGRw^{{@d5eWfPqq#1g(JEQy9AofZI05MZl%?kD| zj^Fioqiy@8>=B6f9?zU3pRUGP6mC5`hO2U|_0bxjr9Z4y>6%ABG){)qcRZjZm_bh7yaEiTE9^hqO8s!`;(eF`3FlQmA) z)v4$po&hFPsB5Ln>s5(Hxw+@{^l>Ia+0|wr z?TwR)l1@-4{2Zp1)G&5av@DP&)x&hWhHQ{r`@j*YcM~OtJOW#jaggJ?Vns$;ln$z( zBfsW;Oi)EIE${-Oc+`aPPi{Wo!UBw{GbPC(%IOv%o;htUHQwh#IE&ulY6&FJpj3PJ ztu#ZDkO&h_yQKS7((OK#I|M2!~E~8la~zL;cSegL_P5rM$|l z7x-9yrz)JHW*!7QU%E@facUrVt#I(tu@9qk5cY5v^GNBQMJ9Tt%Oj+9eoKo7H;q(R+CjX2dwzROIMg6ezSGSU|>Bl4;?FHo}EU@!W zLg7cyMiQtn0ysSqESNWm_6(dX>s@SG(Ah5;_0}BMx;OeT*l0uqU@;?feev$8EVyg@ z93jOVbU&gxyfsqKF-W?C>Nci|@HRAcc~>pxyNPmo$OEf&;gFcq5YWs3i#R2p9Z{gk z{;YCC%gehz2AeB%kyPU&M!0A6Yjoe^ygIm6<>-680Ycm~x2U-0KXb$tZWV2=#Vv~1 zy_cO34qfW)Tky`;x0p;z4U{vf{ufWrybFuk#o%+em2C8uodG2$twf{-8egn(fVoPQ z9Mwp_GRY1XW#GhQ`tJ}p-yCQ1U3>3NjJVpj{~7Ypm{K13&Cr~h{a3=R;`@oIyZ!g; zzlRUoZ`-(*wb=a&Mf?wSTLw}krWadXIer%x-6UE=zY5;WJc81kThoG{7RL@7pr2+4 z6&5ObCS{Kx@6t#Zetrr-RONl7;e44=P25Nq@whfv%px@C�oOOdlTO`^0!D7ISz# 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%0AZzp#I6y3b%v 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 P;7?IjMW#y1BIxh#-QbB1m_Kq5^_+cS*-=&be#N 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%0A>)p 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 SNqD8@rgIwF>N#lhyZ-@;UImu` diff --git a/lovelace_example/zaptec-home.png b/lovelace_example/zaptec-home.png deleted file mode 100644 index 135db44a8a4ba8d1baaa2b62426ea7fe5a2596b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1419510 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 DqTDY}_eheham(qNzVBKdoxa1W?9M!Yf- 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 A?*IS* diff --git a/lovelace_example/zaptec_home_api_data.txt b/lovelace_example/zaptec_home_api_data.txt deleted file mode 100644 index c838f72..0000000 --- a/lovelace_example/zaptec_home_api_data.txt +++ /dev/null @@ -1,1347 +0,0 @@ -### ZAPTEC DOCUMENTATION - -### /api/constants - -{ - "Languages": [ - { - "Key": "nl", - "Name": "Dutch" - }, - { - "Key": "en", - "Name": "English" - }, - { - "Key": "fi", - "Name": "Finnish" - }, - { - "Key": "fr", - "Name": "French" - }, - { - "Key": "de", - "Name": "German" - }, - { - "Key": "ch", - "Name": "German (Switzerland)" - }, - { - "Key": "it", - "Name": "Italian" - }, - { - "Key": "no", - "Name": "Norwegian" - }, - { - "Key": "pl", - "Name": "Polish" - }, - { - "Key": "pt", - "Name": "Portuguese" - }, - { - "Key": "es", - "Name": "Spanish" - }, - { - "Key": "se", - "Name": "Swedish" - } - ], - "Countries": { - "d6267571-6cf4-4061-92d8-f9d55a4f3a40": { - "Id": "d6267571-6cf4-4061-92d8-f9d55a4f3a40", - "Code": "AUT", - "Name": "Austria", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "3073c0bd-9f5c-4f18-80dd-a7b163beb3fe": { - "Id": "3073c0bd-9f5c-4f18-80dd-a7b163beb3fe", - "Code": "BEL", - "Name": "Belgium", - "TimeZoneName": "(UTC+01:00) Central European Time (Paris)" - }, - "a33ba41a-16e4-4b00-a2a1-4934d89c3a1c": { - "Id": "a33ba41a-16e4-4b00-a2a1-4934d89c3a1c", - "Code": "HRV", - "Name": "Croatia", - "TimeZoneName": "(UTC+01:00) Central European Time (Warsaw)" - }, - "52f1e37e-f314-4d48-8a6a-82c74e4830a1": { - "Id": "52f1e37e-f314-4d48-8a6a-82c74e4830a1", - "Code": "DNK", - "Name": "Denmark", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "0047a9bd-adea-4272-9708-3354dee0bd8c": { - "Id": "0047a9bd-adea-4272-9708-3354dee0bd8c", - "Code": "EST", - "Name": "Estonia", - "TimeZoneName": "(UTC+02:00) Eastern European Time (Tallinn)" - }, - "42ec863c-edb5-46b0-afeb-36f66fa7eebf": { - "Id": "42ec863c-edb5-46b0-afeb-36f66fa7eebf", - "Code": "FIN", - "Name": "Finland", - "TimeZoneName": "(UTC+02:00) Eastern European Time (Kyiv)" - }, - "c66b0918-9e65-4dd4-9b6c-47f989538e6d": { - "Id": "c66b0918-9e65-4dd4-9b6c-47f989538e6d", - "Code": "FRA", - "Name": "France", - "TimeZoneName": "(UTC+01:00) Central European Time (Paris)" - }, - "fb894235-7a19-4fe0-b6a1-8b9c9ceb9e4e": { - "Id": "fb894235-7a19-4fe0-b6a1-8b9c9ceb9e4e", - "Code": "DEU", - "Name": "Germany", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "a0e21e29-474f-466a-9a2f-3a17458465d8": { - "Id": "a0e21e29-474f-466a-9a2f-3a17458465d8", - "Code": "ISL", - "Name": "Iceland", - "TimeZoneName": "(UTC+00:00) Iceland Time" - }, - "04918e62-f761-4ef2-8923-1483fa9fdd05": { - "Id": "04918e62-f761-4ef2-8923-1483fa9fdd05", - "Code": "IRL", - "Name": "Ireland", - "TimeZoneName": "(UTC+00:00) United Kingdom Time" - }, - "2365ed9c-8a79-44b5-9bf8-f7a2cf42fb44": { - "Id": "2365ed9c-8a79-44b5-9bf8-f7a2cf42fb44", - "Code": "ITA", - "Name": "Italy", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "51f14b70-8cfb-4f43-b389-a4966e225ddd": { - "Id": "51f14b70-8cfb-4f43-b389-a4966e225ddd", - "Code": "LVA", - "Name": "Latvia", - "TimeZoneName": "(UTC+02:00) Eastern European Time (Riga)" - }, - "8022f9e8-d2a7-4446-a248-bcb25ce6d0e2": { - "Id": "8022f9e8-d2a7-4446-a248-bcb25ce6d0e2", - "Code": "LTU", - "Name": "Lithuania", - "TimeZoneName": "(UTC+02:00) Eastern European Time (Vilnius)" - }, - "d7158bfc-ab96-4b85-88fb-26740ab8f17c": { - "Id": "d7158bfc-ab96-4b85-88fb-26740ab8f17c", - "Code": "LUX", - "Name": "Luxembourg", - "TimeZoneName": "(UTC+01:00) Central European Time (Warsaw)" - }, - "bda681ab-adcb-4f67-bac5-5cbf28d42cc7": { - "Id": "bda681ab-adcb-4f67-bac5-5cbf28d42cc7", - "Code": "NLD", - "Name": "Netherlands", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "83bffdb1-0a92-4574-bb7b-bd0d17387c17": { - "Id": "83bffdb1-0a92-4574-bb7b-bd0d17387c17", - "Code": "NOR", - "Name": "Norway", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "758c174f-d840-4b80-a408-1d46ce2b7e05": { - "Id": "758c174f-d840-4b80-a408-1d46ce2b7e05", - "Code": "POL", - "Name": "Poland", - "TimeZoneName": "(UTC+01:00) Central European Time (Warsaw)" - }, - "53215e81-d4a1-48ab-bfa3-001934b71734": { - "Id": "53215e81-d4a1-48ab-bfa3-001934b71734", - "Code": "PRT", - "Name": "Portugal", - "TimeZoneName": "(UTC+00:00) United Kingdom Time" - }, - "5456f813-850b-418f-9eef-1225e66302d2": { - "Id": "5456f813-850b-418f-9eef-1225e66302d2", - "Code": "ESP", - "Name": "Spain", - "TimeZoneName": "(UTC+01:00) Central European Time (Paris)" - }, - "7ededdc0-d2d3-4daf-8d93-37f998de07b1": { - "Id": "7ededdc0-d2d3-4daf-8d93-37f998de07b1", - "Code": "SWE", - "Name": "Sweden", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "3705d443-31df-4633-8bf4-b8379c82a5cb": { - "Id": "3705d443-31df-4633-8bf4-b8379c82a5cb", - "Code": "CHE", - "Name": "Switzerland", - "TimeZoneName": "(UTC+01:00) Central European Time (Berlin)" - }, - "8e2a76e1-2f43-46bc-9319-0a77b12182e6": { - "Id": "8e2a76e1-2f43-46bc-9319-0a77b12182e6", - "Code": "THA", - "Name": "Thailand", - "TimeZoneName": "(UTC+07:00) Indochina Time (Bangkok)" - }, - "f743b0d4-1f38-43e8-a95d-cf18fe1c3ede": { - "Id": "f743b0d4-1f38-43e8-a95d-cf18fe1c3ede", - "Code": "GBR", - "Name": "United Kingdom", - "TimeZoneName": "(UTC+00:00) United Kingdom Time" - } - }, - "InstallationCategories": [ - { - "Id": "5c624162-e595-4167-a8bb-8b33a1487b62", - "Category": "Community_Installation_Category" - }, - { - "Id": "c43d09c7-b734-4319-af16-9e8fac37d7ec", - "Category": "Company_Installation_Category" - }, - { - "Id": "d72d6374-7f73-4df5-8056-60635b177421", - "Category": "Private_Installation_Category" - }, - { - "Id": "08814e5f-bd84-45cd-9e0e-d225c8d675e1", - "Category": "Public_Installation_Category" - } - ], - "InstallationTypes": { - "Pro": { - "Id": 0, - "Name": "Pro", - "DefaultFeatures": 183, - "DefaultRoute": "default" - }, - "Smart": { - "Id": 1, - "Name": "Smart", - "MaxCircuits": 1, - "MaxCircuitCurrent": 32.0, - "MaxChargers": 3, - "DefaultFeatures": 471, - "DefaultRoute": "default" - } - }, - "UserRoles": { - "None": 0, - "User": 1, - "Owner": 2, - "Maintainer": 4, - "Administrator": 8, - "Any": 15, - "Onboarding": 16, - "DeviceAdministrator": 32, - "PartnerAdministrator": 64, - "Technical": 128, - "InternalData": 256 - }, - "NetworkTypes": { - "Unknown": 0, - "IT_1_Phase": 1, - "IT_3_Phase": 2, - "TN_1_Phase": 3, - "TN_3_Phase": 4 - }, - "ChargerOperationModes": { - "Unknown": 0, - "Disconnected": 1, - "Connected_Requesting": 2, - "Connected_Charging": 3, - "Connected_Finished": 5 - }, - "Phases": { - "None": 0, - "Phase_1": 1, - "Phase_2": 2, - "Phase_3": 4, - "All": 7 - }, - "WildcardGuid": "ffffffff-ffff-ffff-ffff-ffffffffffff", - "RegionalInfo": { - "GBR": { - "RegionName": "GBR", - "Name": "GB", - "NativeName": "United Kingdom", - "CurrencySymbol": "£", - "CurrencyName": "British Pound", - "CurrencyNativeName": "British Pound", - "ISOCurrencySymbol": "GBP" - }, - "NOR": { - "RegionName": "NOR", - "Name": "NO", - "NativeName": "Norway", - "CurrencySymbol": "NOK", - "CurrencyName": "Norwegian Krone", - "CurrencyNativeName": "Norwegian Krone", - "ISOCurrencySymbol": "NOK" - }, - "CHE": { - "RegionName": "CHE", - "Name": "CH", - "NativeName": "Switzerland", - "CurrencySymbol": "CHF", - "CurrencyName": "Swiss Franc", - "CurrencyNativeName": "Swiss Franc", - "ISOCurrencySymbol": "CHF" - } - }, - "MessageCodes": { - "Success": 0, - "Error": 1, - "Information": 2, - "Warning": 3, - "KnownErrors": 500, - "UnknownObject": 501 - }, - "ErrorCodes": { - "Unknown": 500, - "MissingRequiredData": 503, - "UnknownSetting": 504, - "OperationFailedForUnknownReasons": 505, - "NotApplicableForUser": 506, - "UnknownUser": 507, - "RfidTokenInUse": 508, - "SignUpTooManyRequests": 509, - "EmailInUse": 510, - "CellPhoneInUse": 511, - "UnknownObject": 512, - "InvalidPassword": 513, - "IncorrectPassword": 514, - "UserActivationLinkExpired": 515, - "LinkRequestExpired": 516, - "ChargerDeviceIdExists": 517, - "UnknownDeviceId": 518, - "UnknownCommand": 519, - "ErrorCommunicatingWithDevice": 520, - "StringIsNotAWellFormedVersion": 521, - "FirmwareVersionExists": 522, - "FirmwareFileExists": 523, - "CreateConflict": 524, - "DeviceFirmwareNotConfigured": 525, - "FeatureNotEnabled": 526, - "NotSupported": 527, - "DeviceCommandRejected": 528, - "InvalidFormat": 529, - "MailSendFailed": 530, - "ConcurrencyError": 531, - "ConfigurationError": 532, - "Forbidden": 533, - "InstallationTypeViolation": 534, - "PaymentFailed": 535, - "PaymentAuthorizationRequired": 536, - "OperationFailedActiveSubscriptions": 537, - "OperationFailedDueToChargerState": 538, - "InstallationConstraintViolation": 539, - "UnknownInstallationId": 540, - "UnknownEnergySensorId": 541 - }, - "Settings": { - "AuthenticationRequired": 120, - "PaymentActive": 130, - "PaymentCurrency": 131, - "PaymentSessionUnitPrice": 132, - "PaymentEnergyUnitPrice": 133, - "PaymentTimeUnitPrice": 134, - "CommunicationMode": 150, - "PermanentCableLock": 151, - "HmiBrightness": 153, - "LockCableWhenConnected": 154, - "SoftStartDisabled": 155, - "MIDBlinkEnabled": 170, - "CurrentInMaximum": 510, - "CurrentInMinimum": 511, - "MaxPhases": 520, - "DefaultOfflinePhase": 522, - "DefaultOfflineCurrent": 523, - "SignedMeterValueInterval": 555, - "ChargeCurrentInstallationMaxLimit": 707, - "IsEnabled": 711, - "Standalone": 712, - "NetworkType": 715, - "EnabledNfcTechnologies": 752, - "LteRoamingDisabled": 753, - "InstallationId": 800, - "RoutingId": 801, - "ChargePointName": 802, - "DiagnosticsMode": 805, - "DisableBLEChargePointName": 806, - "InternalDiagnosticsLog": 807, - "UnconditionalNfcDetectionIndication": 855, - "EnableLteDetailedSignalStrength": 856 - }, - "Commands": { - "Unknown": 0, - "InChargePingReply": 1, - "OfflineModeOverride": 2, - "RestartCharger": 102, - "RestartMcu": 103, - "UpdateSettings": 104, - "RestartNtp": 105, - "ExitAppWithCode": 106, - "UpgradeFirmware": 200, - "UpgradeFirmwareForced": 201, - "ResetComErrors": 260, - "ResetNotifications": 261, - "ResetComWarnings": 262, - "LocalSettings": 300, - "SetPlcNpw": 320, - "SetPlcCCoMode": 321, - "SetPlcNmk": 322, - "SetRemotePlcNmk": 323, - "SetRemotePlcNpw": 324, - "StartCharging": 501, - "StopCharging": 502, - "ReportChargingState": 503, - "SetSessionId": 504, - "SetUserUuid": 505, - "StopChargingFinal": 506, - "ResumeCharging": 507, - "ShowGranted": 601, - "ShowDenied": 602, - "IndicateAppConnect": 603, - "ConfirmChargeCardAdded": 750, - "SetAuthenticationList": 751, - "Debug": 800, - "GetPlcTopology": 801, - "ResetPlc": 802, - "RemoteCommand": 803, - "RunGridTest": 804, - "ClearObservationCache": 805, - "RunPostProductionTest": 901, - "GetFirmwareVersion": 902, - "DumpPilotCounter": 950, - "RunPilotTest": 951, - "CombinedMin": 10000, - "DeauthorizeAndStop": 10001, - "CombinedMax": 10999 - }, - "Observations": { - "Unknown": 0, - "OfflineMode": 1, - "AuthenticationRequired": 120, - "PaymentActive": 130, - "PaymentCurrency": 131, - "PaymentSessionUnitPrice": 132, - "PaymentEnergyUnitPrice": 133, - "PaymentTimeUnitPrice": 134, - "CommunicationMode": 150, - "PermanentCableLock": 151, - "ProductCode": 152, - "HmiBrightness": 153, - "LockCableWhenConnected": 154, - "SoftStartDisabled": 155, - "FirmwareApiHost": 156, - "MIDBlinkEnabled": 170, - "ProductionTesterEnabled": 180, - "ProductionTestStationOverride": 181, - "TemperatureInternal5": 201, - "TemperatureInternal6": 202, - "TemperatureInternalLimit": 203, - "TemperaturePowerBoard": 205, - "TemperatureInternalMaxLimit": 241, - "Humidity": 270, - "TamperCover": 280, - "VoltagePhase1": 501, - "VoltagePhase2": 502, - "VoltagePhase3": 503, - "CurrentPhase1": 507, - "CurrentPhase2": 508, - "CurrentPhase3": 509, - "ChargerMaxCurrent": 510, - "ChargerMinCurrent": 511, - "ActivePhases": 512, - "TotalChargePower": 513, - "RcdCurrent": 515, - "Internal12vCurrent": 517, - "PowerFactor": 518, - "SetPhases": 519, - "MaxPhases": 520, - "ChargerOfflinePhase": 522, - "ChargerOfflineCurrent": 523, - "RcdCalibration": 540, - "RcdCalibrationNoise": 541, - "ManualRcdTest": 542, - "TotalChargePowerSession": 553, - "SignedMeterValue": 554, - "SignedMeterValueInterval": 555, - "SessionEnergyCountExportActive": 560, - "SessionEnergyCountExportReactive": 561, - "SessionEnergyCountImportActive": 562, - "SessionEnergyCountImportReactive": 563, - "SoftStartTime": 570, - "ChargeDuration": 701, - "ChargeMode": 702, - "ChargePilotLevelInstant": 703, - "ChargePilotLevelAverage": 704, - "PilotVsProximityTime": 706, - "ChargeCurrentInstallationMaxLimit": 707, - "ChargeCurrentSet": 708, - "ChargerOperationMode": 710, - "IsEnabled": 711, - "IsStandAlone": 712, - "ChargerCurrentUserUuidDeprecated": 713, - "CableType": 714, - "NetworkType": 715, - "DetectedCar": 716, - "GridTestResult": 717, - "FinalStopActive": 718, - "SessionIdentifier": 721, - "ChargerCurrentUserUuid": 722, - "CompletedSession": 723, - "PlugAndChargeAuthorizeRequest": 724, - "NewChargeCard": 750, - "AuthenticationListVersion": 751, - "EnabledNfcTechnologies": 752, - "LteRoamingDisabled": 753, - "Location": 760, - "TimeZone": 761, - "TimeSchedule": 762, - "NextScheduleEvent": 763, - "MaxStartDelay": 764, - "InstallationId": 800, - "RoutingId": 801, - "Notifications": 803, - "Warnings": 804, - "DiagnosticsMode": 805, - "InternalDiagnosticsLog": 807, - "DiagnosticsString": 808, - "CommunicationSignalStrength": 809, - "CloudConnectionStatus": 810, - "McuResetSource": 811, - "McuRxErrors": 812, - "McuToVariscitePacketErrors": 813, - "VarisciteToMcuPacketErrors": 814, - "UptimeVariscite": 820, - "UptimeMCU": 821, - "SecurityLog": 830, - "CarSessionLog": 850, - "CommunicationModeConfigurationInconsistency": 851, - "RawPilotMonitor": 852, - "IT3PhaseDiagnosticsLog": 853, - "PilotTestResults": 854, - "UnconditionalNfcDetectionIndication": 855, - "EnableLteDetailedSignalStrength": 856, - "EmcTestCounter": 899, - "ProductionTestResults": 900, - "PostProductionTestResults": 901, - "SmartMainboardSoftwareApplicationVersion": 908, - "SmartMainboardSoftwareBootloaderVersion": 909, - "SmartComputerSoftwareApplicationVersion": 911, - "SmartComputerSoftwareBootloaderVersion": 912, - "SmartComputerHardwareVersion": 913, - "MacMain": 950, - "MacPlcModuleGrid": 951, - "MacWiFi": 952, - "MacPlcModuleEv": 953, - "LteImsi": 960, - "LteMsisdn": 961, - "LteIccid": 962, - "LteImei": 963, - "LteVersion": 964, - "LteDetailedSignalStrength": 965, - "ProductionTestStationNumber": 970, - "MIDCalibration": 980, - "IsOcppConnected": -3, - "IsOnline": -2, - "Pulse": -1 - }, - "Schema": { - "Smart": { - "DeviceType": 1, - "ObservationIds": { - "Unknown": 0, - "OfflineMode": 1, - "AuthenticationRequired": 120, - "PaymentActive": 130, - "PaymentCurrency": 131, - "PaymentSessionUnitPrice": 132, - "PaymentEnergyUnitPrice": 133, - "PaymentTimeUnitPrice": 134, - "CommunicationMode": 150, - "PermanentCableLock": 151, - "ProductCode": 152, - "HmiBrightness": 153, - "LockCableWhenConnected": 154, - "SoftStartDisabled": 155, - "FirmwareApiHost": 156, - "MIDBlinkEnabled": 170, - "ProductionTesterEnabled": 180, - "ProductionTestStationOverride": 181, - "TemperatureInternal5": 201, - "TemperatureInternal6": 202, - "TemperatureInternalLimit": 203, - "TemperaturePowerBoard": 205, - "TemperatureInternalMaxLimit": 241, - "Humidity": 270, - "TamperCover": 280, - "VoltagePhase1": 501, - "VoltagePhase2": 502, - "VoltagePhase3": 503, - "CurrentPhase1": 507, - "CurrentPhase2": 508, - "CurrentPhase3": 509, - "ChargerMaxCurrent": 510, - "ChargerMinCurrent": 511, - "ActivePhases": 512, - "TotalChargePower": 513, - "RcdCurrent": 515, - "Internal12vCurrent": 517, - "PowerFactor": 518, - "SetPhases": 519, - "MaxPhases": 520, - "ChargerOfflinePhase": 522, - "ChargerOfflineCurrent": 523, - "RcdCalibration": 540, - "RcdCalibrationNoise": 541, - "ManualRcdTest": 542, - "TotalChargePowerSession": 553, - "SignedMeterValue": 554, - "SignedMeterValueInterval": 555, - "SessionEnergyCountExportActive": 560, - "SessionEnergyCountExportReactive": 561, - "SessionEnergyCountImportActive": 562, - "SessionEnergyCountImportReactive": 563, - "SoftStartTime": 570, - "ChargeDuration": 701, - "ChargeMode": 702, - "ChargePilotLevelInstant": 703, - "ChargePilotLevelAverage": 704, - "PilotVsProximityTime": 706, - "ChargeCurrentInstallationMaxLimit": 707, - "ChargeCurrentSet": 708, - "ChargerOperationMode": 710, - "IsEnabled": 711, - "IsStandAlone": 712, - "ChargerCurrentUserUuidDeprecated": 713, - "CableType": 714, - "NetworkType": 715, - "DetectedCar": 716, - "GridTestResult": 717, - "FinalStopActive": 718, - "SessionIdentifier": 721, - "ChargerCurrentUserUuid": 722, - "CompletedSession": 723, - "PlugAndChargeAuthorizeRequest": 724, - "NewChargeCard": 750, - "AuthenticationListVersion": 751, - "EnabledNfcTechnologies": 752, - "LteRoamingDisabled": 753, - "Location": 760, - "TimeZone": 761, - "TimeSchedule": 762, - "NextScheduleEvent": 763, - "MaxStartDelay": 764, - "InstallationId": 800, - "RoutingId": 801, - "Notifications": 803, - "Warnings": 804, - "DiagnosticsMode": 805, - "InternalDiagnosticsLog": 807, - "DiagnosticsString": 808, - "CommunicationSignalStrength": 809, - "CloudConnectionStatus": 810, - "McuResetSource": 811, - "McuRxErrors": 812, - "McuToVariscitePacketErrors": 813, - "VarisciteToMcuPacketErrors": 814, - "UptimeVariscite": 820, - "UptimeMCU": 821, - "SecurityLog": 830, - "CarSessionLog": 850, - "CommunicationModeConfigurationInconsistency": 851, - "RawPilotMonitor": 852, - "IT3PhaseDiagnosticsLog": 853, - "PilotTestResults": 854, - "UnconditionalNfcDetectionIndication": 855, - "EnableLteDetailedSignalStrength": 856, - "EmcTestCounter": 899, - "ProductionTestResults": 900, - "PostProductionTestResults": 901, - "SmartMainboardSoftwareApplicationVersion": 908, - "SmartMainboardSoftwareBootloaderVersion": 909, - "SmartComputerSoftwareApplicationVersion": 911, - "SmartComputerSoftwareBootloaderVersion": 912, - "SmartComputerHardwareVersion": 913, - "MacMain": 950, - "MacPlcModuleGrid": 951, - "MacWiFi": 952, - "MacPlcModuleEv": 953, - "LteImsi": 960, - "LteMsisdn": 961, - "LteIccid": 962, - "LteImei": 963, - "LteVersion": 964, - "LteDetailedSignalStrength": 965, - "ProductionTestStationNumber": 970, - "MIDCalibration": 980, - "IsOcppConnected": -3, - "IsOnline": -2, - "Pulse": -1 - }, - "CommandIds": { - "Unknown": 0, - "InChargePingReply": 1, - "OfflineModeOverride": 2, - "RestartCharger": 102, - "RestartMcu": 103, - "UpdateSettings": 104, - "RestartNtp": 105, - "ExitAppWithCode": 106, - "UpgradeFirmware": 200, - "UpgradeFirmwareForced": 201, - "ResetComErrors": 260, - "ResetNotifications": 261, - "ResetComWarnings": 262, - "LocalSettings": 300, - "SetPlcNpw": 320, - "SetPlcCCoMode": 321, - "SetPlcNmk": 322, - "SetRemotePlcNmk": 323, - "SetRemotePlcNpw": 324, - "StartCharging": 501, - "StopCharging": 502, - "ReportChargingState": 503, - "SetSessionId": 504, - "SetUserUuid": 505, - "StopChargingFinal": 506, - "ResumeCharging": 507, - "ShowGranted": 601, - "ShowDenied": 602, - "IndicateAppConnect": 603, - "ConfirmChargeCardAdded": 750, - "SetAuthenticationList": 751, - "Debug": 800, - "GetPlcTopology": 801, - "ResetPlc": 802, - "RemoteCommand": 803, - "RunGridTest": 804, - "ClearObservationCache": 805, - "RunPostProductionTest": 901, - "GetFirmwareVersion": 902, - "DumpPilotCounter": 950, - "RunPilotTest": 951, - "CombinedMin": 10000, - "DeauthorizeAndStop": 10001, - "CombinedMax": 10999 - }, - "SettingIds": { - "AuthenticationRequired": 120, - "PaymentActive": 130, - "PaymentCurrency": 131, - "PaymentSessionUnitPrice": 132, - "PaymentEnergyUnitPrice": 133, - "PaymentTimeUnitPrice": 134, - "CommunicationMode": 150, - "PermanentCableLock": 151, - "HmiBrightness": 153, - "LockCableWhenConnected": 154, - "SoftStartDisabled": 155, - "MIDBlinkEnabled": 170, - "CurrentInMaximum": 510, - "CurrentInMinimum": 511, - "MaxPhases": 520, - "DefaultOfflinePhase": 522, - "DefaultOfflineCurrent": 523, - "SignedMeterValueInterval": 555, - "ChargeCurrentInstallationMaxLimit": 707, - "IsEnabled": 711, - "Standalone": 712, - "NetworkType": 715, - "EnabledNfcTechnologies": 752, - "LteRoamingDisabled": 753, - "InstallationId": 800, - "RoutingId": 801, - "ChargePointName": 802, - "DiagnosticsMode": 805, - "DisableBLEChargePointName": 806, - "InternalDiagnosticsLog": 807, - "UnconditionalNfcDetectionIndication": 855, - "EnableLteDetailedSignalStrength": 856 - }, - "Warnings": { - "None": 0, - "HUMIDITY": 1, - "TEMPERATURE": 2, - "TEMPERATURE_ERROR": 4, - "EMETER_NO_RESPONSE": 8, - "MAX_SESSION_RESTART": 16, - "CHARGE_OVERCURRENT": 32, - "PILOT_STATE": 64, - "RELAY_WELDED": 128, - "PILOT_LOW_LEVEL": 256, - "FPGA_COM_TIMEOUT": 512, - "REBOOT": 1024, - "DISABLED": 2048, - "RCD_AC": 4096, - "RCD_DC": 8192, - "RCD_PEAK": 16384, - "INVALID_HARDWARE_ID": 32768, - "RCD_TEST_AC": 65536, - "RCD_TEST_DC": 131072, - "RCD_FAILURE": 262144, - "RCD_TEST_TIMEOUT": 524288, - "FPGA_VERSION": 1048576, - "FPGA_UNEXPECTED_RELAY": 2097152, - "FPGA_CHARGING_RESET": 4194304, - "PILOT_NO_PROXIMITY": 8388608, - "EMETER_ALARM": 16777216, - "EMETER_LINK": 33554432, - "NO_VOLTAGE_L1": 67108864, - "NO_VOLTAGE_L2_L3": 134217728, - "FPGA_WATCHDOG": 268435456, - "EMETER_CAL": 536870912, - "MID": 1073741824, - "VARISCITE": 2147483648, - "MCU_BOOTLOADER": 4294967296, - "FPGA_INIT_FAILED": 8589934592, - "ILLEGAL_PHASE": 17179869184 - } - }, - "Apollo": { - "DeviceType": 4, - "ObservationIds": { - "Unknown": 0, - "AuthenticationRequired": 120, - "PaymentActive": 130, - "PaymentCurrency": 131, - "PaymentSessionUnitPrice": 132, - "PaymentEnergyUnitPrice": 133, - "PaymentTimeUnitPrice": 134, - "TransmitInterval": 145, - "TransmitChangeLevel": 146, - "PulseInterval": 147, - "CommunicationMode": 150, - "PermanentCableLock": 151, - "ProductCode": 152, - "HmiBrightness": 153, - "LockCableWhenConnected": 154, - "TemperatureInternal5": 201, - "TemperatureEMeterPhase1": 202, - "TemperatureEMeterPhase2": 204, - "TemperatureEMeterPhase3": 205, - "TemperatureTM": 206, - "TemperatureTM2": 207, - "TemperatureInternalMaxLimit": 241, - "Humidity": 270, - "TamperCover": 280, - "VoltagePhase1": 501, - "VoltagePhase2": 502, - "VoltagePhase3": 503, - "CurrentPhase1": 507, - "CurrentPhase2": 508, - "CurrentPhase3": 509, - "ChargerMaxCurrent": 510, - "ChargerMinCurrent": 511, - "ActivePhases": 512, - "TotalChargePower": 513, - "RcdCurrent": 515, - "Internal12vVoltage": 516, - "Internal12vCurrent": 517, - "PowerFactor": 518, - "SetPhases": 519, - "MaxPhases": 520, - "ChargerOfflinePhase": 522, - "ChargerOfflineCurrent": 523, - "VoltageBeforeRelay": 526, - "MaxCurrentConfigurationSource": 544, - "SwitchPosition": 545, - "ChargeCurrentInstallationMaxLimit": 546, - "StandAloneCurrent": 547, - "PhaseRotation": 548, - "TotalChargePowerSession": 553, - "SignedMeterValue": 554, - "SignedMeterValueInterval": 555, - "SessionEnergyCountExportActive": 560, - "SessionEnergyCountExportReactive": 561, - "SessionEnergyCountImportActive": 562, - "SessionEnergyCountImportReactive": 563, - "ChargeDuration": 701, - "ChargeMode": 702, - "ChargePilotLevelInstant": 703, - "ChargePilotLevelAverage": 704, - "PilotVsProximityTime": 706, - "ChargeCurrentSet": 708, - "ChargerOperationMode": 710, - "IsEnabled": 711, - "IsStandAlone": 712, - "CableType": 714, - "NetworkType": 715, - "DetectedCar": 716, - "GridTestResult": 717, - "SessionIdentifier": 721, - "ChargerCurrentUserUuid": 722, - "CompletedSession": 723, - "OverrideNetworkType": 733, - "IT3OptimizationEnabled": 734, - "NrOfChargeCards": 749, - "NewChargeCard": 750, - "AuthenticationListVersion": 751, - "Location": 760, - "TimeZone": 761, - "TimeSchedule": 762, - "NextScheduleEvent": 763, - "MaxStartDelay": 764, - "InstallationId": 800, - "RoutingId": 801, - "ChargePointName": 802, - "Notifications": 803, - "Warnings": 804, - "DiagnosticsMode": 805, - "InternalDiagnosticsLog": 807, - "DiagnosticsString": 808, - "CommunicationSignalStrength": 809, - "CloudConnectionStatus": 810, - "McuResetSource": 811, - "McuRxErrors": 812, - "McuToESPPacketErrors": 813, - "ESPToMcuPacketErrors": 814, - "ESPResetSource": 815, - "UptimeESP": 820, - "UptimeMCU": 821, - "DataUsage": 822, - "CertificateVersion": 823, - "SecurityLog": 830, - "CarSessionLog": 850, - "CommunicationModeConfigurationInconsistency": 851, - "RawPilotMonitor": 852, - "IT3PhaseDiagnosticsLog": 853, - "ProductionTestResults": 900, - "PostProductionTestResults": 901, - "SmartMainboardSoftwareApplicationVersion": 908, - "SmartComputerSoftwareApplicationVersion": 911, - "SmartComputerSoftwareBootloaderVersion": 912, - "SmartComputerHardwareVersion": 913, - "SourceVersion": 916, - "HwIdMainBoard": 917, - "HwIdPowerBoard": 918, - "MacWiFi": 952, - "LteImsi": 960, - "LteMsisdn": 961, - "LteIccid": 962, - "LteImei": 963, - "FactoryTestStage": 970, - "IsOcppConnected": -3, - "IsOnline": -2, - "Pulse": -1 - }, - "CommandIds": { - "Unknown": 0, - "RestartCharger": 102, - "RestartMcu": 103, - "UpdateSettings": 104, - "RestartNtp": 105, - "UpgradeFirmware": 200, - "UpgradeFirmwareForced": 201, - "RollbackFirmware": 202, - "ResetComErrors": 260, - "ResetNotifications": 261, - "ResetComWarnings": 262, - "LocalSettings": 300, - "StartCharging": 501, - "StopCharging": 502, - "ReportChargingState": 503, - "SetSessionId": 504, - "SetUserUuid": 505, - "StopChargingFinal": 506, - "ResumeCharging": 507, - "ShowGranted": 601, - "ShowDenied": 602, - "IndicateAppConnect": 603, - "ConfirmChargeCardAdded": 750, - "SetAuthenticationList": 751, - "Debug": 800, - "SetLoggingInterval": 801, - "SetPulseInterval": 802, - "RemoteCommand": 803, - "RunGridTest": 804, - "RunPostProductionTest": 901, - "GetFirmwareVersion": 902, - "CombinedMin": 10000, - "DeauthorizeAndStop": 10001, - "CombinedMax": 10999 - }, - "SettingIds": { - "AuthenticationRequired": 120, - "CommunicationMode": 150, - "PermanentCableLock": 151, - "HmiBrightness": 153, - "LockCableWhenConnected": 154, - "CurrentInMaximum": 510, - "CurrentInMinimum": 511, - "MaxPhases": 520, - "DefaultOfflinePhase": 522, - "DefaultOfflineCurrent": 523, - "SignedMeterValueInterval": 555, - "ChargeCurrentInstallationMaxLimit": 707, - "IsEnabled": 711, - "Standalone": 712, - "NetworkType": 715, - "InstallationId": 800, - "RoutingId": 801, - "ChargePointName": 802, - "DiagnosticsMode": 805, - "DisableBLEChargePointName": 806, - "InternalDiagnosticsLog": 807 - }, - "Warnings": { - "None": 0, - "HUMIDITY": 1, - "TEMPERATURE": 2, - "TEMPERATURE_ERROR": 4, - "EMETER_NO_RESPONSE": 8, - "MAX_SESSION_RESTART": 16, - "CHARGE_OVERCURRENT": 32, - "PILOT_STATE": 64, - "PILOT_LOW_LEVEL": 256, - "NO_SWITCH_POW_DEF": 512, - "REBOOT": 1024, - "DISABLED": 2048, - "RCD_6MA": 4096, - "RCD_30MA": 8192, - "RCD_TEST_6MA": 65536, - "RCD_TEST_30MA": 131072, - "RCD_FAILURE": 262144, - "RCD_TEST_TIMEOUT": 524288, - "OVERCURRENT_INSTALLATION": 1048576, - "UNEXPECTED_RELAY": 2097152, - "O_PEN": 4194304, - "PILOT_NO_PROXIMITY": 8388608, - "EMETER_ALARM": 16777216, - "EMETER_LINK": 33554432, - "NO_VOLTAGE_L1": 67108864, - "NO_VOLTAGE_L2_L3": 134217728, - "FPGA_WATCHDOG": 268435456, - "SERVO": 536870912, - "ESP": 2147483648, - "MCU_BOOTLOADER": 4294967296, - "ILLEGAL_PHASE": 17179869184 - } - }, - "HomeApm": { - "DeviceType": 3, - "ObservationIds": { - "CommunicationMode": 150, - "TemperatureInternal5": 201, - "VoltagePhase1": 501, - "VoltagePhase2": 502, - "VoltagePhase3": 503, - "CurrentPhase1": 507, - "CurrentPhase2": 508, - "CurrentPhase3": 509, - "EnergyLevel": 521, - "Notifications": 803, - "Warnings": 804, - "DiagnosticsMode": 805, - "DiagnosticsString": 808, - "CommunicationSignalStrength": 809, - "ESPResetSource": 815, - "CertificateVersion": 823, - "ProductionTestResults": 900, - "SoftwareApplicationVersion": 911, - "HardwareVersion": 913, - "SourceVersion": 916, - "AMSMeterId": 1000, - "AMSMeterType": 1001, - "AMSObisListVersionIdentifier": 1002, - "AMSHourlyEnergyImport": 1010, - "AMSHourlyEnergyExport": 1011, - "AMSHourlyReactiveEnergyImport": 1012, - "AMSHourlyReactiveEnergyExport": 1013, - "AMSCumulativeEnergyImport": 1020, - "AMSCumulativeEnergyExport": 1021, - "AMSCumulativeReactiveEnergyImport": 1022, - "AMSCumulativeReactiveEnergyExport": 1023, - "ReadingUpdate": -500 - }, - "CommandIds": { - "Unknown": 0, - "Restart": 102, - "UpgradeFirmware": 200, - "ReportState": 503, - "Debug": 800 - } - }, - "GenericApm": { - "DeviceType": 6, - "ObservationIds": { - "CommunicationMode": 150, - "VoltagePhase1": 501, - "VoltagePhase2": 502, - "VoltagePhase3": 503, - "CurrentPhase1": 507, - "CurrentPhase2": 508, - "CurrentPhase3": 509, - "VoltageEnergyLevel": 522, - "Notifications": 803, - "Warnings": 804, - "DiagnosticsMode": 805, - "DiagnosticsString": 808, - "CommunicationSignalStrength": 809, - "ESPResetSource": 815, - "CertificateVersion": 823, - "ProductionTestResults": 900, - "SoftwareApplicationVersion": 911, - "HardwareVersion": 913, - "SourceVersion": 916, - "MeterId": 1000, - "MeterType": 1001, - "ReadingUpdate": -500 - }, - "CommandIds": { - "Unknown": 0, - "Restart": 102, - "UpgradeFirmware": 200, - "ReportState": 503, - "Debug": 800 - } - } - }, - "ObjectTypes": { - "Unknown": 0, - "Installation": 1, - "Circuit": 2, - "Charger": 3, - "User": 4, - "UserGroup": 5, - "InactiveUser": 6, - "InvitedUser": 7, - "Country": 8 - }, - "Version": "6.5.8.0", - "SmartWarnings": { - "WARNING_OK": 0, - "WARNING_HUMIDITY": 1, - "WARNING_TEMPERATURE": 2, - "WARNING_TEMPERATURE_ERROR": 4, - "WARNING_EMETER_NO_RESPONSE": 8, - "WARNING_MAX_SESSION_RESTART": 16, - "WARNING_CHARGE_OVERCURRENT": 32, - "WARNING_PILOT_STATE": 64, - "WARNING_RELAY_WELDED": 128, - "WARNING_PILOT_LOW_LEVEL": 256, - "WARNING_FPGA_COM_TIMEOUT": 512, - "WARNING_REBOOT": 1024, - "WARNING_DISABLED": 2048, - "WARNING_RCD_AC": 4096, - "WARNING_RCD_DC": 8192, - "WARNING_RCD_PEAK": 16384, - "WARNING_RCD_TEST_AC": 65536, - "WARNING_RCD_TEST_DC": 131072, - "WARNING_RCD_FAILURE": 262144, - "WARNING_RCD_TEST_TIMEOUT": 524288, - "WARNING_RCD": 1011712, - "WARNING_FPGA_VERSION": 1048576, - "WARNING_FPGA_UNEXPECTED_RELAY": 2097152, - "WARNING_FPGA_CHARGING_RESET": 4194304, - "WARNING_PILOT_NO_PROXIMITY": 8388608, - "WARNING_EMETER_ALARM": 16777216, - "WARNING_EMETER_LINK": 33554432, - "WARNING_NO_VOLTAGE_L1": 67108864, - "WARNING_NO_VOLTAGE_L2_L3": 134217728, - "WARNING_FPGA_WATCHDOG": 268435456, - "WARNING_EMETER_CAL": 536870912, - "WARNING_MID": 1073741824, - "WARNING_VARISCITE": 2147483648, - "WARNING_MCU_BOOTLOADER": 4294967296, - "WARNING_FPGA_INIT_FAILED": 8589934592, - "WARNING_VARISCITE_ILLEGAL_PHASE": 17179869184 - }, - "VarisciteWarnings": { - "WARNING_MCU_BOOTLOADER": 1, - "WARNING_FPGA_INIT_FAILED": 2, - "WARNING_VARISCITE_ILLEGAL_PHASE": 4 - }, - "PhaseIdMap": [ - { - "Id": 1, - "Phases": 1, - "NetworkFamily": "TN" - }, - { - "Id": 2, - "Phases": 2, - "NetworkFamily": "TN" - }, - { - "Id": 3, - "Phases": 4, - "NetworkFamily": "TN" - }, - { - "Id": 4, - "Phases": 7, - "NetworkFamily": "TN" - }, - { - "Id": 5, - "Phases": 4, - "NetworkFamily": "IT" - }, - { - "Id": 6, - "Phases": 2, - "NetworkFamily": "IT" - }, - { - "Id": 8, - "Phases": 1, - "NetworkFamily": "IT" - }, - { - "Id": 9, - "Phases": 7, - "NetworkFamily": "IT" - } - ], - "DeviceLogTypes": { - "OcppIn": 0, - "OcppOut": 1, - "OcppError": 2, - "OcppConnected": 3, - "OcppConnectionFailed": 4, - "OcppClientClose": 5, - "IotCommandExecuted": 6, - "IotCommandFailed": 7, - "IotCloudSettingUpdated": 8, - "SessionCommit": 9, - "OfflineSessionCommit": 10, - "AuthorizationRequest": 11, - "AuthorizationSuccess": 12, - "AuthorizationError": 13, - "AuthorizationFailed": 14 - }, - "Features": { - "None": 0, - "Api_MessageSubscription": 1, - "Authentication_Internal": 2, - "PowerManagement_Apm": 4, - "PowerManagement_EcoMode": 8, - "PowerManagement_Schedule": 16, - "PowerManagement_Apm_PowerLimit": 32, - "Connectivity_4G": 64, - "Authentication_Ocpp": 128, - "PowerManagement_Apm_Tariff_PowerLimit": 256, - "LoadBalacing": 32768 - }, - "InstallationTypeConstraints": { - "Valid": 0, - "MaxCircuits": 1, - "MaxCircuitCurrent": 2, - "MaxChargers": 4, - "UnsupportedFeature": 8, - "IncompatibleCharger": 16 - }, - "SessionCommitMetadata": { - "None": 0, - "Online": 1, - "Offline": 2, - "ReliableClock": 4, - "StoppedByRFID": 8, - "Signed": 16, - "Void": 32, - "Aborted": 64 - }, - "InstallationUpdateStatusCodes": { - "Ok": 0, - "EcoScheduleUpdated": 1 - }, - "EntityTypes": { - "Unknown": 0, - "Installation": 1, - "Charger": 2 - }, - "DeviceTypes": { - "Unknown": 0, - "Smart": 1, - "Portable": 2, - "HomeApm": 3, - "Apollo": 4, - "OtherApm": 5, - "GenericApm": 6, - "HanApm": 7 - }, - "InstallationAuthenticationType": { - "Native": 0, - "WebHooks": 1, - "Ocpp": 2 - }, - "EnergyPrices": { - "DeliveryArea": { - "Unknown": 0, - "NO1": 1, - "NO2": 2, - "NO3": 3, - "NO4": 4, - "NO5": 5, - "SE1": 6, - "SE2": 7, - "SE3": 8, - "SE4": 9, - "DK1": 10, - "DK2": 11, - "FI": 12, - "NL": 13 - }, - "PriceUnit": { - "Unsupported": 0, - "EuroPerMWh": 1 - }, - "CountryDeliveryAreas": { - "53215e81-d4a1-48ab-bfa3-001934b71734": [], - "8e2a76e1-2f43-46bc-9319-0a77b12182e6": [], - "5456f813-850b-418f-9eef-1225e66302d2": [], - "04918e62-f761-4ef2-8923-1483fa9fdd05": [], - "758c174f-d840-4b80-a408-1d46ce2b7e05": [], - "d7158bfc-ab96-4b85-88fb-26740ab8f17c": [], - "0047a9bd-adea-4272-9708-3354dee0bd8c": [], - "42ec863c-edb5-46b0-afeb-36f66fa7eebf": [ - 12 - ], - "7ededdc0-d2d3-4daf-8d93-37f998de07b1": [ - 6, - 7, - 8, - 9 - ], - "a0e21e29-474f-466a-9a2f-3a17458465d8": [], - "c66b0918-9e65-4dd4-9b6c-47f989538e6d": [], - "a33ba41a-16e4-4b00-a2a1-4934d89c3a1c": [], - "bda681ab-adcb-4f67-bac5-5cbf28d42cc7": [ - 13 - ], - "52f1e37e-f314-4d48-8a6a-82c74e4830a1": [ - 10, - 11 - ], - "fb894235-7a19-4fe0-b6a1-8b9c9ceb9e4e": [], - "51f14b70-8cfb-4f43-b389-a4966e225ddd": [], - "3073c0bd-9f5c-4f18-80dd-a7b163beb3fe": [], - "3705d443-31df-4633-8bf4-b8379c82a5cb": [], - "8022f9e8-d2a7-4446-a248-bcb25ce6d0e2": [], - "83bffdb1-0a92-4574-bb7b-bd0d17387c17": [ - 1, - 2, - 3, - 4, - 5 - ], - "f743b0d4-1f38-43e8-a95d-cf18fe1c3ede": [], - "2365ed9c-8a79-44b5-9bf8-f7a2cf42fb44": [], - "d6267571-6cf4-4061-92d8-f9d55a4f3a40": [] - } - }, - "UserActionTypes": { - "AddSense": 1 - }, - "OcppCloudUrlVersions": { - "Legacy": 0, - "Ocpp16Compliant": 1 - } -} \ No newline at end of file diff --git a/lovelace_example/zh-1.png b/lovelace_example/zh-1.png deleted file mode 100644 index 129bf61e37a7bfe815d2d83de3b2e3faabaf86cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40660 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 diff --git a/lovelace_example/zh-2.png b/lovelace_example/zh-2.png deleted file mode 100644 index 70d3e2c96c8e7218225ada12efffddab6303d4a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40660 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 r+zM1l%@BMn@G`)X5^RSb2)VwVbf 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 From 167847d8f012de1a3e01cecef7b4c83ccac920b3 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Sat, 11 Nov 2023 14:54:34 +0100 Subject: [PATCH 17/17] Bump revision to 0.7.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ custom_components/zaptec/const.py | 2 +- custom_components/zaptec/manifest.json | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0bea6ff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## 0.7.0 + +Major refactor of the component +* Objective: Update the component to the "HA way" using devices and entities +* Zaptec devices (installation, circuit and charger) use name set in Zaptec portal +* Added entities for each device. No need for using attrs and templates. +* Added support for selecting which chargers to add to zaptec +* Added support for adding an optional prefix to the device names +* Fixed adjustable charging currents, all or individual three phase. +* Added support for authorization and deauthorization of charging using the + Zaptec internal *native* authentication +* Zaptec produces energy sensors that can be used with HA energy dashboard +* Refactor services in order to make them easier to use with the service call UIs +* Hardnened the cloud connection robustness (better timeout, better data and + error handling) +* Use data update coordinator for polling entities +* Added "Download diagnostics" +* Bugfixes and documentation update diff --git a/custom_components/zaptec/const.py b/custom_components/zaptec/const.py index 33fb9dc..82e40ea 100644 --- a/custom_components/zaptec/const.py +++ b/custom_components/zaptec/const.py @@ -2,7 +2,7 @@ from __future__ import annotations NAME = "zaptec" -VERSION = "0.0.6b2" +VERSION = "0.7.0" ISSUEURL = "https://github.com/custom-components/zaptec/issues" DOMAIN = "zaptec" diff --git a/custom_components/zaptec/manifest.json b/custom_components/zaptec/manifest.json index a29145e..872b1a5 100644 --- a/custom_components/zaptec/manifest.json +++ b/custom_components/zaptec/manifest.json @@ -10,5 +10,5 @@ ], "iot_class": "cloud_polling", "requirements": ["azure-servicebus", "pydantic"], - "version": "0.0.6b2" + "version": "0.7.0" }