Add type hints to nissan_leaf integration (#62967)

This commit is contained in:
Phil Cole 2021-12-29 11:23:54 +00:00 committed by GitHub
parent 2df0adfbc7
commit 54d1e20948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 64 deletions

View File

@ -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.*

View File

@ -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)

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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