mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add type hints to nissan_leaf integration (#62967)
This commit is contained in:
parent
2df0adfbc7
commit
54d1e20948
@ -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.*
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
11
mypy.ini
11
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user