diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index e7d27306..43282cba 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -9,26 +9,17 @@ on: - master jobs: - validate-black: - name: With black + validate-ruff: + name: With Ruff runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v5 + - name: Check ruff + uses: chartboost/ruff-action@v1 with: - python-version: "3.10" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black - - - name: Check lint - run: | - black --check --diff --color --line-length=120 . + args: 'format --diff' validate-hassfest: name: With hassfest diff --git a/.gitignore b/.gitignore index ee05595b..c08061c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea __pycache__ +.DS_Store diff --git a/README.md b/README.md index 092a6d8a..55a03cd7 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,24 @@ ![Version](https://img.shields.io/github/v/release/robinostlund/homeassistant-volkswagencarnet) ![PyPi](https://img.shields.io/pypi/v/volkswagencarnet?label=latest%20pypi) ![Downloads](https://img.shields.io/github/downloads/robinostlund/homeassistant-volkswagencarnet/total) -![CodeStyle](https://img.shields.io/badge/code%20style-black-black) +![CodeStyle](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json) ![Known Vulnerabilities](https://snyk.io/test/github/robinostlund/homeassistant-volkswagencarnet/badge.svg) [![CodeQL](https://github.com/robinostlund/homeassistant-volkswagencarnet/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/robinostlund/homeassistant-volkswagencarnet/actions/workflows/codeql-analysis.yml) -# Volkswagen We Connect - An Home Assistant custom component to interact with the We Connect service +# Volkswagen Connect - An Home Assistant custom component to interact with the VW Connect service. (EU ONLY) [![buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/robinostlund) ## Description -Welcome to Volkswagen We Connect custom component designed for [Home Assistant](https://www.home-assistant.io) with the capability to interact with the Volkswagen Connect service (your car). +Welcome to Volkswagen Connect custom component designed for [Home Assistant](https://www.home-assistant.io) with the capability to interact with the Volkswagen Connect service (your car). This custom component supports the **Volkswagen Connect cars** such as the Passat, Golf, e-Golf, Tiguan, ID etc. It requires you to have an active and working VW online subscription connected to your car. The new electric vehicles such as the ID series are supported as well. Most of the functionality found the "Volkswagen Connect app" should be available via this integration, this includes options such as auxiliary heater control. -Note: Some features included in Volkswagen Connect 2023 and newer are not fully tested. This custom component should work with any models such as Golf/Passat 8.5/Tiguan etc. But please bear with me and report any fault or error as an issue. - Please note that there has only been reports of success with this component for cars sold (and based) in the EU. Please feel free to contribue to make this component work in the US as well, we do not have access to any Volkswagen Connect accounts to verify at this stage. ## Having issues with this custom component? @@ -36,7 +34,7 @@ Please, before posting an issue make sure that VW´s Connect service works for y * Add the component again as per below (install+configuration) ### Install with HACS (recommended) -Do you you have [HACS](https://community.home-assistant.io/t/custom-component-hacs) installed? Just search for Volkswagen We Connect and install it direct from HACS. HACS will keep track of updates and you can easly upgrade this component to latest version. +Do you you have [HACS](https://community.home-assistant.io/t/custom-component-hacs) installed? Just search for Volkswagen Connect and install it direct from HACS. HACS will keep track of updates and you can easly upgrade this component to latest version. ### Install manually Make sure you have an account on Volkswagen Connect. @@ -48,12 +46,12 @@ Note that only the packaged releases (zip file) have the dependencies configured ## Configuration * Restart Home Assistant -* Add and configure the component via the UI: Configuration > Integrations > search for "Volkswagen We Connect" and follow the wizard to configure (use your Volkswagen Connect credentials) +* Add and configure the component via the UI: Configuration > Integrations > search for "Volkswagen Connect" and follow the wizard to configure (use your Volkswagen Connect credentials) * All available features of your car should be added automatically after you have selected the VIN ### Configuration flow settings * Name your car - Enter a custom name, defaults to VIN (Optional) -* Username/Password - We Connect (Required) +* Username/Password - Volkswagen Connect (Required) * Region - The country where the car was sold (Required) * Mutable - If enabled you can interact with the car, if disabled only data from the car will be presented (Optional) * S-PIN - Required for some specific options such as lock/unlock (Optional) @@ -287,20 +285,9 @@ These templates create a new sensor with kilometers converted to miles. Add to y ``` ## Enable debug logging -```yaml -logger: - default: info - logs: - volkswagencarnet: debug - dashboard: debug - custom_components.volkswagencarnet: debug - custom_components.volkswagencarnet.climate: debug - custom_components.volkswagencarnet.lock: debug - custom_components.volkswagencarnet.device_tracker: debug - custom_components.volkswagencarnet.switch: debug - custom_components.volkswagencarnet.binary_sensor: debug - custom_components.volkswagencarnet.sensor: debug - ``` + +Check out this Wiki page: +https://github.com/robinostlund/homeassistant-volkswagencarnet/wiki/Enabling-Debug-Logging-in-Home-Assistant ## Lovelace Card diff --git a/custom_components/volkswagencarnet/__init__.py b/custom_components/volkswagencarnet/__init__.py old mode 100755 new mode 100644 index 6939cb49..c03b3ca8 --- a/custom_components/volkswagencarnet/__init__.py +++ b/custom_components/volkswagencarnet/__init__.py @@ -1,10 +1,10 @@ -"""We Connect custom integration for Home Assistant.""" +"""Volkswagen Connect custom integration for Home Assistant.""" import asyncio -import logging from datetime import datetime, timedelta +import logging -from homeassistant.config_entries import ConfigEntry, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -14,74 +14,59 @@ EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, Event, callback, State +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed, CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +# pylint: disable=no-name-in-module,hass-relative-import from volkswagencarnet.vw_connection import Connection from volkswagencarnet.vw_dashboard import ( - Instrument, BinarySensor, - Sensor, - Switch, DoorLock, - Position, + Instrument, Number, + Position, + Select, + Sensor, + Switch, TrunkLock, ) from volkswagencarnet.vw_vehicle import Vehicle from .const import ( COMPONENTS, + CONF_AVAILABLE_RESOURCES, + CONF_CONVERT, + CONF_DEBUG, + CONF_IMPERIAL_UNITS, CONF_MUTABLE, + CONF_NO_CONVERSION, CONF_REGION, - CONF_REPORT_REQUEST, - CONF_REPORT_SCAN_INTERVAL, CONF_SCANDINAVIAN_MILES, CONF_SPIN, CONF_VEHICLE, DATA, - DEFAULT_REPORT_UPDATE_INTERVAL, + DEFAULT_DEBUG, DEFAULT_UPDATE_INTERVAL, DOMAIN, SIGNAL_STATE_UPDATED, UNDO_UPDATE_LISTENER, UPDATE_CALLBACK, - CONF_DEBUG, - DEFAULT_DEBUG, - CONF_CONVERT, - CONF_IMPERIAL_UNITS, - SERVICE_SET_CHARGER_MAX_CURRENT, - CONF_AVAILABLE_RESOURCES, - CONF_NO_CONVERSION, -) -from .services import ( - ChargerService, - SERVICE_SET_CHARGER_MAX_CURRENT_SCHEMA, ) from .util import get_convert_conf _LOGGER = logging.getLogger(__name__) -def unload_services(hass: HomeAssistant): - """Unload the services from HA.""" - hass.services.async_remove(DOMAIN, SERVICE_SET_CHARGER_MAX_CURRENT) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Perform Volkswagen WeConnect component setup.""" - - def register_services(): - cs = ChargerService(hass) - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_SET_CHARGER_MAX_CURRENT, - service_func=cs.set_charger_max_current, - schema=SERVICE_SET_CHARGER_MAX_CURRENT_SCHEMA, - ) + """Perform Volkswagen Connect component setup.""" scan_interval_conf = entry.options.get( CONF_SCAN_INTERVAL, entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL) @@ -117,9 +102,13 @@ def is_new(attr): return attr not in entry.options.get(CONF_AVAILABLE_RESOURCES, [attr]) components = set() - for instrument in (instrument for instrument in instruments if instrument.component in COMPONENTS): + for instrument in ( + instrument for instrument in instruments if instrument.component in COMPONENTS + ): # Add resource if it's enabled or new - if is_enabled(instrument.slug_attr) or (is_new(instrument.slug_attr) and not entry.pref_disable_new_entities): + if is_enabled(instrument.slug_attr) or ( + is_new(instrument.slug_attr) and not entry.pref_disable_new_entities + ): data.instruments.add(instrument) components.add(COMPONENTS[instrument.component]) @@ -131,9 +120,9 @@ def is_new(attr): for component in components: coordinator.platforms.append(component) - hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, component)) - - register_services() + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) return True @@ -185,8 +174,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - _LOGGER.debug("Removing services") - unload_services(hass) _LOGGER.debug("Removing update listener") hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() @@ -219,7 +206,9 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> boo class VolkswagenData: """Hold component state.""" - def __init__(self, config: dict, coordinator: DataUpdateCoordinator | None = None): + def __init__( + self, config: dict, coordinator: DataUpdateCoordinator | None = None + ) -> None: """Initialize the component state.""" self.vehicles: set[Vehicle] = set() self.instruments: set[Instrument] = set() @@ -232,13 +221,21 @@ def instrument(self, vin: str, component: str, attr: str) -> Instrument: ret = next( ( instrument - for instrument in (self.coordinator.data if self.coordinator is not None else self.instruments) - if instrument.vehicle.vin == vin and instrument.component == component and instrument.attr == attr + for instrument in ( + self.coordinator.data + if self.coordinator is not None + else self.instruments + ) + if instrument.vehicle.vin == vin + and instrument.component == component + and instrument.attr == attr ), None, ) if ret is None: - raise ValueError(f"Instrument not found; component: {component}, attribute: {attr}") + raise ValueError( + f"Instrument not found; component: {component}, attribute: {attr}" + ) return ret def vehicle_name(self, vehicle: Vehicle) -> str: @@ -248,8 +245,7 @@ def vehicle_name(self, vehicle: Vehicle) -> str: if vehicle.vin: return vehicle.vin - else: - return "" + return "" class VolkswagenEntity(CoordinatorEntity, RestoreEntity): @@ -264,8 +260,8 @@ def __init__( vin: str, component: str, attribute: str, - callback=None, - ): + callback=None, # pylint: disable=redefined-outer-name + ) -> None: """Initialize the entity.""" # Pass coordinator to CoordinatorEntity. super().__init__(data.coordinator) @@ -296,12 +292,18 @@ def async_write_ha_state(self) -> None: # but the stored one is a string... sigh) if ( prev is None - or str(prev.attributes.get("last_updated", None)) != str(backend_refresh_time) + or str(prev.attributes.get("last_updated", None)) + != str(backend_refresh_time) or str(self.state or STATE_UNKNOWN) != str(prev.state) ): super().async_write_ha_state() else: - _LOGGER.debug(f"{self.name}: state not changed ('{prev.state}' == '{self.state}'), skipping update.") + _LOGGER.debug( + "%s: state not changed ('%s' == '%s'), skipping update", + self.name, + prev.state, + self.state, + ) @callback def _handle_coordinator_update(self) -> None: @@ -321,18 +323,35 @@ async def async_update(self) -> None: else: _LOGGER.error("Coordinator not set") + # pylint: disable-next=hass-missing-super-call async def async_added_to_hass(self) -> None: """Register update dispatcher.""" self.restored_state = await self.async_get_last_state() if self.coordinator is not None: - self.async_on_remove(self.coordinator.async_add_listener(self.async_write_ha_state)) + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) else: - self.async_on_remove(async_dispatcher_connect(self.hass, SIGNAL_STATE_UPDATED, self.async_write_ha_state)) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_STATE_UPDATED, self.async_write_ha_state + ) + ) @property def instrument( self, - ) -> BinarySensor | DoorLock | Position | Sensor | Switch | TrunkLock | Number | Instrument: + ) -> ( + BinarySensor + | DoorLock + | Position + | Select + | Sensor + | Switch + | TrunkLock + | Number + | Instrument + ): """Return corresponding instrument.""" return self.data.instrument(self.vin, self.component, self.attribute) @@ -340,9 +359,10 @@ def instrument( def icon(self) -> str | None: """Return the icon.""" if self.instrument.attr in ["battery_level", "charging"]: - return icon_for_battery_level(battery_level=self.instrument.state, charging=self.vehicle.charging) - else: - return self.instrument.icon + return icon_for_battery_level( + battery_level=self.instrument.state, charging=self.vehicle.charging + ) + return self.instrument.icon @property def vehicle(self) -> Vehicle: @@ -421,17 +441,20 @@ def notify_updated(self): class VolkswagenCoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry, update_interval: timedelta): + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, update_interval: timedelta + ) -> None: """Initialize the coordinator.""" self.vin = entry.data[CONF_VEHICLE].upper() self.entry = entry self.platforms: list[str] = [] - self.report_last_updated: datetime | None = None self.connection = Connection( session=async_get_clientsession(hass), username=self.entry.data[CONF_USERNAME], password=self.entry.data[CONF_PASSWORD], - fulldebug=self.entry.options.get(CONF_DEBUG, self.entry.data.get(CONF_DEBUG, DEFAULT_DEBUG)), + fulldebug=self.entry.options.get( + CONF_DEBUG, self.entry.data.get(CONF_DEBUG, DEFAULT_DEBUG) + ), country=self.entry.options.get(CONF_REGION, self.entry.data[CONF_REGION]), ) @@ -439,7 +462,12 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, update_interval: tim def async_update_listener(self) -> None: """Listen for update events.""" - _LOGGER.debug(f"Async update finished for {self.vin} ({self.name}). Next update in {self.update_interval}.") + _LOGGER.debug( + "Async update finished for %s (%s). Next update in %s", + self.vin, + self.name, + self.update_interval, + ) async def _async_update_data(self) -> list[Instrument]: """Update data via library.""" @@ -447,14 +475,13 @@ async def _async_update_data(self) -> list[Instrument]: if vehicle is None: raise UpdateFailed( - "Failed to update WeConnect. Need to accept EULA? " - "Try logging in to the portal: https://www.portal.volkswagen-we.com/" + "Failed to update Volkswagen Connect. Need to accept EULA? " + "Try logging in to the portal: https://www.myvolkswagen.net/" ) - if self.entry.options.get(CONF_REPORT_REQUEST, self.entry.data.get(CONF_REPORT_REQUEST, False)): - await self.report_request(vehicle) - - convert_conf = self.entry.options.get(CONF_CONVERT, self.entry.data.get(CONF_CONVERT, CONF_NO_CONVERSION)) + convert_conf = self.entry.options.get( + CONF_CONVERT, self.entry.data.get(CONF_CONVERT, CONF_NO_CONVERSION) + ) dashboard = vehicle.dashboard( mutable=self.entry.data.get(CONF_MUTABLE), @@ -466,25 +493,25 @@ async def _async_update_data(self) -> list[Instrument]: return dashboard.instruments async def async_logout(self, event: Event = None) -> bool: - """Logout from Volkswagen WeConnect.""" + """Logout from Volkswagen Connect.""" if event is not None: - _LOGGER.debug(f"Logging out due to event {event.event_type}") + _LOGGER.debug("Logging out due to event %s", event.event_type) try: if self.connection.logged_in: await self.connection.logout() - except Exception as ex: - _LOGGER.error("Could not log out from WeConnect, %s", ex) + except Exception as ex: # pylint: disable=broad-exception-caught + _LOGGER.error("Could not log out from Volkswagen Connect, %s", ex) return False return True async def async_login(self) -> bool: - """Login to Volkswagen WeConnect.""" + """Login to Volkswagen Connect.""" # check if we can login if not self.connection.logged_in: await self.connection.doLogin(3) if not self.connection.logged_in: _LOGGER.warning( - "Could not login to volkswagen WeConnect, " + "Could not login to Volkswagen Connect, " "please check your credentials and verify that " "the service is working" ) @@ -493,52 +520,15 @@ async def async_login(self) -> bool: return True async def update(self) -> Vehicle | None: - """Update status from Volkswagen WeConnect.""" + """Update status from Volkswagen Connect.""" # update vehicles if not await self.connection.update(): - _LOGGER.warning("Could not query update from volkswagen WeConnect") + _LOGGER.warning("Could not query update from Volkswagen Connect") return None - _LOGGER.debug("Updating data from volkswagen WeConnect") + _LOGGER.debug("Updating data from Volkswagen Connect") for vehicle in self.connection.vehicles: if vehicle.vin.upper() == self.vin: return vehicle return None - - async def report_request(self, vehicle: Vehicle) -> None: - """Request car to report itself an update to Volkswagen WeConnect.""" - report_interval = self.entry.options.get( - CONF_REPORT_SCAN_INTERVAL, self.entry.data.get(CONF_REPORT_SCAN_INTERVAL, DEFAULT_REPORT_UPDATE_INTERVAL) - ) - - if not self.report_last_updated: - days_since_last_update = 1 - else: - days_since_last_update = (datetime.now() - self.report_last_updated).days - - if days_since_last_update < report_interval: - return - - # noinspection PyBroadException - try: - # check if we can login - if not self.connection.logged_in: - await self.connection.doLogin() - if not self.connection.logged_in: - _LOGGER.warning( - "Could not login to volkswagen WeConnect, please " - "check your credentials and verify that the service " - "is working" - ) - return - - # request report - if not await vehicle.request_report(): - _LOGGER.warning("Could not request report from volkswagen WeConnect") - return - - self.report_last_updated = datetime.now() - except: # noqa: E722 - # This is actually not critical so... - pass diff --git a/custom_components/volkswagencarnet/binary_sensor.py b/custom_components/volkswagencarnet/binary_sensor.py index 41afbc79..32839869 100644 --- a/custom_components/volkswagencarnet/binary_sensor.py +++ b/custom_components/volkswagencarnet/binary_sensor.py @@ -1,30 +1,35 @@ -"""BinarySensor support for Volkswagen We Connect integration.""" +"""BinarySensor support for Volkswagen Connect integration.""" import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, - BinarySensorEntity, BinarySensorDeviceClass, + BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from . import VolkswagenEntity -from .const import DATA_KEY, DATA, DOMAIN, UPDATE_CALLBACK +from .const import DATA, DATA_KEY, DOMAIN, UPDATE_CALLBACK _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass: HomeAssistant, config: ConfigEntry, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities, + discovery_info=None, +): """Set up the Volkswagen binary sensors platform.""" if discovery_info is None: return async_add_entities([VolkswagenBinarySensor(hass.data[DATA_KEY], *discovery_info)]) -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): """Set up the Volkswagen binary sensor.""" data = hass.data[DOMAIN][entry.entry_id][DATA] coordinator = data.coordinator @@ -37,7 +42,11 @@ async def async_setup_entry(hass, entry, async_add_devices): attribute=instrument.attr, callback=hass.data[DOMAIN][entry.entry_id][UPDATE_CALLBACK], ) - for instrument in (instrument for instrument in data.instruments if instrument.component == "binary_sensor") + for instrument in ( + instrument + for instrument in data.instruments + if instrument.component == "binary_sensor" + ) ) return True @@ -49,7 +58,7 @@ class VolkswagenBinarySensor(VolkswagenEntity, BinarySensorEntity): @property def is_on(self): """Return True if the binary sensor is on.""" - _LOGGER.debug("Getting state of %s" % self.instrument.attr) + _LOGGER.debug("Getting state of %s", self.instrument.attr) return self.instrument.is_on @property @@ -57,7 +66,7 @@ def device_class(self) -> BinarySensorDeviceClass | str | None: """Return the device class.""" if self.instrument.device_class in DEVICE_CLASSES: return self.instrument.device_class - _LOGGER.warning(f"Unknown device class {self.instrument.device_class}") + _LOGGER.warning("Unknown device class %s", self.instrument.device_class) return None @property diff --git a/custom_components/volkswagencarnet/config_flow.py b/custom_components/volkswagencarnet/config_flow.py index 94cb43ff..559e0f6c 100644 --- a/custom_components/volkswagencarnet/config_flow.py +++ b/custom_components/volkswagencarnet/config_flow.py @@ -1,8 +1,8 @@ -import logging import asyncio +import logging -import homeassistant.helpers.config_validation as cv import voluptuous as vol + from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -14,27 +14,27 @@ ) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_get + +# pylint: disable=no-name-in-module,hass-relative-import from volkswagencarnet.vw_connection import Connection from volkswagencarnet.vw_vehicle import Vehicle from .const import ( + CONF_AVAILABLE_RESOURCES, CONF_CONVERT, CONF_DEBUG, - CONVERT_DICT, CONF_MUTABLE, + CONF_NO_CONVERSION, CONF_REGION, - CONF_REPORT_REQUEST, - CONF_REPORT_SCAN_INTERVAL, CONF_SPIN, CONF_VEHICLE, + CONVERT_DICT, + DEFAULT_DEBUG, DEFAULT_REGION, - DEFAULT_REPORT_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL, DOMAIN, - DEFAULT_DEBUG, - CONF_AVAILABLE_RESOURCES, - CONF_NO_CONVERSION, ) from .util import get_coordinator, get_vehicle @@ -53,12 +53,14 @@ class VolkswagenCarnetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config Flow.""" + VERSION = 3 task_login: asyncio.Task | None = None task_finish: asyncio.Task | None = None entry = None - def __init__(self): + def __init__(self) -> None: """Initialize.""" self._entry: ConfigEntry | None = None self._init_info = {} @@ -67,14 +69,14 @@ def __init__(self): self._session = None async def async_step_user(self, user_input=None): + """Handle user step.""" if user_input is not None: self.task_login = None - self.task_update = None self.task_finish = None self._errors = {} self._init_info = user_input - _LOGGER.debug("Creating connection to volkswagen weconnect") + _LOGGER.debug("Creating connection to Volkswagen Connect") self._connection = Connection( session=async_get_clientsession(self.hass), username=self._init_info[CONF_USERNAME], @@ -85,22 +87,27 @@ async def async_step_user(self, user_input=None): return await self.async_step_login() - return self.async_show_form(step_id="user", data_schema=vol.Schema(DATA_SCHEMA), errors=self._errors) + return self.async_show_form( + step_id="user", data_schema=vol.Schema(DATA_SCHEMA), errors=self._errors + ) # noinspection PyBroadException async def _async_task_login(self): try: await self._connection.doLogin() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught _LOGGER.error("Failed to login due to error: %s", str(e)) self._errors["base"] = "cannot_connect" if not self._connection.logged_in: self._errors["base"] = "cannot_connect" - self.hass.async_create_task(self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)) + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) async def async_step_select_vehicle(self, user_input=None): + """Handle select vehicle step.""" if user_input is not None: self._init_info[CONF_VEHICLE] = user_input[CONF_VEHICLE] @@ -114,8 +121,11 @@ async def async_step_select_vehicle(self, user_input=None): ) async def async_step_select_instruments(self, user_input=None): + """Handle select instruments step.""" instruments = self._init_info["CONF_VEHICLES"][self._init_info[CONF_VEHICLE]] - instruments_dict = {instrument.attr: instrument.name for instrument in instruments} + instruments_dict = { + instrument.attr: instrument.name for instrument in instruments + } if user_input is not None: # self._init_info[CONF_RESOURCES] = user_input[CONF_RESOURCES] @@ -140,12 +150,17 @@ async def async_step_select_instruments(self, user_input=None): step_id="select_instruments", errors=self._errors, data_schema=vol.Schema( - {vol.Optional(CONF_RESOURCES, default=list(instruments_dict.keys())): cv.multi_select(instruments_dict)} + { + vol.Optional( + CONF_RESOURCES, default=list(instruments_dict.keys()) + ): cv.multi_select(instruments_dict) + } ), last_step=True, ) async def async_step_login(self, user_input=None): + """Handle login step.""" if not self.task_login: self.task_login = self.hass.async_create_task(self._async_task_login()) @@ -158,38 +173,43 @@ async def async_step_login(self, user_input=None): # noinspection PyBroadException try: await self.task_login - except Exception: - return self.async_abort(reason="Failed to connect to WeConnect") + except Exception: # pylint: disable=broad-exception-caught + return self.async_abort(reason="Failed to connect to Volkswagen Connect") if self._errors: return self.async_show_progress_done(next_step_id="user") for vehicle in self._connection.vehicles: - _LOGGER.info(f"Found data for VIN: {vehicle.vin} from WeConnect") + _LOGGER.info("Found data for VIN: %s from Volkswagen Connect", vehicle.vin) self._init_info["CONF_VEHICLES"] = { - vehicle.vin: vehicle.dashboard().instruments for vehicle in self._connection.vehicles + vehicle.vin: vehicle.dashboard().instruments + for vehicle in self._connection.vehicles } return self.async_show_progress_done(next_step_id="select_vehicle") async def async_step_reauth(self, entry) -> dict: - """Handle initiation of re-authentication with WeConnect.""" + """Handle initiation of re-authentication with Volkswagen Connect.""" self.entry = entry return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input: dict = None) -> dict: - """Handle re-authentication with WeConnect.""" + async def async_step_reauth_confirm(self, user_input=None) -> dict: + """Handle re-authentication with Volkswagen Connect.""" errors: dict = {} if user_input is not None: - _LOGGER.debug("Creating connection to volkswagen WeConnect") + _LOGGER.debug("Creating connection to Volkswagen Connect") self._connection = Connection( session=async_get_clientsession(self.hass), username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], - fulldebug=self.entry.options.get(CONF_DEBUG, self.entry.data.get(CONF_DEBUG, DEFAULT_DEBUG)), - country=self.entry.options.get(CONF_REGION, self.entry.data[CONF_REGION]), + fulldebug=self.entry.options.get( + CONF_DEBUG, self.entry.data.get(CONF_DEBUG, DEFAULT_DEBUG) + ), + country=self.entry.options.get( + CONF_REGION, self.entry.data[CONF_REGION] + ), ) # noinspection PyBroadException @@ -198,8 +218,8 @@ async def async_step_reauth_confirm(self, user_input: dict = None) -> dict: if not await self._connection.validate_login: _LOGGER.debug( - "Unable to login to WeConnect. Need to accept a new EULA?" - "Try logging in to the portal: https://www.portal.volkswagen-we.com/" + "Unable to login to Volkswagen Connect. Need to accept a new EULA?" + "Try logging in to the portal: https://www.myvolkswagen.net/" ) errors["base"] = "cannot_connect" else: @@ -212,18 +232,24 @@ async def async_step_reauth_confirm(self, user_input: dict = None) -> dict: CONF_PASSWORD: user_input[CONF_PASSWORD], }, ) - self.hass.async_create_task(self.hass.config_entries.async_reload(self.entry.entry_id)) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) return self.async_abort(reason="reauth_successful") - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught _LOGGER.error("Failed to login due to error: %s", str(e)) - return self.async_abort(reason="Failed to connect to WeConnect") + return self.async_abort( + reason="Failed to connect to Volkswagen Connect" + ) return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema( { - vol.Required(CONF_USERNAME, default=self.entry.data[CONF_USERNAME]): str, + vol.Required( + CONF_USERNAME, default=self.entry.data[CONF_USERNAME] + ): str, vol.Required(CONF_PASSWORD): str, } ), @@ -240,7 +266,7 @@ def async_get_options_flow(config_entry): class VolkswagenCarnetOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plaato options.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize domain options flow.""" super().__init__() self._session = None @@ -263,13 +289,6 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=vol.Schema( { - vol.Optional( - CONF_REPORT_REQUEST, - default=self._config_entry.options.get( - CONF_REPORT_REQUEST, - self._config_entry.data.get(CONF_REPORT_REQUEST, False), - ), - ): cv.boolean, vol.Optional( CONF_DEBUG, default=self._config_entry.options.get( @@ -279,41 +298,40 @@ async def async_step_user(self, user_input=None): ): cv.boolean, vol.Optional( CONF_CONVERT, - default=self._config_entry.data.get(CONF_CONVERT, CONF_NO_CONVERSION), + default=self._config_entry.data.get( + CONF_CONVERT, CONF_NO_CONVERSION + ), ): vol.In(CONVERT_DICT), vol.Optional( - CONF_REPORT_SCAN_INTERVAL, + CONF_SCAN_INTERVAL, default=self._config_entry.options.get( - CONF_REPORT_SCAN_INTERVAL, + CONF_SCAN_INTERVAL, self._config_entry.data.get( - CONF_REPORT_SCAN_INTERVAL, - DEFAULT_REPORT_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL ), ), ): cv.positive_int, vol.Optional( - CONF_SCAN_INTERVAL, + CONF_REGION, default=self._config_entry.options.get( - CONF_SCAN_INTERVAL, - self._config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL), + CONF_REGION, self._config_entry.data[CONF_REGION] ), - ): cv.positive_int, - vol.Optional( - CONF_REGION, - default=self._config_entry.options.get(CONF_REGION, self._config_entry.data[CONF_REGION]), ): str, } ), ) async def async_step_select_instruments(self, user_input=None): + """Manage select instruments.""" coordinator = await get_coordinator(self.hass, self._config_entry) data = self._config_entry.as_dict() v: Vehicle = get_vehicle(coordinator=coordinator) d = v.dashboard() - instruments_dict = {instrument.attr: instrument.name for instrument in d.instruments} + instruments_dict = { + instrument.attr: instrument.name for instrument in d.instruments + } if user_input is not None: options = { @@ -322,18 +340,26 @@ async def async_step_select_instruments(self, user_input=None): CONF_AVAILABLE_RESOURCES: instruments_dict, } - removed_resources = set(data.get("options", {}).get("resources", {})) - set(options[CONF_RESOURCES]) - added_resources = set(options[CONF_RESOURCES]) - set(data.get("options", {}).get("resources", {})) + removed_resources = set(data.get("options", {}).get("resources", {})) - set( + options[CONF_RESOURCES] + ) + added_resources = set(options[CONF_RESOURCES]) - set( + data.get("options", {}).get("resources", {}) + ) - _LOGGER.info(f"Adding resources: {added_resources}, removing resources: {removed_resources}") + _LOGGER.info( + "Adding resources: %s, removing resources: %s", + added_resources, + removed_resources, + ) # Need to recreate entitiesin some cases # Some resource was removed recreate_entities = len(removed_resources) > 0 # distance conversion was changed - recreate_entities = recreate_entities or self._data[CONF_CONVERT] != data.get("data", {}).get( - CONF_CONVERT, "" - ) + recreate_entities = recreate_entities or self._data[ + CONF_CONVERT + ] != data.get("data", {}).get(CONF_CONVERT, "") if recreate_entities: entity_registry = async_get(self.hass) @@ -358,7 +384,11 @@ async def async_step_select_instruments(self, user_input=None): step_id="select_instruments", errors=self._errors, data_schema=vol.Schema( - {vol.Optional(CONF_RESOURCES, default=list(selected)): cv.multi_select(instruments_dict)} + { + vol.Optional( + CONF_RESOURCES, default=list(selected) + ): cv.multi_select(instruments_dict) + } ), last_step=True, ) diff --git a/custom_components/volkswagencarnet/const.py b/custom_components/volkswagencarnet/const.py index 599c6e5f..cce0e190 100644 --- a/custom_components/volkswagencarnet/const.py +++ b/custom_components/volkswagencarnet/const.py @@ -16,8 +16,6 @@ CONF_NO_CONVERSION = "no_conversion" CONF_CONVERT = "convert" CONF_VEHICLE = "vehicle" -CONF_REPORT_REQUEST = "report_request" -CONF_REPORT_SCAN_INTERVAL = "report_scan_interval" CONF_DEBUG = "debug" CONF_AVAILABLE_RESOURCES = "available_resources" @@ -29,7 +27,6 @@ MIN_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = 5 -DEFAULT_REPORT_UPDATE_INTERVAL = 1 CONVERT_DICT = { CONF_NO_CONVERSION: "No conversion", @@ -42,8 +39,7 @@ "binary_sensor": "binary_sensor", "lock": "lock", "device_tracker": "device_tracker", + "select": "select", "switch": "switch", "number": "number", } - -SERVICE_SET_CHARGER_MAX_CURRENT = "set_charger_max_current" diff --git a/custom_components/volkswagencarnet/device_tracker.py b/custom_components/volkswagencarnet/device_tracker.py index dc504ee8..d86dcde7 100644 --- a/custom_components/volkswagencarnet/device_tracker.py +++ b/custom_components/volkswagencarnet/device_tracker.py @@ -1,6 +1,4 @@ -""" -Support for Volkswagen WeConnect Platform -""" +"""Support for Volkswagen Connect Platform.""" import logging @@ -17,6 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """Set up the Volkswagen device tracker.""" data = hass.data[DOMAIN][entry.entry_id][DATA] coordinator = data.coordinator if coordinator.data is not None: @@ -28,14 +27,18 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): attribute=instrument.attr, ) for instrument in ( - instrument for instrument in data.instruments if instrument.component == "device_tracker" + instrument + for instrument in data.instruments + if instrument.component == "device_tracker" ) ) return True -async def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner( + hass: HomeAssistant, config, async_see, discovery_info=None +): """Set up the Volkswagen tracker.""" if discovery_info is None: return @@ -48,7 +51,7 @@ async def see_vehicle(): """Handle the reporting of the vehicle position.""" host_name = data.vehicle_name(instrument.vehicle) dev_id = f"{slugify(host_name)}" - _LOGGER.debug("Getting location of %s" % host_name) + _LOGGER.debug("Getting location of %s", host_name) await async_see( dev_id=dev_id, host_name=host_name, @@ -63,6 +66,8 @@ async def see_vehicle(): class VolkswagenDeviceTracker(VolkswagenEntity, TrackerEntity): + """Representation of a Volkswagen Device Tracker.""" + @property def latitude(self) -> float: """Return latitude value of the device.""" diff --git a/custom_components/volkswagencarnet/lock.py b/custom_components/volkswagencarnet/lock.py index f946ee4c..be4b8f54 100644 --- a/custom_components/volkswagencarnet/lock.py +++ b/custom_components/volkswagencarnet/lock.py @@ -1,16 +1,19 @@ -"""Lock support for Volkswagen WeConnect Platform.""" +"""Lock support for Volkswagen Connect Platform.""" import logging from homeassistant.components.lock import LockEntity +from homeassistant.core import HomeAssistant from . import VolkswagenEntity -from .const import DATA_KEY, DATA, DOMAIN +from .const import DATA, DATA_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None +): """Perform platform setup.""" if discovery_info is None: return @@ -18,7 +21,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([VolkswagenLock(hass.data[DATA_KEY], *discovery_info)]) -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): """Perform entity setup.""" data = hass.data[DOMAIN][entry.entry_id][DATA] coordinator = data.coordinator @@ -30,14 +33,18 @@ async def async_setup_entry(hass, entry, async_add_devices): component=instrument.component, attribute=instrument.attr, ) - for instrument in (instrument for instrument in data.instruments if instrument.component == "lock") + for instrument in ( + instrument + for instrument in data.instruments + if instrument.component == "lock" + ) ) return True class VolkswagenLock(VolkswagenEntity, LockEntity): - """Represents a Volkswagen WeConnect Lock.""" + """Represents a Volkswagen Connect Lock.""" def lock(self, **kwargs: object) -> None: """Not implemented.""" @@ -54,7 +61,7 @@ def open(self, **kwargs: object) -> None: @property def is_locked(self): """Return true if lock is locked.""" - _LOGGER.debug("Getting state of %s" % self.instrument.attr) + _LOGGER.debug("Getting state of %s", self.instrument.attr) return self.instrument.is_locked async def async_lock(self, **kwargs): diff --git a/custom_components/volkswagencarnet/manifest.json b/custom_components/volkswagencarnet/manifest.json index cce13173..a652638e 100644 --- a/custom_components/volkswagencarnet/manifest.json +++ b/custom_components/volkswagencarnet/manifest.json @@ -1,15 +1,15 @@ { - "domain": "volkswagencarnet", - "name": "Volkswagen WeConnect", - "after_dependencies": ["http"], - "codeowners": ["@robinostlund"], - "config_flow": true, - "dependencies": [], - "documentation": "https://github.com/robinostlund/homeassistant-volkswagencarnet", - "import_executor": true, - "integration_type": "hub", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/robinostlund/homeassistant-volkswagencarnet/issues", - "requirements": [], - "version": "v0.0.0" -} \ No newline at end of file + "domain": "volkswagencarnet", + "name": "Volkswagen Connect", + "after_dependencies": ["http"], + "codeowners": ["@robinostlund"], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/robinostlund/homeassistant-volkswagencarnet", + "import_executor": true, + "integration_type": "hub", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/robinostlund/homeassistant-volkswagencarnet/issues", + "requirements": [], + "version": "v0.0.0" +} diff --git a/custom_components/volkswagencarnet/number.py b/custom_components/volkswagencarnet/number.py index dbe56810..51069ef0 100644 --- a/custom_components/volkswagencarnet/number.py +++ b/custom_components/volkswagencarnet/number.py @@ -1,28 +1,27 @@ -"""Number support for Volkswagen We Connect integration.""" +"""Number support for Volkswagen Connect integration.""" import logging -from homeassistant.components.number import ( - NumberEntity, - NumberDeviceClass, -) +from homeassistant.components.number import NumberDeviceClass, NumberEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from . import VolkswagenEntity -from .const import DATA_KEY, DATA, DOMAIN, UPDATE_CALLBACK +from .const import DATA, DATA_KEY, DOMAIN, UPDATE_CALLBACK _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass: HomeAssistant, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None +): """Set up the Volkswagen number platform.""" if discovery_info is None: return async_add_entities([VolkswagenNumber(hass.data[DATA_KEY], *discovery_info)]) -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): """Set up the Volkswagen number.""" data = hass.data[DOMAIN][entry.entry_id][DATA] coordinator = data.coordinator @@ -35,7 +34,11 @@ async def async_setup_entry(hass, entry, async_add_devices): attribute=instrument.attr, callback=hass.data[DOMAIN][entry.entry_id][UPDATE_CALLBACK], ) - for instrument in (instrument for instrument in data.instruments if instrument.component == "number") + for instrument in ( + instrument + for instrument in data.instruments + if instrument.component == "number" + ) ) return True @@ -65,6 +68,7 @@ def native_step(self) -> float: @property def native_unit_of_measurement(self) -> str: + """Return unit of measurement.""" if self.instrument.unit: return self.instrument.unit return "" @@ -79,9 +83,12 @@ def native_value(self) -> float: @property def device_class(self) -> NumberDeviceClass | None: """Return the device class.""" - if self.instrument.device_class is None or self.instrument.device_class in NumberDeviceClass: + if ( + self.instrument.device_class is None + or self.instrument.device_class in NumberDeviceClass + ): return self.instrument.device_class - _LOGGER.warning(f"Unknown device class {self.instrument.device_class}") + _LOGGER.warning("Unknown device class %s", self.instrument.device_class) return None @property diff --git a/custom_components/volkswagencarnet/select.py b/custom_components/volkswagencarnet/select.py new file mode 100644 index 00000000..99d83e58 --- /dev/null +++ b/custom_components/volkswagencarnet/select.py @@ -0,0 +1,75 @@ +"""Number support for Volkswagen Connect integration.""" + +import logging + +from homeassistant.components.select import SelectEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory + +from . import VolkswagenEntity +from .const import DATA, DATA_KEY, DOMAIN, UPDATE_CALLBACK + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None +): + """Set up the Volkswagen select platform.""" + if discovery_info is None: + return + async_add_entities([VolkswagenSelect(hass.data[DATA_KEY], *discovery_info)]) + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """Set up the Volkswagen select.""" + data = hass.data[DOMAIN][entry.entry_id][DATA] + coordinator = data.coordinator + if coordinator.data is not None: + async_add_devices( + VolkswagenSelect( + data=data, + vin=coordinator.vin, + component=instrument.component, + attribute=instrument.attr, + callback=hass.data[DOMAIN][entry.entry_id][UPDATE_CALLBACK], + ) + for instrument in ( + instrument + for instrument in data.instruments + if instrument.component == "select" + ) + ) + + return True + + +class VolkswagenSelect(VolkswagenEntity, SelectEntity): + """Representation of a Volkswagen select.""" + + @property + def options(self) -> list: + """Return the options list.""" + if self.instrument.options: + return self.instrument.options + return None + + @property + def current_option(self) -> str: + """Return the current option.""" + if self.instrument.current_option: + return self.instrument.current_option + return None + + @property + def entity_category(self) -> EntityCategory | str | None: + """Return entity category.""" + if self.instrument.entity_type == "diag": + return EntityCategory.DIAGNOSTIC + if self.instrument.entity_type == "config": + return EntityCategory.CONFIG + + async def async_select_option(self, option: str) -> None: + """Update the current value.""" + await self.instrument.set_value(option) + self.notify_updated() diff --git a/custom_components/volkswagencarnet/sensor.py b/custom_components/volkswagencarnet/sensor.py index bc82283d..b10667b9 100644 --- a/custom_components/volkswagencarnet/sensor.py +++ b/custom_components/volkswagencarnet/sensor.py @@ -1,32 +1,34 @@ -"""Sensor support for Volkswagen We Connect platform.""" +"""Sensor support for Volkswagen Connect platform.""" import logging from homeassistant.components.sensor import ( - SensorEntity, - SensorStateClass, - SensorDeviceClass, DEVICE_CLASSES, STATE_CLASSES, + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from . import VolkswagenEntity -from .const import DATA_KEY, DATA, DOMAIN, UPDATE_CALLBACK +from .const import DATA, DATA_KEY, DOMAIN, UPDATE_CALLBACK _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass: HomeAssistant, config: ConfigEntry, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, config: ConfigEntry, async_add_entities, discovery_info=None +): """Set up the Volkswagen sensors.""" if discovery_info is None: return async_add_entities([VolkswagenSensor(hass.data[DATA_KEY], *discovery_info)]) -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): """Set up the entity.""" data = hass.data[DOMAIN][entry.entry_id][DATA] coordinator = data.coordinator @@ -39,20 +41,24 @@ async def async_setup_entry(hass, entry, async_add_devices): attribute=instrument.attr, callback=hass.data[DOMAIN][entry.entry_id][UPDATE_CALLBACK], ) - for instrument in (instrument for instrument in data.instruments if instrument.component == "sensor") + for instrument in ( + instrument + for instrument in data.instruments + if instrument.component == "sensor" + ) ) return True class VolkswagenSensor(VolkswagenEntity, SensorEntity): - """Representation of a Volkswagen WeConnect Sensor.""" + """Representation of a Volkswagen Connect Sensor.""" @property def native_value(self): """Return the state of the sensor.""" if self.instrument is not None: - _LOGGER.debug("Getting state of %s" % self.instrument.attr) + _LOGGER.debug("Getting state of %s", self.instrument.attr) else: _LOGGER.debug("Getting state of of a broken entity?") return "" @@ -68,17 +74,23 @@ def native_unit_of_measurement(self): @property def device_class(self) -> SensorDeviceClass | None: """Return the device class.""" - if self.instrument.device_class is None or self.instrument.device_class in DEVICE_CLASSES: + if ( + self.instrument.device_class is None + or self.instrument.device_class in DEVICE_CLASSES + ): return self.instrument.device_class - _LOGGER.warning(f"Unknown device class {self.instrument.device_class}") + _LOGGER.warning("Unknown device class %s", self.instrument.device_class) return None @property def state_class(self) -> SensorStateClass | None: """Return the state class.""" - if self.instrument.state_class is None or self.instrument.state_class in STATE_CLASSES: + if ( + self.instrument.state_class is None + or self.instrument.state_class in STATE_CLASSES + ): return self.instrument.state_class - _LOGGER.warning(f"Unknown state class {self.instrument.state_class}") + _LOGGER.warning("Unknown state class %s", self.instrument.state_class) return None @property diff --git a/custom_components/volkswagencarnet/services.py b/custom_components/volkswagencarnet/services.py deleted file mode 100644 index 04e5dfd5..00000000 --- a/custom_components/volkswagencarnet/services.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Services exposed to Home Assistant.""" - -import logging -import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv - -from .util import get_coordinator_by_device_id, get_vehicle, validate_charge_max_current - -_LOGGER = logging.getLogger(__name__) - -SERVICE_SET_CHARGER_MAX_CURRENT_SCHEMA = vol.Schema( - { - vol.Optional("device_id"): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Optional("max_current"): vol.In([5, 10, 13, 16, 32, "5", "10", "13", "16", "32", "reduced", "max"]), - }, - extra=vol.ALLOW_EXTRA, # FIXME, should not be needed -) - - -class ChargerService: - """Charger services class.""" - - def __init__(self, hass: HomeAssistant): - """Init.""" - self.hass: HomeAssistant = hass - - async def set_charger_max_current(self, service_call: ServiceCall) -> bool: - """Service for setting max charging current.""" - c = await get_coordinator_by_device_id(self.hass, service_call.data.get("device_id")) - v = get_vehicle(c) - - # parse service call - level = validate_charge_max_current(service_call.data.get("max_current", None)) - - if level is None: - raise ValueError("Can't change value to None") - - # Apply - res = await v.set_charger_current(level) - self.hass.add_job(c.async_request_refresh) - return res diff --git a/custom_components/volkswagencarnet/services.yaml b/custom_components/volkswagencarnet/services.yaml deleted file mode 100644 index 2c7a44e3..00000000 --- a/custom_components/volkswagencarnet/services.yaml +++ /dev/null @@ -1,262 +0,0 @@ -set_charger_max_current: - name: Set the maximum charging current - description: > - Set the maximum charging current for manually started slow (AC) charges. - fields: - device_id: - name: Vehicle - description: Vehicle device id - required: true - selector: - device: - integration: volkswagencarnet - max_current: - name: Max current - description: Charger max current (A) - example: 13 - required: true - selector: - select: - options: - - '5' - - '10' - - '13' - - '16' - - '32' - - 'reduced' - - 'max' - -set_timer_basic_settings: - name: Set the basic departure timer settings - description: > - Set the target temperature and minimum limit that the car will immediately - charge to when a departure timer is active. This minimum limit overrides - most other settings, including off-peak times. - Provide at least one of the properties. - fields: - device_id: - name: Vehicle - description: The vehicle to set charge limit for - required: true - selector: - device: - integration: volkswagencarnet - min_level: - name: Charge minimum level - description: The minimum charge level (0-100%) - example: 50 - required: false - selector: - number: - min: 0 - max: 100 - step: 10 - unit_of_measurement: "%" - target_temperature: - name: Temperature - description: Climatisation target temperature in system temperature unit (°C/°F) - example: 24.5 - required: false - selector: - number: - mode: box - min: 15 - max: 87 - step: 0.5 - unit_of_measurement: "C/F" -# heater_source: -# name: Heater source -# description: Select heater source (only if auxiliary heater is installed) -# example: 'electric' -# required: false -# selector: -# select: -# options: -# - 'electric' -# - 'auxiliary' - -update_schedule: - name: Update departure schedule - description: > - Update a departure schedule - fields: - device_id: - name: Vehicle - description: The vehicle device - required: true - selector: - device: - integration: volkswagencarnet - timer_id: - name: Timer id - description: The timer id (1, 2 or 3) - example: 1 - required: true - selector: - number: - min: 1 - max: 3 - step: 1 - profile_id: - name: Charging profile - description: The charging profile id (1..10) to use - example: 3 - required: false - selector: - number: - min: 1 - max: 10 - step: 1 - enabled: - name: Timer enabled - description: Enable this timer - example: 3 - required: false - selector: - boolean: - frequency: - name: Timer frequency - description: cyclic / single - example: "cyclic" - required: false - selector: - select: - options: - - "cyclic" - - "single" - departure_time: - name: Departure time - description: "hh:mm for cyclic frequency" - example: "06:45" - required: false - selector: - time: - departure_datetime: - name: Departure time - description: "timestamp for single" - example: "2022-02-22T06:45:00+0200" - required: false - selector: - text: - weekday_mask: - name: Weekday mask - description: Weekdays to apply the cyclic timer - example: "yyyyynn" - required: false - selector: - text: - -update_profile: - name: Update charging profile - description: > - Update a charging profile - fields: - device_id: - name: Vehicle - description: The vehicle device - required: true - selector: - device: - integration: volkswagencarnet - profile_id: - name: Charging profile - description: The charging profile id (1..10) to update - example: 3 - required: true - selector: - select: - options: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - - '9' - - '10' - profile_name: - name: Name - description: A descriptive name - example: Garage at night - required: false - selector: - text: - charging: - name: Charging - description: Enable battery charging - example: true - required: false - selector: - boolean: - climatisation: - name: Climatisation - description: Enable climatisation - example: true - required: false - selector: - boolean: - night_rate: - name: Night rate - description: Enable off-peak charging - example: true - required: false - selector: - boolean: - night_rate_start: - name: Night rate start time - description: "hh:mm (24h format)" - example: "21:45" - required: false - selector: - time: - night_rate_end: - name: Night rate end time - description: "hh:mm (24h format)" - example: "06:00" - required: false - selector: - time: - charge_max_current: - name: Max charge current - description: 0 = max, else 5/10/16/other car supported value. No idea what happens for wrong values... - example: 16 - required: false - selector: - select: - options: - - '5' - - '10' - - '13' - - '16' - - '32' - target_level: - name: Battery target level % - description: Charge battery to this level - example: '70' - required: false - selector: - select: - options: - - '0' - - '10' - - '20' - - '30' - - '40' - - '50' - - '60' - - '70' - - '80' - - '90' - - '100' -# heater_source: -# name: Heater source -# description: Select heater source (only if auxiliary heater is installed) -# example: 'electric' -# required: false -# selector: -# select: -# options: -# - 'electric' -# - 'auxiliary' diff --git a/custom_components/volkswagencarnet/strings.json b/custom_components/volkswagencarnet/strings.json index 4d3edc79..8bd19a2c 100644 --- a/custom_components/volkswagencarnet/strings.json +++ b/custom_components/volkswagencarnet/strings.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "We Connect Configuration", - "description": "Fill in We Connect information\n\nS-PIN is required for some specific options such as lock/unlock and combustion engine heating start/stop", + "title": "Volkswagen Connect Configuration", + "description": "Fill in Volkswagen Connect information\n\nS-PIN is required for some specific options such as lock/unlock and combustion engine heating start/stop", "data": { "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Re-authenticate with your We Connect account. Make sure to accept any new EULA in the We Connect portal (https://www.portal.volkswagen-we.com/) before proceeding. ", + "description": "Re-authenticate with your Volkswagen Connect account. Make sure to accept any new EULA in the Volkswagen Connect portal (https://www.myvolkswagen.net/) before proceeding. ", "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } @@ -43,23 +43,21 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "cannot_connect": "Could not login to Volkswagen We Connect, please check your credentials and verify that the service is working", - "cannot_update": "Could not query update from Volkswagen We Connect", + "cannot_connect": "Could not login to Volkswagen Connect, please check your credentials and verify that the service is working", + "cannot_update": "Could not query update from Volkswagen Connect", "unknown": "[%key:common::config_flow::error::unknown%]" }, "progress": { - "task_login": "Logging in to We Connect", + "task_login": "Logging in to Volkswagen Connect", "task_update": "Fetching vehicles" } }, "options": { "step": { "user": { - "title": "Options for We Connect", + "title": "Options for Volkswagen Connect", "description": "Configure update intervals", "data": { - "report_request": "Enable automatic report request", - "report_scan_interval": "Report request update interval (days)", "scan_interval": "Sensors update interval (minutes)", "region": "Region (The country where the car was sold)", "debug": "Enable full debug logs from API (requires debug logging enabled in config)", diff --git a/custom_components/volkswagencarnet/switch.py b/custom_components/volkswagencarnet/switch.py index 042ce8cf..a2876385 100644 --- a/custom_components/volkswagencarnet/switch.py +++ b/custom_components/volkswagencarnet/switch.py @@ -1,17 +1,19 @@ -"""Support for Volkswagen WeConnect Platform.""" +"""Support for Volkswagen Connect Platform.""" import logging from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import ToggleEntity, EntityCategory +from homeassistant.helpers.entity import EntityCategory, ToggleEntity -from . import VolkswagenEntity, VolkswagenData +from . import VolkswagenData, VolkswagenEntity from .const import DATA, DATA_KEY, DOMAIN, UPDATE_CALLBACK _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass: HomeAssistant, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None +): """Set up the volkswagen switch platform.""" if discovery_info is None: return @@ -31,14 +33,19 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): attribute=instrument.attr, callback=hass.data[DOMAIN][entry.entry_id][UPDATE_CALLBACK], ) - for instrument in (instrument for instrument in data.instruments if instrument.component == "switch") + for instrument in ( + instrument + for instrument in data.instruments + if instrument.component == "switch" + ) ) return True class VolkswagenSwitch(VolkswagenEntity, ToggleEntity): - """Representation of a Volkswagen WeConnect Switch.""" + """Representation of a Volkswagen Connect Switch.""" + # pylint: disable=useless-parent-delegation def __init__( self, data: VolkswagenData, @@ -46,7 +53,7 @@ def __init__( component: str, attribute: str, callback=None, - ): + ) -> None: """Initialize switch.""" super().__init__(data, vin, component, attribute, callback) @@ -61,18 +68,18 @@ def turn_off(self, **kwargs: object) -> None: @property def is_on(self): """Return true if switch is on.""" - _LOGGER.debug("Getting state of %s" % self.instrument.attr) + _LOGGER.debug("Getting state of %s", self.instrument.attr) return self.instrument.state async def async_turn_on(self, **kwargs): """Turn the switch on.""" - _LOGGER.debug("Turning ON %s." % self.instrument.attr) + _LOGGER.debug("Turning ON %s", self.instrument.attr) await self.instrument.turn_on() self.notify_updated() async def async_turn_off(self, **kwargs): """Turn the switch off.""" - _LOGGER.debug("Turning OFF %s." % self.instrument.attr) + _LOGGER.debug("Turning OFF %s", self.instrument.attr) await self.instrument.turn_off() self.notify_updated() diff --git a/custom_components/volkswagencarnet/translations/da.json b/custom_components/volkswagencarnet/translations/da.json index 10f94435..70332674 100644 --- a/custom_components/volkswagencarnet/translations/da.json +++ b/custom_components/volkswagencarnet/translations/da.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "We Connect konfiguration", - "description": "Udfyld We Connect detaljer\n\nS-PIN kræves for visse optioner som f.eks. lås/oplås samt start/stop af kabinevarmer", + "title": "Volkswagen Connect konfiguration", + "description": "Udfyld Volkswagen Connect detaljer\n\nS-PIN kræves for visse optioner som f.eks. lås/oplås samt start/stop af kabinevarmer", "data": { "username": "Email", "password": "Adgangskode", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Log ind igen med din We Connect konto. Vær sikker på at acceptere nye EULA i We Connect portalen (https://www.portal.volkswagen-we.com/) før du fortsætter. ", + "description": "Log ind igen med din Volkswagen Connect konto. Vær sikker på at acceptere nye EULA i Volkswagen Connect portalen (https://www.myvolkswagen.net/) før du fortsætter. ", "email": "Email", "password": "Adgangskode" } @@ -43,23 +43,21 @@ "reauth_successful": "Log ind var succesfuld" }, "error": { - "cannot_connect": "Der kunne ikke logges ind i Volkswagen We Connect, venligst kontroller dine log ind oplysninger og at servicen svarer", - "cannot_update": "Kunne ikke hente opdatering fra Volkswagen We Connect", + "cannot_connect": "Der kunne ikke logges ind i Volkswagen Connect, venligst kontroller dine log ind oplysninger og at servicen svarer", + "cannot_update": "Kunne ikke hente opdatering fra Volkswagen Connect", "unknown": "Der opstod en ukendt fejl" }, "progress": { - "task_login": "Logger ind til We Connect", + "task_login": "Logger ind til Volkswagen Connect", "task_update": "Henter køretøjer" } }, "options": { "step": { "user": { - "title": "We Connect indstillinger", + "title": "Volkswagen Connect indstillinger", "description": "Konfigurer opdateringsinterval", "data": { - "report_request": "Slå automatisk rapportforespørgsel til", - "report_scan_interval": "Rapport forespørgselsinterval (dage)", "scan_interval": "Sensor opdateringsinterval (minutter)", "region": "Region (Landet hvor bilen blev solgt)", "debug": "Tillad fuld fejlfindingslog fra API (kræver at fejlfindingslogføring er slået til)", diff --git a/custom_components/volkswagencarnet/translations/en.json b/custom_components/volkswagencarnet/translations/en.json index fcf03b1f..8791e1c7 100644 --- a/custom_components/volkswagencarnet/translations/en.json +++ b/custom_components/volkswagencarnet/translations/en.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "We Connect Configuration", - "description": "Fill in We Connect information\n\nS-PIN is required for some specific options such as lock/unlock and combustion engine heating start/stop", + "title": "Volkswagen Connect Configuration", + "description": "Fill in Volkswagen Connect information\n\nS-PIN is required for some specific options such as lock/unlock and combustion engine heating start/stop", "data": { "username": "Email", "password": "Password", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Re-authenticate with your We Connect account. Make sure to accept any new EULA in the We Connect portal (https://www.portal.volkswagen-we.com/) before proceeding. ", + "description": "Re-authenticate with your Volkswagen Connect account. Make sure to accept any new EULA in the Volkswagen Connect portal (https://www.myvolkswagen.net/) before proceeding. ", "email": "Email", "password": "Password" } @@ -43,23 +43,21 @@ "reauth_successful": "Re-authentication was successful" }, "error": { - "cannot_connect": "Could not login to Volkswagen We Connect, please check your credentials and verify that the service is working", - "cannot_update": "Could not query update from Volkswagen We Connect", + "cannot_connect": "Could not login to Volkswagen Connect, please check your credentials and verify that the service is working", + "cannot_update": "Could not query update from Volkswagen Connect", "unknown": "An unknown error occurred" }, "progress": { - "task_login": "Logging in to We Connect", + "task_login": "Logging in to Volkswagen Connect", "task_update": "Fetching vehicles" } }, "options": { "step": { "user": { - "title": "Options for We Connect", + "title": "Options for Volkswagen Connect", "description": "Configure update intervals", "data": { - "report_request": "Enable automatic report request", - "report_scan_interval": "Report request update interval (days)", "scan_interval": "Sensors update interval (minutes)", "region": "Region (The country where the car was sold)", "debug": "Enable full debug logs from API (requires debug logging enabled in config)", diff --git a/custom_components/volkswagencarnet/translations/fi.json b/custom_components/volkswagencarnet/translations/fi.json index df6d8f68..15512809 100644 --- a/custom_components/volkswagencarnet/translations/fi.json +++ b/custom_components/volkswagencarnet/translations/fi.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "We Connect Asetukset", - "description": "Täydennä We Connect tiedot\n\nS-PIN vaaditaan joihinkin toimintoihin, esim. lukituksen hallintaan ja polttomoottorin lämmityksen käynnistämiseen", + "title": "Volkswagen Connect Asetukset", + "description": "Täydennä Volkswagen Connect tiedot\n\nS-PIN vaaditaan joihinkin toimintoihin, esim. lukituksen hallintaan ja polttomoottorin lämmityksen käynnistämiseen", "data": { "username": "Sähköposti", "password": "Salasana", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Kirjausu uudelleen WeConnectiin. Varmista että olet hyväksynyt mahdolliset uudet WeConnect portaalin käyttöehdot (https://www.portal.volkswagen-we.com/) ensin. ", + "description": "Kirjausu uudelleen Volkswagen Connectiin. Varmista että olet hyväksynyt mahdolliset uudet Volkswagen Connect portaalin käyttöehdot (https://www.myvolkswagen.net/) ensin. ", "username": "Sähköposti", "password": "Salasana" } @@ -43,23 +43,21 @@ "reauth_successful": "Uudelleenautentikointi onnistui" }, "error": { - "cannot_connect": "Kirjautuminen Volkswagen We Connect-palveluun epäonnistui, tarkista käyttäjätiedot sekä varmista että palvelu on käytössä ja toimii", - "cannot_update": "Tietojen päivitys Volkswagen We Connect-palvelusta epäonnistui", + "cannot_connect": "Kirjautuminen Volkswagen Connect-palveluun epäonnistui, tarkista käyttäjätiedot sekä varmista että palvelu on käytössä ja toimii", + "cannot_update": "Tietojen päivitys Volkswagen Connect-palvelusta epäonnistui", "unknown": "[%key:common::config_flow::error::unknown%]" }, "progress": { - "task_login": "Kirjaudutaan We Connect-palveluun", + "task_login": "Kirjaudutaan Volkswagen Connect-palveluun", "task_update": "Haetaan ajoneuvoja" } }, "options": { "step": { "user": { - "title": "We Connect Asetukset", + "title": "Volkswagen Connect Asetukset", "description": "Aseta päivitysväli", "data": { - "report_request": "Ota automaattinen raporttipäivitys käyttöön", - "report_scan_interval": "Raportointiväli (päivissä)", "scan_interval": "Anturidatan päivitysväli (minuuteissa)", "region": "Maa jossa ajoneuvo myytiin (ISO 3155-1 / Alpha-2)", "debug": "Ota täysi API:n debuglogitus käyttöön (debug-taso pitää olla asetettu)", diff --git a/custom_components/volkswagencarnet/translations/nb.json b/custom_components/volkswagencarnet/translations/nb.json index 61447f21..211ed86b 100644 --- a/custom_components/volkswagencarnet/translations/nb.json +++ b/custom_components/volkswagencarnet/translations/nb.json @@ -1,78 +1,76 @@ { - "title": "Volkswagen We Connect", - "config": { - "step": { - "user": { - "title": "We Connect konfigurasjon", - "description": "Fyll inn We Connect informasion\n\nS-PIN er nødvendig for enkelte valg som å låse opp/igjen, og starte/stoppe oppvarming med drivstoff", - "data": { - "username": "E-post", - "password": "Passord", - "mutable": "Fjern hake for å bare lese verdier. Hak av dersom du vil sende kommandoer til bilen", - "region": "Landkode for landet der bilen ble solgt (ISO 3155-1 / Alpha-2)", - "spin": "S-PIN", - "name": "Gi bilen et kallenavn (Standard er VIN number)", - "debug": "Aktiver full debug-logg fra API (krever feilsøkingslogging aktivert i config)", - "convert": "Velg om du vil gjøre noen avstandsenhetskonverteringer" - } - }, - "select_vehicle": { - "title": "Kjøretøy", - "description": "Følgende kjøretøy ble funnet. Velg kjøretøyet du ønsker å overvåke", - "data": { - "vehicle": "VIN nummer" - } - }, - "select_instruments": { - "title": "Instrumenter", - "description": "Velg instrumenter som skal overvåkes", - "data": { - "resources": "Velg ressurser som skal overvåkes" - } - }, - "reauth_confirm": { - "data": { - "description": "Autentiser på nytt med WeConnect-kontoen din. Sørg for å godta enhver ny EULA i WeConnect-portalen (https://www.portal.volkswagen-we.com/) før du fortsetter.", - "username": "E-post", - "password": "Passord" - } + "title": "Volkswagen Connect", + "config": { + "step": { + "user": { + "title": "Volkswagen Connect konfigurasjon", + "description": "Fyll inn Volkswagen Connect informasion\n\nS-PIN er nødvendig for enkelte valg som å låse opp/igjen, og starte/stoppe oppvarming med drivstoff", + "data": { + "username": "E-post", + "password": "Passord", + "mutable": "Fjern hake for å bare lese verdier. Hak av dersom du vil sende kommandoer til bilen", + "region": "Landkode for landet der bilen ble solgt (ISO 3155-1 / Alpha-2)", + "spin": "S-PIN", + "name": "Gi bilen et kallenavn (Standard er VIN number)", + "debug": "Aktiver full debug-logg fra API (krever feilsøkingslogging aktivert i config)", + "convert": "Velg om du vil gjøre noen avstandsenhetskonverteringer" } }, - "abort": { - "already_configured": "Bil med dette VIN-nummeret er allerede konfigurert", - "reauth_successful": "Re-autentisering var vellykket" + "select_vehicle": { + "title": "Kjøretøy", + "description": "Følgende kjøretøy ble funnet. Velg kjøretøyet du ønsker å overvåke", + "data": { + "vehicle": "VIN nummer" + } }, - "error": { - "cannot_connect": "Kunne ikke logge på Volkswagen We Connect, sjekk legitimasjonen din og bekreft at tjenesten fungerer", - "cannot_update": "Kunne ikke søke etter oppdatering fra Volkswagen We Connect", - "unknown": "[%key:common::config_flow::error::unknown%]" + "select_instruments": { + "title": "Instrumenter", + "description": "Velg instrumenter som skal overvåkes", + "data": { + "resources": "Velg ressurser som skal overvåkes" + } }, - "progress": { - "task_login": "Logger på We Connect", - "task_update": "Henter kjøretøy" + "reauth_confirm": { + "data": { + "description": "Autentiser på nytt med Volkswagen Connect-kontoen din. Sørg for å godta enhver ny EULA i Volkswagen Connect-portalen (https://www.myvolkswagen.net/) før du fortsetter.", + "username": "E-post", + "password": "Passord" + } } }, - "options": { - "step": { - "user": { - "title": "Alternativer for We Connect", - "description": "Konfigurer oppdateringsintervaller", - "data": { - "report_request": "Aktiver automatisk rapportforespørsel", - "report_scan_interval": "Oppdateringsintervall for rapportforespørsel (dager)", - "scan_interval": "Sensorenes oppdateringsintervall (minutter)", - "region": "Region (landet der bilen ble solgt)", - "debug": "Aktiver fullstendige feilsøkingslogger fra API (krever feilsøkingslogging aktivert i config)", - "convert": "Velg om du vil gjøre noen avstandsenhetskonverteringer" - } - }, - "select_instruments": { - "title": "Instrumenter", - "description": "Velg instrumenter som skal overvåkes", - "data": { - "resources": "Velg ressurser som skal overvåkes" - } + "abort": { + "already_configured": "Bil med dette VIN-nummeret er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" + }, + "error": { + "cannot_connect": "Kunne ikke logge på Volkswagen Connect, sjekk legitimasjonen din og bekreft at tjenesten fungerer", + "cannot_update": "Kunne ikke søke etter oppdatering fra Volkswagen Connect", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "progress": { + "task_login": "Logger på Volkswagen Connect", + "task_update": "Henter kjøretøy" + } + }, + "options": { + "step": { + "user": { + "title": "Alternativer for Volkswagen Connect", + "description": "Konfigurer oppdateringsintervaller", + "data": { + "scan_interval": "Sensorenes oppdateringsintervall (minutter)", + "region": "Region (landet der bilen ble solgt)", + "debug": "Aktiver fullstendige feilsøkingslogger fra API (krever feilsøkingslogging aktivert i config)", + "convert": "Velg om du vil gjøre noen avstandsenhetskonverteringer" + } + }, + "select_instruments": { + "title": "Instrumenter", + "description": "Velg instrumenter som skal overvåkes", + "data": { + "resources": "Velg ressurser som skal overvåkes" } } } } +} diff --git a/custom_components/volkswagencarnet/translations/nn.json b/custom_components/volkswagencarnet/translations/nn.json index ba5cfd18..06ac0090 100644 --- a/custom_components/volkswagencarnet/translations/nn.json +++ b/custom_components/volkswagencarnet/translations/nn.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "We Connect konfigurasjon", - "description": "Fyll inn We Connect informasjon\n\nS-PIN er påkrevd for nokre val som for eksempel låsing/opplåsing og start/stopp av motorvarmar", + "title": "Volkswagen Connect konfigurasjon", + "description": "Fyll inn Volkswagen Connect informasjon\n\nS-PIN er påkrevd for nokre val som for eksempel låsing/opplåsing og start/stopp av motorvarmar", "data": { "username": "E-post", "password": "Passord", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Autentiser deg på nytt med WeConnect-kontoen din. Pass på at du godtar einkvar ny EULA i WeConnect-portalen (https://www.portal.volkswagen-we.com/) før du går vidare.", + "description": "Autentiser deg på nytt med Volkswagen Connect-kontoen din. Pass på at du godtar einkvar ny EULA i Volkswagen Connect-portalen (https://www.myvolkswagen.net/) før du går vidare.", "username": "E-post", "password": "Passord" } @@ -43,23 +43,21 @@ "reauth_successful": "Re-autentisering var vellykka" }, "error": { - "cannot_connect": "Klarte ikkje å logge inn på Volkswagen We Connect, sjekk brukarnamn/passord og verifiser at tenesta fungerer", - "cannot_update": "Klarte ikkje å hente oppdatering frå Volkswagen We Connect", + "cannot_connect": "Klarte ikkje å logge inn på Volkswagen Connect, sjekk brukarnamn/passord og verifiser at tenesta fungerer", + "cannot_update": "Klarte ikkje å hente oppdatering frå Volkswagen Connect", "unknown": "[%key:common::config_flow::error::unknown%]" }, "progress": { - "task_login": "Loggar inn på We Connect", + "task_login": "Loggar inn på Volkswagen Connect", "task_update": "Hentar køyretøy" } }, "options": { "step": { "user": { - "title": "Val for We Connect", + "title": "Val for Volkswagen Connect", "description": "Konfigurer oppdateringsintervall", "data": { - "report_request": "Aktiver automatisk rapportforespurnad", - "report_scan_interval": "Oppdateringsintervall for rapportforespurnad (dagar)", "scan_interval": "Oppdateringsintervall for sensorar (minutt)", "region": "Region (landet der bilen vart solgt)", "debug": "Skru på debug logger frå API (krev at debug logging er skrudd på i konfigurasjonen", diff --git a/custom_components/volkswagencarnet/translations/pl.json b/custom_components/volkswagencarnet/translations/pl.json index 10fac4b8..6e66d08b 100644 --- a/custom_components/volkswagencarnet/translations/pl.json +++ b/custom_components/volkswagencarnet/translations/pl.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "Konfiguracja We Connect", - "description": "Wypełnij informacje dla We Connect\n\nS-PIN jest wymagany w przypadku niektórych opcji, takich jak blokowanie/odblokowywanie auta i włączanie/wyłączanie silnika", + "title": "Konfiguracja Connect", + "description": "Wypełnij informacje dla Volkswagen Connect\n\nS-PIN jest wymagany w przypadku niektórych opcji, takich jak blokowanie/odblokowywanie auta i włączanie/wyłączanie silnika", "data": { "username": "Email", "password": "Hasło", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Ponownie uwierzytelnij się do swojego konta We Connect. Przed kontynuowaniem należy zaakceptować każdą nową umowę EULA w portalu We Connect (https://www.portal.volkswagen-we.com/).", + "description": "Ponownie uwierzytelnij się do swojego konta Volkswagen Connect. Przed kontynuowaniem należy zaakceptować każdą nową umowę EULA w portalu Volkswagen Connect (https://www.myvolkswagen.net/).", "username": "Email", "password": "Hasło" } @@ -43,23 +43,21 @@ "reauth_successful": "Ponowne uwierzytelnienie powiodło się" }, "error": { - "cannot_connect": "Nie można zalogować się do Volkswagen We Connect, sprawdź wpisane dane logowania i sprawdź, czy usługa działa", - "cannot_update": "Nie można zaktualizować danych Volkswagen We Connect", + "cannot_connect": "Nie można zalogować się do Volkswagen Connect, sprawdź wpisane dane logowania i sprawdź, czy usługa działa", + "cannot_update": "Nie można zaktualizować danych Volkswagen Connect", "unknown": "[%key:common::config_flow::error::unknown%]" }, "progress": { - "task_login": "Logowanie do We Connect", + "task_login": "Logowanie do Volkswagen Connect", "task_update": "Pobieranie listy pojazdów" } }, "options": { "step": { "user": { - "title": "Opcje We Connect", + "title": "Opcje Volkswagen Connect", "description": "Skonfiguruj częstotliwość aktualizacji", "data": { - "report_request": "Włącz automatyczne żądanie raportu", - "report_scan_interval": "Częstotliwość aktualizacji żądania raportu (dni)", "scan_interval": "Częstotliwość aktualizacji czujników (minuty)", "region": "Region (kraj, w którym samochód został sprzedany)", "debug": "Włącz pełne dzienniki debugowania z interfejsu API (wymaga włączenia rejestrowania debugowania w konfiguracji)", diff --git a/custom_components/volkswagencarnet/translations/pt-BR.json b/custom_components/volkswagencarnet/translations/pt-BR.json index f80f1af7..5c666b2b 100644 --- a/custom_components/volkswagencarnet/translations/pt-BR.json +++ b/custom_components/volkswagencarnet/translations/pt-BR.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "Configurações We Connect", - "description": "Preencha as informações do We Connect\n\nS-PIN é necessário para algumas opções específicas, como travar/desbloquear e partida/parada do aquecimento do motor de combustão", + "title": "Configurações Volkswagen Connect", + "description": "Preencha as informações do Volkswagen Connect\n\nS-PIN é necessário para algumas opções específicas, como travar/desbloquear e partida/parada do aquecimento do motor de combustão", "data": { "username": "Email", "password": "Senha", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Re-autentique com sua conta WeConnect. Certifique-se de aceitar qualquer novo EULA no portal WeConnect (https://www.portal.volkswagen-we.com/) antes de continuar. ", + "description": "Re-autentique com sua conta Volkswagen Connect. Certifique-se de aceitar qualquer novo EULA no portal Volkswagen Connect (https://www.myvolkswagen.net/) antes de continuar. ", "username": "Email", "password": "Senha" } @@ -43,23 +43,21 @@ "reauth_successful": "A re-autenticação foi bem-sucedida" }, "error": { - "cannot_connect": "Não foi possível fazer login no Volkswagen We Connect, verifique suas credenciais e verifique se o serviço está funcionando", - "cannot_update": "Não foi possível consultar a atualização do Volkswagen We Connect", + "cannot_connect": "Não foi possível fazer login no Volkswagen Connect, verifique suas credenciais e verifique se o serviço está funcionando", + "cannot_update": "Não foi possível consultar a atualização do Volkswagen Connect", "unknown": "[%key:common::config_flow::error::unknown%]" }, "progress": { - "task_login": "Entrando no We Connect", + "task_login": "Entrando no Volkswagen Connect", "task_update": "Buscando veículos" } }, "options": { "step": { "user": { - "title": "Opções para We Connect", + "title": "Opções para Volkswagen Connect", "description": "Configurar intervalos de atualização", "data": { - "report_request": "Ativar solicitação de relatório automático", - "report_scan_interval": "Intervalo de atualização da solicitação de relatório (dias)", "scan_interval": "Intervalo de atualização dos sensores (minutos)", "region": "Região (o país onde o carro foi vendido)", "debug": "Habilitar logs de depuração completos da API (requer log de depuração habilitado nas configurações)", diff --git a/custom_components/volkswagencarnet/translations/ru.json b/custom_components/volkswagencarnet/translations/ru.json index 2ed42078..17297c5f 100644 --- a/custom_components/volkswagencarnet/translations/ru.json +++ b/custom_components/volkswagencarnet/translations/ru.json @@ -1,9 +1,9 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "We Connect Настройка", + "title": "Volkswagen Connect Настройка", "description": "Заполните учётные данные\n\nS-PIN требуется для некоторых отдельных операций таких как блокировка/разблокировка и операций включения предварительного обогрева двигателя.", "data": { "username": "Email", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Повторно войдите в свою учетную запись We Connect. Перед продолжением обязательно примите новое лицензионное соглашение с конечным пользователем на портале We Connect (https://www.portal.volkswagen-we.com/). ", + "description": "Повторно войдите в свою учетную запись Volkswagen Connect. Перед продолжением обязательно примите новое лицензионное соглашение с конечным пользователем на портале Volkswagen Connect (https://www.myvolkswagen.net/). ", "email": "Email", "password": "Password" } @@ -43,23 +43,21 @@ "reauth_successful": "Повторный вход успешен" }, "error": { - "cannot_connect": "Не удалось войти в Volkswagen We Connect, проверьте свои учетные данные и убедитесь, что служба работает", - "cannot_update": "Не возможно обновить данные с Volkswagen We Connect", + "cannot_connect": "Не удалось войти в Volkswagen Connect, проверьте свои учетные данные и убедитесь, что служба работает", + "cannot_update": "Не возможно обновить данные с Volkswagen Connect", "unknown": "Получена неизвестная ошибка" }, "progress": { - "task_login": "Вход в We Connect", + "task_login": "Вход в Volkswagen Connect", "task_update": "Обновление данных автомобиля" } }, "options": { "step": { "user": { - "title": "Настройки для We Connect", + "title": "Настройки для Volkswagen Connect", "description": "Настройте частоту обновлений", "data": { - "report_request": "Включить автоматический запрос отчётов", - "report_scan_interval": "Интервал запроса обновления отчётов (дни)", "scan_interval": "Интервал обновления значений (минуты)", "region": "Регион (Страна где был продан автомобиль)", "debug": "Включить полное журналирование отладки API (требует включённого отладочного логирования в файле настроек)", diff --git a/custom_components/volkswagencarnet/translations/sl.json b/custom_components/volkswagencarnet/translations/sl.json index fbbfb062..fe12e84a 100644 --- a/custom_components/volkswagencarnet/translations/sl.json +++ b/custom_components/volkswagencarnet/translations/sl.json @@ -1,10 +1,10 @@ { - "title": "Volkswagen We Connect", + "title": "Volkswagen Connect", "config": { "step": { "user": { - "title": "Konfiguracija We Connect", - "description": "Izpolnite informacije We Connect\n\nZa nekatere posebne možnosti, kot so zaklepanje/odklepanje in zagon/ustavitev ogrevanja motorja, je potreben S-PIN", + "title": "Konfiguracija Volkswagen Connect", + "description": "Izpolnite informacije Volkswagen Connect\n\nZa nekatere posebne možnosti, kot so zaklepanje/odklepanje in zagon/ustavitev ogrevanja motorja, je potreben S-PIN", "data": { "username": "E-pošta", "password": "Geslo", @@ -32,7 +32,7 @@ }, "reauth_confirm": { "data": { - "description": "Ponovno se prijavite v svoj We Connect račun. Poskrbite, da sprejmete morebitno novo licenco EULA v portalu We Connect (https://www.portal.volkswagen-we.com/), preden nadaljujete. ", + "description": "Ponovno se prijavite v svoj Volkswagen Connect račun. Poskrbite, da sprejmete morebitno novo licenco EULA v portalu Volkswagen Connect (https://www.myvolkswagen.net/), preden nadaljujete. ", "email": "E-pošta", "password": "Geslo" } @@ -43,23 +43,21 @@ "reauth_successful": "Ponovna overitev je bila uspešna" }, "error": { - "cannot_connect": "Prijave v Volkswagen We Connect ni mogoče izvesti. Preverite svoje poverilnice in preverite, ali storitev deluje", - "cannot_update": "Pridobivanje posodobitev iz Volkswagen We Connect ni mogoče", + "cannot_connect": "Prijave v Volkswagen Connect ni mogoče izvesti. Preverite svoje poverilnice in preverite, ali storitev deluje", + "cannot_update": "Pridobivanje posodobitev iz Volkswagen Connect ni mogoče", "unknown": "Prišlo je do neznane napake" }, "progress": { - "task_login": "Prijava v We Connect", + "task_login": "Prijava v Volkswagen Connect", "task_update": "Pridobivanje podatkov o vozilih" } }, "options": { "step": { "user": { - "title": "Možnosti za We Connect", + "title": "Možnosti za Volkswagen Connect", "description": "Nastavite intervale posodobitev", "data": { - "report_request": "Omogoči samodejno zahtevo za poročilo", - "report_scan_interval": "Interval posodobitve zahteve za poročilo (dnevi)", "scan_interval": "Interval posodobitve senzorjev (minute)", "region": "Regija (Država, kjer je bil avto prodan)", "debug": "Omogoči celotne dnevnike odpravljanja težav iz API (zahteva omogočeno beleženje odpravljanja težav v konfiguraciji)", diff --git a/custom_components/volkswagencarnet/util.py b/custom_components/volkswagencarnet/util.py index 9514b732..8ee3d830 100644 --- a/custom_components/volkswagencarnet/util.py +++ b/custom_components/volkswagencarnet/util.py @@ -2,35 +2,40 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry -from homeassistant.helpers.device_registry import DeviceRegistry, DeviceEntry +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry, DeviceRegistry + +# pylint: disable=no-name-in-module,hass-relative-import from volkswagencarnet.vw_vehicle import Vehicle -from .const import CONF_SCANDINAVIAN_MILES, CONF_NO_CONVERSION, DOMAIN +from .const import CONF_NO_CONVERSION, CONF_SCANDINAVIAN_MILES, DOMAIN from .error import ServiceError _LOGGER = logging.getLogger(__name__) def get_convert_conf(entry: ConfigEntry) -> str | None: - """ - Convert old configuration. + """Convert old configuration. Used in migrating config entry to version 2. """ return ( CONF_SCANDINAVIAN_MILES - if entry.options.get(CONF_SCANDINAVIAN_MILES, entry.data.get(CONF_SCANDINAVIAN_MILES, False)) + if entry.options.get( + CONF_SCANDINAVIAN_MILES, entry.data.get(CONF_SCANDINAVIAN_MILES, False) + ) else CONF_NO_CONVERSION ) async def get_coordinator_by_device_id(hass: HomeAssistant, device_id: str): """Get the ConfigEntry.""" - registry: DeviceRegistry = device_registry.async_get(hass) + registry: DeviceRegistry = dr.async_get(hass) dev_entry: DeviceEntry = registry.async_get(device_id) - config_entry = hass.config_entries.async_get_entry(list(dev_entry.config_entries)[0]) + config_entry = hass.config_entries.async_get_entry( + list(dev_entry.config_entries)[0] + ) return await get_coordinator(hass, config_entry) @@ -51,7 +56,7 @@ async def get_coordinator(hass: HomeAssistant, config_entry: ConfigEntry): def get_vehicle(coordinator) -> Vehicle: """Find requested vehicle.""" # find VIN - _LOGGER.debug(f"Found VIN {coordinator.vin}") + _LOGGER.debug("Found VIN %s", coordinator.vin) # parse service call v: Vehicle | None = None @@ -60,13 +65,12 @@ def get_vehicle(coordinator) -> Vehicle: v = vehicle break if v is None: - raise Exception("Vehicle not found") + raise Exception("Vehicle not found") # pylint: disable=broad-exception-raised return v def validate_charge_max_current(charge_max_current: int | str | None) -> int | None: - """ - Validate value against known valid ones and return numeric value. + """Validate value against known valid ones and return numeric value. Maybe there is a way to actually check which values the car supports? """ @@ -77,9 +81,9 @@ def validate_charge_max_current(charge_max_current: int | str | None) -> int | N ): if charge_max_current is None: return None - elif charge_max_current == "max": + if charge_max_current == "max": return 254 - elif charge_max_current == "reduced": + if charge_max_current == "reduced": return 252 return int(charge_max_current) raise ValueError(f"{charge_max_current} looks to be an invalid value") diff --git a/hacs.json b/hacs.json index f336b87c..4c1d248b 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "Volkswagen We Connect", + "name": "Volkswagen Connect", "homeassistant": "2024.2.0", "hide_default_branch": true, "zip_release": true, diff --git a/info.md b/info.md index acdebff2..0b445b4f 100644 --- a/info.md +++ b/info.md @@ -1,7 +1,7 @@ [![buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/robinostlund) -This platform plugin allows you to see some information from volkswagen carnet related to your car that has a valid carnet subscription. +This integration allows you to interact with the Volkswagen Connect service (your car) (EU ONLY). {%- if selected_tag == "master" %} ## This is a development version! diff --git a/manage/update_manifest.py b/manage/update_manifest.py index ef199a4c..bbee6dad 100644 --- a/manage/update_manifest.py +++ b/manage/update_manifest.py @@ -13,7 +13,9 @@ def update_manifest(): version = sys.argv[index + 1] # read manifest - with open(f"{os.getcwd()}/custom_components/volkswagencarnet/manifest.json") as manifestfile: + with open( + f"{os.getcwd()}/custom_components/volkswagencarnet/manifest.json" + ) as manifestfile: manifest = json.load(manifestfile) # set version in manifest @@ -27,7 +29,9 @@ def update_manifest(): manifest["requirements"] = requirements # save manifest - with open(f"{os.getcwd()}/custom_components/volkswagencarnet/manifest.json", "w") as manifestfile: + with open( + f"{os.getcwd()}/custom_components/volkswagencarnet/manifest.json", "w" + ) as manifestfile: manifestfile.write(json.dumps(manifest, indent=4, sort_keys=False)) # print output diff --git a/requirements.txt b/requirements.txt index d606eb88..a0214756 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ pytz -volkswagencarnet==4.4.74 +volkswagencarnet==5.0.0 diff --git a/tests/config_flow_test.py b/tests/config_flow_test.py index 0abc014e..2caacf49 100644 --- a/tests/config_flow_test.py +++ b/tests/config_flow_test.py @@ -64,7 +64,9 @@ async def test_flow_user_init_auth_fails(m_connection, hass: HomeAssistant): user_input=DUMMY_CONFIG, ) - flow: VolkswagenCarnetConfigFlow = hass.config_entries.flow._progress[result["flow_id"]] + flow: VolkswagenCarnetConfigFlow = hass.config_entries.flow._progress[ + result["flow_id"] + ] with patch.object(flow._connection, "doLogin") as aaaaa: aaaaa.side_effect = Exception diff --git a/tests/timer_services_test.py b/tests/timer_services_test.py index 7fc620e8..fe68654a 100644 --- a/tests/timer_services_test.py +++ b/tests/timer_services_test.py @@ -51,7 +51,9 @@ async def test_call_service(conn: MagicMock, hass: HomeAssistant): } ) - c: VolkswagenCoordinator = VolkswagenCoordinator(hass=hass, entry=e, update_interval=timedelta(minutes=10)) + c: VolkswagenCoordinator = VolkswagenCoordinator( + hass=hass, entry=e, update_interval=timedelta(minutes=10) + ) c.connection.vehicles = [MagicMock(Vehicle)] c.connection.vehicles[0].vin = "XYZ" @@ -74,9 +76,9 @@ async def test_call_service(conn: MagicMock, hass: HomeAssistant): target_temp = 24.5 data = {"device_id": e.entry_id, "target_temperature": target_temp} - with patch("custom_components.volkswagencarnet.services.get_coordinator_by_device_id") as m, patch.object( - c.connection, "getTimers" - ) as get_timers: + with patch( + "custom_components.volkswagencarnet.services.get_coordinator_by_device_id" + ) as m, patch.object(c.connection, "getTimers") as get_timers: m.return_value = c timer_profiles = [ TimerProfile( @@ -104,7 +106,9 @@ async def test_call_service(conn: MagicMock, hass: HomeAssistant): departureTimeOfDay="07:33", ) ] - basic_settings = BasicSettings(timestamp="2022-02-22T20:22:00Z", targetTemperature=2965, chargeMinLimit=20) + basic_settings = BasicSettings( + timestamp="2022-02-22T20:22:00Z", targetTemperature=2965, chargeMinLimit=20 + ) tpl = TimerProfileList(timer_profiles) tp = TimersAndProfiles( timerProfileList=tpl, @@ -125,7 +129,9 @@ async def test_call_service(conn: MagicMock, hass: HomeAssistant): ) c.connection.vehicles[0].set_climatisation_temp.assert_called_once() - used_args = c.connection.vehicles[0].set_climatisation_temp.call_args_list[0].args[0] + used_args = ( + c.connection.vehicles[0].set_climatisation_temp.call_args_list[0].args[0] + ) # check that the correct VW temperature unit was set assert 2975 == used_args diff --git a/tests/util_test.py b/tests/util_test.py index ab7da837..2c1872ff 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -42,7 +42,9 @@ async def test_get_coordinator(hass: HomeAssistant): } # We want to skip the actual setup flow here - with patch.object(homeassistant.config_entries.ConfigEntries, "async_setup") as flow, patch.object( + with patch.object( + homeassistant.config_entries.ConfigEntries, "async_setup" + ) as flow, patch.object( homeassistant.config_entries.ConfigEntries, "_async_schedule_save" ): f: Future = Future() @@ -56,7 +58,9 @@ async def test_get_coordinator(hass: HomeAssistant): identifiers: set[tuple[str, str]] = {tuple(["volkswagencarnet", dev_id])} # type: ignore - dev_entry = registry.async_get_or_create(config_entry_id=config_entry.entry_id, identifiers=identifiers) + dev_entry = registry.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers=identifiers + ) res = await util.get_coordinator(hass=hass, config_entry=config_entry) assert m_coord == res