From 86533e3599c0ec1318e6e251968e50bada689934 Mon Sep 17 00:00:00 2001 From: Daniel Brunner Date: Mon, 13 Sep 2021 12:27:15 +0200 Subject: [PATCH] Added goe_charger --- CODEOWNERS | 1 + .../components/goe_charger/__init__.py | 91 ++++++++ .../components/goe_charger/binary_sensor.py | 69 ++++++ .../components/goe_charger/common.py | 144 ++++++++++++ .../components/goe_charger/config_flow.py | 88 +++++++ homeassistant/components/goe_charger/const.py | 3 + .../components/goe_charger/manifest.json | 15 ++ .../components/goe_charger/number.py | 106 +++++++++ .../components/goe_charger/select.py | 133 +++++++++++ .../components/goe_charger/sensor.py | 217 ++++++++++++++++++ .../components/goe_charger/strings.json | 29 +++ .../goe_charger/translations/en.json | 29 +++ homeassistant/generated/config_flows.py | 1 + 13 files changed, 926 insertions(+) create mode 100644 homeassistant/components/goe_charger/__init__.py create mode 100644 homeassistant/components/goe_charger/binary_sensor.py create mode 100644 homeassistant/components/goe_charger/common.py create mode 100644 homeassistant/components/goe_charger/config_flow.py create mode 100644 homeassistant/components/goe_charger/const.py create mode 100644 homeassistant/components/goe_charger/manifest.json create mode 100644 homeassistant/components/goe_charger/number.py create mode 100644 homeassistant/components/goe_charger/select.py create mode 100644 homeassistant/components/goe_charger/sensor.py create mode 100644 homeassistant/components/goe_charger/strings.json create mode 100644 homeassistant/components/goe_charger/translations/en.json diff --git a/CODEOWNERS b/CODEOWNERS index 1a9e4bb62f7..b1abd249966 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -192,6 +192,7 @@ homeassistant/components/github/* @timmo001 @ludeeus homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/goalzero/* @tkdrob +homeassistant/components/goe_charger/* @0xFEEDC0DE64 homeassistant/components/gogogate2/* @vangorra @bdraco homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton diff --git a/homeassistant/components/goe_charger/__init__.py b/homeassistant/components/goe_charger/__init__.py new file mode 100644 index 00000000000..83817e26e2b --- /dev/null +++ b/homeassistant/components/goe_charger/__init__.py @@ -0,0 +1,91 @@ +"""The go-e Charger integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers import device_registry + +from .const import DOMAIN +from .common import GoeChargerHub + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[str] = ["binary_sensor", "number", "select", "sensor"] + +async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the go-eCharger integration.""" + + hass.data[DOMAIN] = {} + + return True + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up go-e Charger from a config entry.""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + hub = GoeChargerHub(config_entry.data["host"]) + + try: + data = await hub.get_data(hass, ["alw","acu","adi","sse","eto","ccw","rssi","lmo","amp","fna","car","err","cbl","wh","fwv","oem","typ","tma","nrg","modelStatus","var","fhz","ust","acs","frc","psm","loc"]) + + dr = await device_registry.async_get_registry(hass) + dr.async_get_or_create( + name=data["fna"], + config_entry_id=config_entry.entry_id, + #connections={(device_registry.CONNECTION_NETWORK_MAC, "11:22:33:44:55:66")}, + identifiers={(DOMAIN, config_entry.data["serial"])}, + manufacturer=data["oem"], + model=data["typ"] + " (" + str(data["var"]) + "kW)", + #suggested_area="Kitchen", + sw_version=data["fwv"], + ) + + return data + except Exception as e: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception %s", str(e)) + return None + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="goe_charger_" + config_entry.data["serial"], + update_method=async_update_data, + update_interval=timedelta(seconds=5), + ) + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + await coordinator.async_config_entry_first_refresh() + + if coordinator.data is None: + dr = await device_registry.async_get_registry(hass) + dr.async_get_or_create( + name="go-e_Charger_" + config_entry.data["serial"], + config_entry_id=config_entry.entry_id, + #connections={(device_registry.CONNECTION_NETWORK_MAC, "11:22:33:44:55:66")}, + identifiers={(DOMAIN, config_entry.data["serial"])}, + manufacturer="", + model="", + #suggested_area="Kitchen", + sw_version="", + ) + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if not await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS): + logging.warning('unload platforms failed') + return False; + + hass.data[DOMAIN].pop(config_entry.entry_id) + + return True diff --git a/homeassistant/components/goe_charger/binary_sensor.py b/homeassistant/components/goe_charger/binary_sensor.py new file mode 100644 index 00000000000..b3adee98c74 --- /dev/null +++ b/homeassistant/components/goe_charger/binary_sensor.py @@ -0,0 +1,69 @@ +"""Platform for number integration.""" +from __future__ import annotations + +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator +from homeassistant.components.binary_sensor import BinarySensorEntity + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities) -> None: + coordinator = hass.data[DOMAIN][config_entry.entry_id] + serial = config_entry.data["serial"] + + async_add_entities([ + GoeChargerBinary(coordinator, "Allow charging", serial, "allow_charging", "alw"), + GoeChargerBinary(coordinator, "Adapter used", serial, "adapter_used", "adi"), + ]) + +class GoeChargerBinary(CoordinatorEntity, BinarySensorEntity): + """Representation of a Sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, name: str, serial: str, unique_id: str, key: str): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._name = name + self._serial = serial + self._unique_id = unique_id + self._key = key + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return the unique id of the device.""" + return "goe_charger_" + self._serial + "_" + self._unique_id + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.data is not None and self._key in self.coordinator.data + + @property + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return None if not self.available else self.coordinator.data[self._key] + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + await self.coordinator.async_request_refresh() + + @property + def device_info(self): + """Get attributes about the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + #"name": self._device.label, + #"model": self._device.device_type_name, + #"manufacturer": "Unavailable", + } diff --git a/homeassistant/components/goe_charger/common.py b/homeassistant/components/goe_charger/common.py new file mode 100644 index 00000000000..0c8e044c79b --- /dev/null +++ b/homeassistant/components/goe_charger/common.py @@ -0,0 +1,144 @@ +"""Common code go-e Charger integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +import aiohttp +import asyncio +import async_timeout +import json +import urllib.parse + +_LOGGER = logging.getLogger(__name__) + +class GoeChargerHub: + def __init__(self, host: str) -> None: + """Initialize.""" + self._host = host + + async def get_data(self, hass: HomeAssistant, keys: list[str]) -> bool: + """Get the data from the charger.""" + + url = 'http://' + self._host + '/api/status?filter=' + urllib.parse.quote_plus(','.join(keys)) + + session = async_get_clientsession(hass) + + try: + with async_timeout.timeout(10): + resp = await session.get(url) + content = await resp.text() + except asyncio.TimeoutError: + _LOGGER.warning("Request timeout") + raise TimeoutOccured(url) + except aiohttp.ClientError: + _LOGGER.warning("Request exception") + raise CannotConnect(url) + + if resp.status != 200: + _LOGGER.warning("Request invalid response %i %s", resp.status, content) + raise InvalidRespStatus(resp.status, content) + + try: + parsed = json.loads(content) + except Exception as e: # pylint: disable=broad-except + details = "Could not parse json " + str(e) + _LOGGER.warning("%s %s", details, content) + raise InvalidJson(details, content) + + if type(parsed) is not dict: + details = "json is not a dict ({})".format(type(parsed).__name__) + _LOGGER.warning("%s", details) + raise InvalidJson(details, content) + + for key in keys: + if key not in parsed: + details = key + " not set in json object" + _LOGGER.warning("%s", details) + raise InvalidJson(details, content) + + _LOGGER.debug("Data received successfully for %s!", self._host) + + return parsed + + async def set_data(self, hass: HomeAssistant, data: dict[str, Any]) -> None: + """Set data to the charger.""" + + url = 'http://' + self._host + '/api/set?' + for key, value in data.items(): + url += urllib.parse.quote_plus(key) + '=' + urllib.parse.quote_plus(json.dumps(value)) + '&' + + session = async_get_clientsession(hass) + + try: + with async_timeout.timeout(10): + resp = await session.get(url) + content = await resp.text() + except asyncio.TimeoutError: + _LOGGER.warning("Request timeout") + raise TimeoutOccured(url) + except aiohttp.ClientError: + _LOGGER.warning("Request exception") + raise CannotConnect(url) + + if resp.status != 200: + _LOGGER.warning("Request invalid response %i %s", resp.status, content) + raise InvalidRespStatus(resp.status, content) + + try: + parsed = json.loads(content) + except Exception as e: # pylint: disable=broad-except + details = "Could not parse json " + str(e) + _LOGGER.warning("%s %s", details, content) + raise InvalidJson(details, content) + + if type(parsed) is not dict: + details = "json is not a dict ({})".format(type(parsed).__name__) + _LOGGER.warning("%s", details) + raise InvalidJson(details, content) + + for key in data: + if key not in parsed: + details = key + " not set in json object" + _LOGGER.warning("%s", details) + raise InvalidJson(details, content) + + if parsed[key] != True: + details = key + parsed[key] + _LOGGER.warning("%s", details) + raise InvalidJson(details, content) + + _LOGGER.debug("Data set successfully for %s!", self._host) + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + def __init__(self, url: str) -> None: + """Initialize.""" + self.url = url + +class TimeoutOccured(HomeAssistantError): + """Error to indicate we cannot connect.""" + def __init__(self, url: str) -> None: + """Initialize.""" + self.url = url + +class InvalidRespStatus(HomeAssistantError): + """Error to indicate we got an invalid response status.""" + def __init__(self, status: int, response: str) -> None: + """Initialize.""" + self.status = status + self.response = response + +class InvalidJson(HomeAssistantError): + """Error to indicate we got an invalid json response.""" + def __init__(self, details: str, response: str) -> None: + """Initialize.""" + self.details = details + self.response = response + +class NotImplemented(HomeAssistantError): + """Error to indicate that something is not yet implemented.""" diff --git a/homeassistant/components/goe_charger/config_flow.py b/homeassistant/components/goe_charger/config_flow.py new file mode 100644 index 00000000000..550ba105145 --- /dev/null +++ b/homeassistant/components/goe_charger/config_flow.py @@ -0,0 +1,88 @@ +"""Config flow for go-e Charger integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.data_entry_flow import FlowResult + +import voluptuous as vol + +from .const import DOMAIN +from .common import GoeChargerHub, CannotConnect, TimeoutOccured, InvalidRespStatus, InvalidJson, NotImplemented + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): str, + vol.Required("interval", default=5): int, + } +) + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for go-e Charger.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry): + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA) + + hub = GoeChargerHub(user_input["host"]) + + try: + data = await hub.get_data(self.hass, ["sse"]); + except CannotConnect: + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors={"base": "cannot_connect"}) + except TimeoutOccured: + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors={"base": "timeout_occured"}) + except InvalidRespStatus: + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors={"base": "invalid_resp_status"}) + except InvalidJson: + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors={"base": "invalid_json"}) + except NotImplemented: + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors={"base": "not_implemented"}) + except Exception as e: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception %s", str(e)) + return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors={"base": "unknown"}) + + # TODO duplicate search + + result = self.async_create_entry(title="go-e Charger " + data["sse"], data={ + "host": user_input["host"], + "serial": data["sse"], + }) + + return result + +class OptionsFlowHandler(config_entries.OptionsFlow): + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self._config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + + data_schema = vol.Schema( + { + vol.Required("host", default=self._config_entry.data.get("host")): str, + vol.Required("interval", default=self._config_entry.data.get("interval")): int, + } + ) + + if user_input is None: + return self.async_show_form(step_id="init", data_schema=data_schema) + + # return self.async_create_entry(title="", data=user_input) + return self.async_show_form(step_id="init", data_schema=data_schema, errors={"base": "not_implemented"}) diff --git a/homeassistant/components/goe_charger/const.py b/homeassistant/components/goe_charger/const.py new file mode 100644 index 00000000000..6ade031b17c --- /dev/null +++ b/homeassistant/components/goe_charger/const.py @@ -0,0 +1,3 @@ +"""Constants for the go-e Charger integration.""" + +DOMAIN = "goe_charger" diff --git a/homeassistant/components/goe_charger/manifest.json b/homeassistant/components/goe_charger/manifest.json new file mode 100644 index 00000000000..df2db0a53e2 --- /dev/null +++ b/homeassistant/components/goe_charger/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "goe_charger", + "name": "go-e Charger", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/goe_charger", + "requirements": [], + "ssdp": [], + "zeroconf": [ + {"type":"_http._tcp.local."} + ], + "homekit": {}, + "dependencies": ["http"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/goe_charger/number.py b/homeassistant/components/goe_charger/number.py new file mode 100644 index 00000000000..3bb9e872c35 --- /dev/null +++ b/homeassistant/components/goe_charger/number.py @@ -0,0 +1,106 @@ +"""Platform for number integration.""" +from __future__ import annotations + +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT +from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator +from homeassistant.components.number import NumberEntity + +from .const import DOMAIN +from .common import GoeChargerHub + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities) -> None: + coordinator = hass.data[DOMAIN][config_entry.entry_id] + serial = config_entry.data["serial"] + + async_add_entities([ + GoeChargerNumber(coordinator, config_entry, "Requested current", serial, "requested_current", ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, "amp") + ]) + +class GoeChargerNumber(CoordinatorEntity, NumberEntity): + """Representation of a Sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, config_entry: ConfigEntry, name: str, serial: str, unique_id: str, unit_of_measurement: str, device_class: str | None, key: str): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._config_entry = config_entry + self._name = name + self._serial = serial + self._unique_id = unique_id + self._unit_of_measurement = unit_of_measurement + self._device_class = device_class + self._key = key + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return the unique id of the device.""" + return "goe_charger_" + self._serial + "_" + self._unique_id + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.data is not None and self._key in self.coordinator.data + + @property + def value(self) -> float: + """Return the state of the sensor.""" + return None if not self.available else self.coordinator.data[self._key] + + @property + def min_value(self) -> float: + """Return the state of the sensor.""" + return 6 + + @property + def max_value(self) -> float: + """Return the state of the sensor.""" + return 32 + + @property + def step(self) -> float: + """Return the state of the sensor.""" + return 1 + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + + hub = GoeChargerHub(self._config_entry.data["host"]) + await hub.set_data(self.hass, { + self._key: int(value) + }) + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + await self.coordinator.async_request_refresh() + + @property + def device_info(self): + """Get attributes about the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + #"name": self._device.label, + #"model": self._device.device_type_name, + #"manufacturer": "Unavailable", + } diff --git a/homeassistant/components/goe_charger/select.py b/homeassistant/components/goe_charger/select.py new file mode 100644 index 00000000000..960137817cc --- /dev/null +++ b/homeassistant/components/goe_charger/select.py @@ -0,0 +1,133 @@ +"""Platform for number integration.""" +from __future__ import annotations + +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator +from homeassistant.components.select import SelectEntity + +from .const import DOMAIN +from .common import GoeChargerHub + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities) -> None: + coordinator = hass.data[DOMAIN][config_entry.entry_id] + serial = config_entry.data["serial"] + + async_add_entities([ + GoeChargerSelect(coordinator, config_entry, "Logic mode", serial, "logic_mode", None, None, "lmo", { + 3: "Default", + 4: "Awattar", + 5: "AutomaticStop" + }), + GoeChargerSelect(coordinator, config_entry, "Unlock setting", serial, "unlock_setting", None, None, "ust", { + 0: "Normal", + 1: "AutoUnlock", + 2: "AlwaysLock", + 3: "ForceUnlock" + }), + GoeChargerSelect(coordinator, config_entry, "Access control", serial, "access_control", None, None, "acs", { + 0: "Open", + 1: "Wait" + }), + GoeChargerSelect(coordinator, config_entry, "Force state", serial, "force_state", None, None, "acs", { + 0: "Neutral", + 1: "Off", + 2: "On" + }), + GoeChargerSelect(coordinator, config_entry, "Phase switch mode", serial, "phase_switch_mode", None, None, "psm", { + 1: "Force_1", + 2: "Force_3" + }) + ]) + +class GoeChargerSelect(CoordinatorEntity, SelectEntity): + """Representation of a Sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, config_entry: ConfigEntry, name: str, serial: str, unique_id: str, unit_of_measurement: str, device_class: str | None, key: str, options: dict[int, str]): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._name = name + self._config_entry = config_entry + self._serial = serial + self._unique_id = unique_id + self._unit_of_measurement = unit_of_measurement + self._device_class = device_class + self._key = key + self._options = options + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return the unique id of the device.""" + return "goe_charger_" + self._serial + "_" + self._unique_id + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.data is not None and self._key in self.coordinator.data + + @property + def current_option(self) -> str | None: + """The current select option""" + if not self.available: + return None + + current_data = self.coordinator.data[self._key] + + if current_data in self._options: + return self._options[current_data] + + return "Unknown (" + str(current_data) + ")" + + @property + def options(self) -> list[str]: + """A list of available options as strings""" + return list(self._options.values()) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + key_list = list(self._options.keys()) + val_list = list(self._options.values()) + + index = val_list.index(option) + + hub = GoeChargerHub(self._config_entry.data["host"]) + await hub.set_data(self.hass, { + self._key: key_list[index] + }) + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + await self.coordinator.async_request_refresh() + + @property + def device_info(self): + """Get attributes about the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + #"name": self._device.label, + #"model": self._device.device_type_name, + #"manufacturer": "Unavailable", + } diff --git a/homeassistant/components/goe_charger/sensor.py b/homeassistant/components/goe_charger/sensor.py new file mode 100644 index 00000000000..98ea73dced1 --- /dev/null +++ b/homeassistant/components/goe_charger/sensor.py @@ -0,0 +1,217 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import (ELECTRIC_POTENTIAL_VOLT, ELECTRIC_CURRENT_AMPERE, POWER_WATT, POWER_KILO_WATT, FREQUENCY_HERTZ, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, + SIGNAL_STRENGTH_DECIBELS, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE) +from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING +from homeassistant.components.sensor import SensorEntity + +from .const import DOMAIN + +POWER_FACTOR: Final = "%" + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities) -> None: + coordinator = hass.data[DOMAIN][config_entry.entry_id] + serial = config_entry.data["serial"] + + def car_state_data(data): + car_state_texts = { + 0: "Unknown", + 1: "Idle", + 2: "Charging", + 3: "WaitCar", + 4: "Complete", + 5: "Error" + } + + if data["car"] in car_state_texts: + return car_state_texts[data["car"]] + + return "Unknown (" + str(data["car"]) + ")" + + def error_data(data): + error_texts = { + 0: "None", + 1: "FiAc", + 2: "FiDc", + 3: "Phase", + 4: "Overvolt", + 5: "Overamp", + 6: "Diode", + 7: "PpInvalid", + 8: "GndInvalid", + 9: "ContactorStuck", + 10: "ContactorMiss", + 11: "FiUnknown", + 12: "Unknown", + 13: "Overtemp", + 14: "NoComm", + 15: "StatusLockStuckOpen", + 16: "StatusLockStuckLocked", + 17: "Reserved20", + 18: "Reserved21", + 19: "Reserved22", + 20: "Reserved23", + 21: "Reserved24" + } + + if data["err"] in error_texts: + return error_texts[data["err"]] + + return "Unknown (" + str(data["err"]) + ")" + + def model_status_data(data): + model_status_texts = { + 0: "NotChargingBecauseNoChargeCtrlData", + 1: "NotChargingBecauseOvertemperature", + 2: "NotChargingBecauseAccessControlWait", + 3: "ChargingBecauseForceStateOn", + 4: "NotChargingBecauseForceStateOff", + 5: "NotChargingBecauseScheduler", + 6: "NotChargingBecauseEnergyLimit", + 7: "ChargingBecauseAwattarPriceLow", + 8: "ChargingBecauseAutomaticStopTestLadung", + 9: "ChargingBecauseAutomaticStopNotEnoughTime", + 10: "ChargingBecauseAutomaticStop", + 11: "ChargingBecauseAutomaticStopNoClock", + 12: "ChargingBecausePvSurplus", + 13: "ChargingBecauseFallbackGoEDefault", + 14: "ChargingBecauseFallbackGoEScheduler", + 15: "ChargingBecauseFallbackDefault", + 16: "NotChargingBecauseFallbackGoEAwattar", + 17: "NotChargingBecauseFallbackAwattar", + 18: "NotChargingBecauseFallbackAutomaticStop", + 19: "ChargingBecauseCarCompatibilityKeepAlive", + 20: "ChargingBecauseChargePauseNotAllowed", + 22: "NotChargingBecauseSimulateUnplugging", + 23: "NotChargingBecausePhaseSwitch", + 24: "NotChargingBecauseMinPauseDuration" + } + + if data["modelStatus"] in model_status_texts: + return model_status_texts[data["modelStatus"]] + + return "Unknown (" + str(data["modelStatus"]) + ")" + + async_add_entities([ + GoeChargerSensor(coordinator, "Voltage L1", serial, "voltage_l1", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][0] ), + GoeChargerSensor(coordinator, "Voltage L2", serial, "voltage_l2", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][1] ), + GoeChargerSensor(coordinator, "Voltage L3", serial, "voltage_l3", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][2] ), + GoeChargerSensor(coordinator, "Voltage N", serial, "voltage_n", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][3] ), + GoeChargerSensor(coordinator, "Current L1", serial, "current_l1", ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][4] ), + GoeChargerSensor(coordinator, "Current L2", serial, "current_l2", ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][5] ), + GoeChargerSensor(coordinator, "Current L3", serial, "current_l3", ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][6] ), + GoeChargerSensorNative(coordinator, "Power L1", serial, "power_l1", POWER_KILO_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, "nrg", (lambda data: data["nrg"][7] / 1000) , POWER_KILO_WATT, lambda data: data["nrg"][7] ), + GoeChargerSensorNative(coordinator, "Power L2", serial, "power_l2", POWER_KILO_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, "nrg", (lambda data: data["nrg"][8] / 1000) , POWER_KILO_WATT, lambda data: data["nrg"][8] ), + GoeChargerSensorNative(coordinator, "Power L3", serial, "power_l3", POWER_KILO_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, "nrg", (lambda data: data["nrg"][9] / 1000) , POWER_KILO_WATT, lambda data: data["nrg"][9] ), + GoeChargerSensorNative(coordinator, "Power N", serial, "power_n", POWER_KILO_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, "nrg", (lambda data: data["nrg"][10] / 1000) , POWER_KILO_WATT, lambda data: data["nrg"][10] ), + GoeChargerSensorNative(coordinator, "Power Total", serial, "power_total", POWER_KILO_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, "nrg", (lambda data: data["nrg"][11] / 1000) , POWER_KILO_WATT, lambda data: data["nrg"][11] ), + GoeChargerSensor(coordinator, "Powerfactor L1", serial, "powerfactor_l1", POWER_FACTOR, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][12] ), + GoeChargerSensor(coordinator, "Powerfactor L2", serial, "powerfactor_l2", POWER_FACTOR, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][13] ), + GoeChargerSensor(coordinator, "Powerfactor L3", serial, "powerfactor_l3", POWER_FACTOR, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][14] ), + GoeChargerSensor(coordinator, "Powerfactor N", serial, "powerfactor_n", POWER_FACTOR, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT, "nrg", lambda data: data["nrg"][15] ), + GoeChargerSensor(coordinator, "Frequency", serial, "frequency", FREQUENCY_HERTZ, None, STATE_CLASS_MEASUREMENT, "fhz", lambda data: data["fhz"] ), + GoeChargerSensorNative(coordinator, "Charged", serial, "charged", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING, "wh", (lambda data: data["wh"] / 1000) , POWER_KILO_WATT, lambda data: data["wh"] ), + GoeChargerSensorNative(coordinator, "Charged total", serial, "charged_total", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING, "eto", (lambda data: data["eto"] / 1000) , POWER_KILO_WATT, lambda data: data["eto"] ), + GoeChargerSensor(coordinator, "Temperature 1", serial, "temperature_1", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, "tma", lambda data: data["tma"][0] ), + GoeChargerSensor(coordinator, "Temperature 2", serial, "temperature_2", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, "tma", lambda data: data["tma"][1] ), + GoeChargerSensor(coordinator, "WiFi RSSI", serial, "wifi_rssi", SIGNAL_STRENGTH_DECIBELS, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, "rssi", lambda data: data["rssi"] ), + GoeChargerSensor(coordinator, "Cable current limit", serial, "cable_current_limit", ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, None, "cbl", lambda data: data["cbl"]), + GoeChargerSensor(coordinator, "Allowed current", serial, "allowed_current", ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, None, "acu", lambda data: "" if data["acu"] is None else data["acu"] ), + GoeChargerSensor(coordinator, "Car state", serial, "car_state", None, None, None, "car", car_state_data ), + GoeChargerSensor(coordinator, "Error", serial, "error", None, None, None, "err", error_data ), + GoeChargerSensor(coordinator, "Model status", serial, "model_status", None, None, None, "modelStatus", model_status_data ), + ]) + +class GoeChargerSensor(CoordinatorEntity, SensorEntity): + """Representation of a Sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, name: str, serial: str, unique_id: str, unit_of_measurement: str | None, device_class: str | None, state_class: str | None, key: str, state_cb): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._name = name + self._serial = serial + self._unique_id = unique_id + self._unit_of_measurement = unit_of_measurement + self._device_class = device_class + self._state_class = state_class + self._key = key + self._state_cb = state_cb + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return the unique id of the device.""" + return "goe_charger_" + self._serial + "_" + self._unique_id + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.data is not None and self._key in self.coordinator.data + + @property + def state(self): + """Return the state of the sensor.""" + return None if not self.available else self._state_cb(self.coordinator.data) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def state_class(self): + """Return the state class.""" + return self._state_class + + async def async_update(self): + """Fetch new state data for the sensor. + This is the only method that should fetch new data for Home Assistant. + """ + await self.coordinator.async_request_refresh() + + @property + def device_info(self): + """Get attributes about the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + #"name": self._device.label, + #"model": self._device.device_type_name, + #"manufacturer": "Unavailable", + } + +class GoeChargerSensorNative(GoeChargerSensor): + """Representation of a Sensor with separated native unit/value.""" + + def __init__(self, coordinator: DataUpdateCoordinator, name: str, serial: str, unique_id: str, unit_of_measurement: str | None, device_class: str | None, state_class: str | None, key: str, state_cb, native_unit_of_measurement: str | None, native_state_cb): + """Pass coordinator to GoeChargerSensor.""" + super().__init__(coordinator, name, serial, unique_id, unit_of_measurement, device_class, state_class, key, state_cb) + self._native_unit_of_measurement = native_unit_of_measurement + self._native_state_cb = native_state_cb + + @property + def native_value(self): + """Return the value reported by the sensor.""" + return None if not self.available else self._native_state_cb(self.coordinator.data) + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the native unit of measurement.""" + return self._native_unit_of_measurement diff --git a/homeassistant/components/goe_charger/strings.json b/homeassistant/components/goe_charger/strings.json new file mode 100644 index 00000000000..6150ce51f98 --- /dev/null +++ b/homeassistant/components/goe_charger/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "interval": "[%key:common::config_flow::data::interval%]" + } + }, + "init": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "interval": "[%key:common::config_flow::data::interval%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_occured": "[%key:common::config_flow::error::timeout_occured%]", + "invalid_resp_status": "[%key:common::config_flow::error::invalid_resp_status%]", + "invalid_json": "[%key:common::config_flow::error::invalid_json%]", + "not_implemented": "[%key:common::config_flow::error::not_implemented%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/goe_charger/translations/en.json b/homeassistant/components/goe_charger/translations/en.json new file mode 100644 index 00000000000..9d61ceb3131 --- /dev/null +++ b/homeassistant/components/goe_charger/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "timeout_occured": "Timeout occured", + "invalid_resp_status": "Invalid response status", + "invalid_json": "Invalid JSON", + "not_implemented": "Not implemented", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "interval": "Interval" + } + }, + "init": { + "data": { + "host": "Host", + "interval": "Interval" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2eb4e43fe32..e2c8e534f5d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -99,6 +99,7 @@ FLOWS = [ "gios", "glances", "goalzero", + "goe_charger", "gogogate2", "google_travel_time", "gpslogger",