diff --git a/.strict-typing b/.strict-typing index e0ab8b83ac7..8f7ca3311c2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -93,6 +93,7 @@ homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* homeassistant.components.nfandroidtv.* +homeassistant.components.nissan_leaf.* homeassistant.components.no_ip.* homeassistant.components.notify.* homeassistant.components.notion.* diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 46e1e9bb7a6..7e8dd5c2efb 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,15 +1,22 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta from http import HTTPStatus import logging import sys +from typing import Any, cast -from pycarwings2 import CarwingsError, Session +from pycarwings2 import CarwingsError, Leaf, Session +from pycarwings2.responses import ( + CarwingsLatestBatteryStatusResponse, + CarwingsLatestClimateControlStatusResponse, +) import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( @@ -18,6 +25,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -94,10 +102,10 @@ UPDATE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) START_CHARGE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) -def setup(hass, config): +def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Nissan Leaf integration.""" - async def async_handle_update(service): + async def async_handle_update(service: ServiceCall) -> None: """Handle service to update leaf data from Nissan servers.""" # It would be better if this was changed to use nickname, or # an entity name rather than a vin. @@ -109,7 +117,7 @@ def setup(hass, config): else: _LOGGER.debug("Vin %s not recognised for update", vin) - async def async_handle_start_charge(service): + async def async_handle_start_charge(service: ServiceCall) -> None: """Handle service to start charging.""" # It would be better if this was changed to use nickname, or # an entity name rather than a vin. @@ -134,14 +142,13 @@ def setup(hass, config): else: _LOGGER.debug("Vin %s not recognised for update", vin) - def setup_leaf(car_config): + def setup_leaf(car_config: dict[str, Any]) -> None: """Set up a car.""" _LOGGER.debug("Logging into You+Nissan") - username = car_config[CONF_USERNAME] - password = car_config[CONF_PASSWORD] - region = car_config[CONF_REGION] - leaf = None + username: str = car_config[CONF_USERNAME] + password: str = car_config[CONF_PASSWORD] + region: str = car_config[CONF_REGION] try: # This might need to be made async (somehow) causes @@ -153,13 +160,13 @@ def setup(hass, config): "Unable to fetch car details..." " do you actually have a Leaf connected to your account?" ) - return False + return except CarwingsError: _LOGGER.error( "An unknown error occurred while connecting to Nissan: %s", sys.exc_info()[0], ) - return False + return _LOGGER.warning( "WARNING: This may poll your Leaf too often, and drain the 12V" @@ -195,10 +202,15 @@ def setup(hass, config): return True -def _extract_start_date(battery_info): +def _extract_start_date( + battery_info: CarwingsLatestBatteryStatusResponse, +) -> datetime | None: """Extract the server date from the battery response.""" try: - return battery_info.answer["BatteryStatusRecords"]["OperationDateAndTime"] + return cast( + datetime, + battery_info.answer["BatteryStatusRecords"]["OperationDateAndTime"], + ) except KeyError: return None @@ -206,28 +218,30 @@ def _extract_start_date(battery_info): class LeafDataStore: """Nissan Leaf Data Store.""" - def __init__(self, hass, leaf, car_config): + def __init__( + self, hass: HomeAssistant, leaf: Leaf, car_config: dict[str, Any] + ) -> None: """Initialise the data store.""" self.hass = hass self.leaf = leaf self.car_config = car_config self.force_miles = car_config[CONF_FORCE_MILES] - self.data = {} + self.data: dict[str, Any] = {} self.data[DATA_CLIMATE] = None self.data[DATA_BATTERY] = None self.data[DATA_CHARGING] = None self.data[DATA_RANGE_AC] = None self.data[DATA_RANGE_AC_OFF] = None self.data[DATA_PLUGGED_IN] = None - self.next_update = None - self.last_check = None - self.request_in_progress = False + self.next_update: datetime | None = None + self.last_check: datetime | None = None + self.request_in_progress: bool = False # Timestamp of last successful response from battery or climate. - self.last_battery_response = None - self.last_climate_response = None - self._remove_listener = None + self.last_battery_response: datetime | None = None + self.last_climate_response: datetime | None = None + self._remove_listener: CALLBACK_TYPE | None = None - async def async_update_data(self, now): + async def async_update_data(self, now: datetime) -> None: """Update data from nissan leaf.""" # Prevent against a previously scheduled update and an ad-hoc update # started from an update from both being triggered. @@ -241,11 +255,13 @@ class LeafDataStore: await self.async_refresh_data(now) self.next_update = self.get_next_interval() _LOGGER.debug("Next update=%s", self.next_update) - self._remove_listener = async_track_point_in_utc_time( - self.hass, self.async_update_data, self.next_update - ) - def get_next_interval(self): + if self.next_update is not None: + self._remove_listener = async_track_point_in_utc_time( + self.hass, self.async_update_data, self.next_update + ) + + def get_next_interval(self) -> datetime: """Calculate when the next update should occur.""" base_interval = self.car_config[CONF_INTERVAL] climate_interval = self.car_config[CONF_CLIMATE_INTERVAL] @@ -278,7 +294,7 @@ class LeafDataStore: return utcnow() + interval - async def async_refresh_data(self, now): + async def async_refresh_data(self, now: datetime) -> None: """Refresh the leaf data and update the datastore.""" if self.request_in_progress: _LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname) @@ -333,12 +349,14 @@ class LeafDataStore: self.request_in_progress = False async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) - async def async_get_battery(self): + async def async_get_battery( + self, + ) -> CarwingsLatestBatteryStatusResponse: """Request battery update from Nissan servers.""" try: # Request battery update from the car _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) - start_date = None + start_date: datetime | None = None try: start_server_info = await self.hass.async_add_executor_job( self.leaf.get_latest_battery_status @@ -409,7 +427,9 @@ class LeafDataStore: _LOGGER.error("An error occurred parsing response from server") return None - async def async_get_climate(self): + async def async_get_climate( + self, + ) -> CarwingsLatestClimateControlStatusResponse: """Request climate data from Nissan servers.""" try: return await self.hass.async_add_executor_job( @@ -421,7 +441,7 @@ class LeafDataStore: ) return None - async def async_set_climate(self, toggle): + async def async_set_climate(self, toggle: bool) -> bool: """Set climate control mode via Nissan servers.""" climate_result = None if toggle: @@ -454,7 +474,7 @@ class LeafDataStore: if climate_result is not None: _LOGGER.debug("Climate result: %s", climate_result.__dict__) async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) - return climate_result.is_hvac_running == toggle + return bool(climate_result.is_hvac_running) == toggle _LOGGER.debug("Climate result not returned by Nissan servers") return False @@ -463,11 +483,11 @@ class LeafDataStore: class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" - def __init__(self, car): + def __init__(self, car: Leaf) -> None: """Store LeafDataStore upon init.""" self.car = car - def log_registration(self): + def log_registration(self) -> None: """Log registration.""" _LOGGER.debug( "Registered %s integration for VIN %s", @@ -476,7 +496,7 @@ class LeafEntity(Entity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return default attributes for Nissan leaf entities.""" return { "next_update": self.car.next_update, @@ -486,7 +506,7 @@ class LeafEntity(Entity): "vin": self.car.leaf.vin, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.log_registration() self.async_on_remove( @@ -496,6 +516,6 @@ class LeafEntity(Entity): ) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Update the state.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 2fd692f5518..a46b418e917 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -41,19 +41,19 @@ class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.PLUG @property - def name(self): + def name(self) -> str: """Sensor name.""" return f"{self.car.leaf.nickname} Plug Status" @property - def available(self): + def available(self) -> bool: """Sensor availability.""" return self.car.data[DATA_PLUGGED_IN] is not None @property - def is_on(self): + def is_on(self) -> bool: """Return true if plugged in.""" - return self.car.data[DATA_PLUGGED_IN] + return bool(self.car.data[DATA_PLUGGED_IN]) class LeafChargingSensor(LeafEntity, BinarySensorEntity): @@ -62,16 +62,16 @@ class LeafChargingSensor(LeafEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING @property - def name(self): + def name(self) -> str: """Sensor name.""" return f"{self.car.leaf.nickname} Charging Status" @property - def available(self): + def available(self) -> bool: """Sensor availability.""" return self.car.data[DATA_CHARGING] is not None @property - def is_on(self): + def is_on(self) -> bool: """Return true if charging.""" - return self.car.data[DATA_CHARGING] + return bool(self.car.data[DATA_CHARGING]) diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 416684fad5e..3d1ad088fa0 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -3,6 +3,9 @@ from __future__ import annotations import logging +from pycarwings2.pycarwings2 import Leaf +from voluptuous.validators import Number + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant @@ -50,29 +53,29 @@ class LeafBatterySensor(LeafEntity, SensorEntity): """Nissan Leaf Battery Sensor.""" @property - def name(self): + def name(self) -> str: """Sensor Name.""" return f"{self.car.leaf.nickname} Charge" @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return SensorDeviceClass.BATTERY @property - def native_value(self): + def native_value(self) -> Number | None: """Battery state percentage.""" if self.car.data[DATA_BATTERY] is None: return None return round(self.car.data[DATA_BATTERY]) @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Battery state measured in percentage.""" return PERCENTAGE @property - def icon(self): + def icon(self) -> str: """Battery state icon handling.""" chargestate = self.car.data[DATA_CHARGING] return icon_for_battery_level(battery_level=self.state, charging=chargestate) @@ -81,19 +84,19 @@ class LeafBatterySensor(LeafEntity, SensorEntity): class LeafRangeSensor(LeafEntity, SensorEntity): """Nissan Leaf Range Sensor.""" - def __init__(self, car, ac_on): + def __init__(self, car: Leaf, ac_on: bool) -> None: """Set up range sensor. Store if AC on.""" self._ac_on = ac_on super().__init__(car) @property - def name(self): + def name(self) -> str: """Update sensor name depending on AC.""" if self._ac_on is True: return f"{self.car.leaf.nickname} Range (AC)" return f"{self.car.leaf.nickname} Range" - def log_registration(self): + def log_registration(self) -> None: """Log registration.""" _LOGGER.debug( "Registered LeafRangeSensor integration with Home Assistant for VIN %s", @@ -101,7 +104,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): ) @property - def native_value(self): + def native_value(self) -> float | None: """Battery range in miles or kms.""" if self._ac_on: ret = self.car.data[DATA_RANGE_AC] @@ -117,13 +120,13 @@ class LeafRangeSensor(LeafEntity, SensorEntity): return round(ret) @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Battery range unit.""" if not self.car.hass.config.units.is_metric or self.car.force_miles: return LENGTH_MILES return LENGTH_KILOMETERS @property - def icon(self): + def icon(self) -> str: """Nice icon for range.""" return ICON_RANGE diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index ede0ae459c4..de4ee2dc043 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import ToggleEntity @@ -35,11 +36,11 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): """Nissan Leaf Climate Control switch.""" @property - def name(self): + def name(self) -> str: """Switch name.""" return f"{self.car.leaf.nickname} Climate Control" - def log_registration(self): + def log_registration(self) -> None: """Log registration.""" _LOGGER.debug( "Registered LeafClimateSwitch integration with Home Assistant for VIN %s", @@ -47,23 +48,23 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return climate control attributes.""" attrs = super().extra_state_attributes attrs["updated_on"] = self.car.last_climate_response return attrs @property - def is_on(self): + def is_on(self) -> bool: """Return true if climate control is on.""" - return self.car.data[DATA_CLIMATE] + return bool(self.car.data[DATA_CLIMATE]) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on climate control.""" if await self.car.async_set_climate(True): self.car.data[DATA_CLIMATE] = True - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off climate control.""" if await self.car.async_set_climate(False): self.car.data[DATA_CLIMATE] = False diff --git a/mypy.ini b/mypy.ini index ab9f02990b1..db85c57ae96 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1034,6 +1034,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nissan_leaf.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.no_ip.*] check_untyped_defs = true disallow_incomplete_defs = true