From 86d4e60ec445ff6b39d281cece6d7d6ed495c14f Mon Sep 17 00:00:00 2001 From: Alexey Vlasov Date: Thu, 15 Oct 2020 15:39:58 +0700 Subject: [PATCH] Add options of integration --- README.md | 5 +- custom_components/pandora_cas/__init__.py | 6 ++ custom_components/pandora_cas/api.py | 54 ++++++++--- custom_components/pandora_cas/config_flow.py | 89 ++++++++++++++++++- custom_components/pandora_cas/const.py | 8 ++ custom_components/pandora_cas/sensor.py | 10 ++- .../pandora_cas/translations/en.json | 20 +++++ .../pandora_cas/translations/ru.json | 20 +++++ 8 files changed, 191 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8208fb2..466bd06 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ 1. Нажмите кнопку "+", расположенную в нижней правой части экрана 1. Наберите "Pandora" для поиска компонента 1. Установите компонент Pandora Car Alarm System -1. (?) Перезапустите Home Assistant +1. Перезапустите Home Assistant ## Настройка @@ -35,6 +35,7 @@ 1. Введите логин, пароль, а также частоту обновления информации с сайта p-on.ru 1. При необходимотсти задайте помещение для автомобиля 1. Устройства и сенсоры добавятся в Home Assistant +1. При необходимости, в настройках интеграции выберите единицы измерения топлива, источник данных для одометра и его начальные значения. Для актуализации изменений сенсоров потребуется перезапуск Home Assistant. ## Device Tracker @@ -47,7 +48,7 @@ | Объект | Назначение | Примечание | |-|-|-| | sensor.`PANDORA_ID`_mileage | Пробег | км | -| sensor.`PANDORA_ID`_fuel_level | | % | +| sensor.`PANDORA_ID`_fuel_level | Уровень топлива | % или L | | sensor.`PANDORA_ID`_cabin_temperature | Температура салона | °C | | sensor.`PANDORA_ID`_engine_temperature | Температура двигателя | °C | | sensor.`PANDORA_ID`_ambient_temperature | Уличная температура | °C | diff --git a/custom_components/pandora_cas/__init__.py b/custom_components/pandora_cas/__init__.py index cfb3a0d..d2dd2e3 100644 --- a/custom_components/pandora_cas/__init__.py +++ b/custom_components/pandora_cas/__init__.py @@ -112,6 +112,12 @@ async def _execute_command(call) -> bool: api = hass.data[DOMAIN] = PandoraApi(hass, username, password, polling_interval) await api.load_devices() await api.async_refresh() + + # Save options which got from config_entry + for pandora_id, options in config_entry.options.items(): + if pandora_id in api.devices.keys(): + await api.devices[pandora_id].config_options(options) + except PandoraApiException as ex: _LOGGER.error("Setting up entry %s failed: %s", username, str(ex)) return False diff --git a/custom_components/pandora_cas/api.py b/custom_components/pandora_cas/api.py index b3e0ee8..0de6660 100644 --- a/custom_components/pandora_cas/api.py +++ b/custom_components/pandora_cas/api.py @@ -8,13 +8,21 @@ import aiohttp from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.const import PERCENTAGE from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow -from .const import DOMAIN +from .const import ( + DOMAIN, + MILEAGE_SOURCES, + OPTION_FUEL_UNITS, + OPTION_MILEAGE_SOURCE, + OPTION_MILEAGE_ADJUSTMENT, + FUEL_UNITS, +) _LOGGER = logging.getLogger(__name__) @@ -158,10 +166,8 @@ async def _async_update(self, *_) -> bool: if self._update_ts >= self._force_update_ts + FORCE_UPDATE_INTERVAL: self._update_ts = 0 - response = PandoraApiUpdateResponseParser( - await self._request_safe(UPDATE_PATH + str(self._update_ts - 1)) - ) - + response = PandoraApiUpdateResponseParser(await self._request_safe(UPDATE_PATH + str(self._update_ts - 1))) + stats = response.stats if self._update_ts == 0: self._force_update_ts = self._update_ts = response.timestamp @@ -183,7 +189,6 @@ async def _async_update(self, *_) -> bool: except PandoraApiException as ex: _LOGGER.info("Update failed: %s", str(ex)) - # I made some experiments with my car. How long does it take between sending command # and getting proper state of corresponding entity? Results is placed below: # ---------------------------------------------------------------------------------- @@ -278,19 +283,32 @@ def is_online(self) -> bool: return bool(self.online) @property - def fuel_tank(self) -> int: - """Get the capacity of fuel tank.""" - return int(self._info["fuel_tank"]) + def fuel_percentage(self) -> int: + """Get fuel in percentage.""" + return int(self._attributes["fuel"]) + + @property + def fuel_litres(self) -> int: + """Get fuel in liters.""" + return int(self._info["fuel_tank"]) * self.fuel_percentage / 100 + + @property + def fuel(self) -> int: + """Get fuel in user-defined units.""" + if self._info.get(OPTION_FUEL_UNITS, FUEL_UNITS[0]) == FUEL_UNITS[0]: + return self.fuel_percentage + + return self.fuel_litres @property def mileage(self) -> float: - """Get the mileage.""" + """Get mileage from user-defined source with user-defined adjustment.""" + adjustment = float(self._info.get(OPTION_MILEAGE_ADJUSTMENT, 0)) - # mileage_CAN will be used if supported - if self._attributes["mileage_CAN"] > 0: - return float(self._attributes["mileage_CAN"]) + if self._info.get(OPTION_MILEAGE_SOURCE, MILEAGE_SOURCES[0]) == MILEAGE_SOURCES[0]: + return adjustment + float(self._attributes["mileage"]) - return float(self._attributes["mileage"]) + return adjustment + float(self._attributes["mileage_CAN"]) @property def device_info(self) -> dict: @@ -303,10 +321,18 @@ def device_info(self) -> dict: "sw_version": self._info["firmware"], } + def user_defined_units(self, item): + """Get units of attribute.""" + return self._info.get(item + "_units") + def __getattr__(self, item): """Generic get function for all backend attributes.""" return self._attributes[item] + async def config_options(self, options: dict) -> None: + """Save options from config_entry.""" + self._info.update(options) + async def update(self, attributes: dict) -> None: """Read new status data from the server.""" diff --git a/custom_components/pandora_cas/config_flow.py b/custom_components/pandora_cas/config_flow.py index 6d43abb..20683cf 100644 --- a/custom_components/pandora_cas/config_flow.py +++ b/custom_components/pandora_cas/config_flow.py @@ -3,19 +3,32 @@ DETAILS """ +from collections import OrderedDict import logging from typing import Optional import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, PERCENTAGE from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, CONF_POLLING_INTERVAL, DEFAULT_POLLING_INTERVAL, MIN_POLLING_INTERVAL +from .const import ( + DOMAIN, + CONF_POLLING_INTERVAL, + DEFAULT_POLLING_INTERVAL, + MIN_POLLING_INTERVAL, + MILEAGE_SOURCES, + OPTION_FUEL_UNITS, + OPTION_MILEAGE_SOURCE, + OPTION_MILEAGE_ADJUSTMENT, + FUEL_UNITS, +) _LOGGER = logging.getLogger(__name__) +PANDORA_ID = "pandora_id" + FLOW_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_POLLING_INTERVAL,): int,} ) @@ -59,6 +72,7 @@ async def validate_input(user_input: Optional[ConfigType] = None): raise ValueError +@config_entries.HANDLERS.register("pandora_cas") class PandoraCasConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Pandora CAS""" @@ -68,6 +82,10 @@ class PandoraCasConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): self.data_schema = {} + @staticmethod + def async_get_options_flow(config_entry): + return OptionsFlowHandler(config_entry) + async def async_step_user(self, user_input: Optional[ConfigType] = None): errors = {} @@ -113,3 +131,70 @@ async def async_step_discovery_confirm(self, user_input: Optional[ConfigType] = return self.async_create_entry(title=username, data=user_input) return self.async_show_form(step_id="discovery_confirm", data_schema=self.data_schema, errors=errors) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + def __init__(self, config_entry): + self.config_entry = config_entry + self.options = dict(config_entry.options) + self.pandora_id = None + + async def async_step_init(self, user_input=None): + return await self.async_step_device() + + async def async_step_device(self, user_input=None): + IDS = [] + api = self.hass.data[DOMAIN] + + if user_input is not None: + self.pandora_id = user_input[PANDORA_ID] + return await self.async_step_options() + + for pandora_id in api.devices.keys(): + IDS.append(pandora_id) + + fields = OrderedDict() + fields[vol.Required(PANDORA_ID, default=IDS[0])] = vol.In(IDS) + + return self.async_show_form(step_id="device", data_schema=vol.Schema(fields)) + + async def async_step_options(self, user_input=None): + """Manage the options.""" + api = self.hass.data[DOMAIN] + device_options = {} + + if user_input is not None: + device_options[self.pandora_id] = {} + device_options[self.pandora_id][OPTION_FUEL_UNITS] = user_input.get(OPTION_FUEL_UNITS, FUEL_UNITS[0]) + device_options[self.pandora_id][OPTION_MILEAGE_SOURCE] = user_input.get( + OPTION_MILEAGE_SOURCE, MILEAGE_SOURCES[0] + ) + device_options[self.pandora_id][OPTION_MILEAGE_ADJUSTMENT] = user_input.get(OPTION_MILEAGE_ADJUSTMENT, 0) + self.options.update(device_options) + self.pandora_id = None # invalidate pandora_id + return self.async_create_entry(title="", data=self.options) + + fields = OrderedDict() + device_options = self.options.get(self.pandora_id) + if device_options is None: + fields[vol.Optional(OPTION_FUEL_UNITS, default=FUEL_UNITS[0])] = vol.In(FUEL_UNITS) + fields[vol.Optional(OPTION_MILEAGE_SOURCE, default=MILEAGE_SOURCES[0])] = vol.In(MILEAGE_SOURCES) + fields[vol.Optional(OPTION_MILEAGE_ADJUSTMENT, default=0)] = vol.Coerce(int) + else: + fields[ + vol.Optional(OPTION_FUEL_UNITS, default=device_options.get(OPTION_FUEL_UNITS, FUEL_UNITS[0])) + ] = vol.In(FUEL_UNITS) + fields[ + vol.Optional( + OPTION_MILEAGE_SOURCE, default=device_options.get(OPTION_MILEAGE_SOURCE, MILEAGE_SOURCES[0]) + ) + ] = vol.In(MILEAGE_SOURCES) + fields[ + vol.Optional(OPTION_MILEAGE_ADJUSTMENT, default=device_options.get(OPTION_MILEAGE_ADJUSTMENT, 0)) + ] = vol.Coerce(int) + + return self.async_show_form( + step_id="options", + data_schema=vol.Schema(fields), + description_placeholders={"name": api.devices[self.pandora_id].name}, + ) diff --git a/custom_components/pandora_cas/const.py b/custom_components/pandora_cas/const.py index ca0958a..161632f 100644 --- a/custom_components/pandora_cas/const.py +++ b/custom_components/pandora_cas/const.py @@ -2,6 +2,8 @@ from datetime import timedelta +from homeassistant.const import PERCENTAGE, VOLUME_LITERS + DOMAIN = "pandora_cas" ATTR_IS_CONNECTION_SENSITIVE = "is_connection_sensitive" @@ -17,3 +19,9 @@ CONF_POLLING_INTERVAL = "polling_interval" MIN_POLLING_INTERVAL = timedelta(seconds=10) DEFAULT_POLLING_INTERVAL = timedelta(minutes=1) + +FUEL_UNITS = [PERCENTAGE, VOLUME_LITERS] +MILEAGE_SOURCES = ["GPS", "CAN"] +OPTION_FUEL_UNITS = "fuel_units" +OPTION_MILEAGE_SOURCE = "mileage_source" +OPTION_MILEAGE_ADJUSTMENT = "mileage_adjustment" diff --git a/custom_components/pandora_cas/sensor.py b/custom_components/pandora_cas/sensor.py index 551062f..ad72166 100644 --- a/custom_components/pandora_cas/sensor.py +++ b/custom_components/pandora_cas/sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import ENTITY_ID_FORMAT, DEVICE_CLASS_TEMPERATURE from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, LENGTH_KILOMETERS, TEMP_CELSIUS +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, LENGTH_KILOMETERS, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify @@ -24,7 +24,7 @@ "mileage": { ATTR_NAME: "mileage", ATTR_ICON: "mdi:map-marker-distance", - ATTR_DEVICE_CLASS: None, # TODO: Make propper device class + ATTR_DEVICE_CLASS: None, # TODO: Make propper device class ATTR_UNITS: LENGTH_KILOMETERS, ATTR_IS_CONNECTION_SENSITIVE: True, ATTR_DEVICE_ATTR: "mileage", @@ -34,7 +34,7 @@ ATTR_NAME: "fuel", ATTR_ICON: "mdi:gauge", ATTR_DEVICE_CLASS: None, - ATTR_UNITS: "%", + ATTR_UNITS: PERCENTAGE, ATTR_IS_CONNECTION_SENSITIVE: True, ATTR_DEVICE_ATTR: "fuel", }, @@ -134,6 +134,10 @@ def __init__( self.entity_id = self.ENTITY_ID_FORMAT.format("{}_{}".format(slugify(device.pandora_id), entity_id)) + user_defined_units = device.user_defined_units(self.device_attr) + if user_defined_units is not None: + self._config[ATTR_UNITS] = user_defined_units + @property def icon(self) -> str: """Return the icon of the binary sensor.""" diff --git a/custom_components/pandora_cas/translations/en.json b/custom_components/pandora_cas/translations/en.json index b747470..4364028 100644 --- a/custom_components/pandora_cas/translations/en.json +++ b/custom_components/pandora_cas/translations/en.json @@ -27,5 +27,25 @@ } }, "title": "Pandora Car Alarm System" + }, + "options": { + "step": { + "device": { + "data": { + "pandora_id": "Pandora ID" + }, + "title": "Pandora CAS settings", + "description": "Select device for setup" + }, + "options": { + "data": { + "fuel_units": "Fuel units", + "mileage_source": "Mileage source", + "mileage_adjustment": "Mileage adjustment" + }, + "title": "Pandora CAS settings", + "description": "Options for {name}" + } + } } } \ No newline at end of file diff --git a/custom_components/pandora_cas/translations/ru.json b/custom_components/pandora_cas/translations/ru.json index 74057d9..8be7805 100644 --- a/custom_components/pandora_cas/translations/ru.json +++ b/custom_components/pandora_cas/translations/ru.json @@ -27,5 +27,25 @@ } }, "title": "Pandora Car Alarm System" + }, + "options": { + "step": { + "device": { + "data": { + "pandora_id": "Pandora ID" + }, + "title": "Настройка Pandora CAS", + "description": "Выберите устройство для настройки" + }, + "options": { + "data": { + "fuel_units": "Отображение топлива", + "mileage_source": "Источник пробега", + "mileage_adjustment": "Корректировка пробега" + }, + "title": "Настройка Pandora CAS", + "description": "Задайте параметры для {name}" + } + } } } \ No newline at end of file