1 """Support for the Nissan Leaf Carwings/Nissan Connect API."""
3 from __future__
import annotations
6 from datetime
import datetime, timedelta
7 from http
import HTTPStatus
10 from typing
import Any, cast
12 from pycarwings2
import CarwingsError, Leaf, Session
13 from pycarwings2.responses
import (
14 CarwingsLatestBatteryStatusResponse,
15 CarwingsLatestClimateControlStatusResponse,
17 import voluptuous
as vol
29 CONF_CHARGING_INTERVAL,
30 CONF_CLIMATE_INTERVAL,
41 DEFAULT_CHARGING_INTERVAL,
42 DEFAULT_CLIMATE_INTERVAL,
46 MAX_RESPONSE_ATTEMPTS,
54 _LOGGER = logging.getLogger(__name__)
56 CONFIG_SCHEMA = vol.Schema(
63 vol.Required(CONF_USERNAME): cv.string,
64 vol.Required(CONF_PASSWORD): cv.string,
65 vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS),
66 vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): (
67 vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))
70 CONF_CHARGING_INTERVAL, default=DEFAULT_CHARGING_INTERVAL
72 vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))
75 CONF_CLIMATE_INTERVAL, default=DEFAULT_CLIMATE_INTERVAL
77 vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))
79 vol.Optional(CONF_FORCE_MILES, default=
False): cv.boolean,
85 extra=vol.ALLOW_EXTRA,
88 PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
91 SERVICE_UPDATE_LEAF =
"update"
92 SERVICE_START_CHARGE_LEAF =
"start_charge"
95 UPDATE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string})
96 START_CHARGE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string})
99 def setup(hass: HomeAssistant, config: ConfigType) -> bool:
100 """Set up the Nissan Leaf integration."""
102 async
def async_handle_update(service: ServiceCall) ->
None:
103 """Handle service to update leaf data from Nissan servers."""
104 vin = service.data[ATTR_VIN]
106 if vin
in hass.data[DATA_LEAF]:
107 data_store = hass.data[DATA_LEAF][vin]
108 await data_store.async_update_data(
utcnow())
110 _LOGGER.debug(
"Vin %s not recognised for update", vin)
112 async
def async_handle_start_charge(service: ServiceCall) ->
None:
113 """Handle service to start charging."""
115 "The 'nissan_leaf.start_charge' service is deprecated and has been"
116 "replaced by a dedicated button entity: Please use that to start"
119 vin = service.data[ATTR_VIN]
121 if vin
in hass.data[DATA_LEAF]:
122 data_store = hass.data[DATA_LEAF][vin]
128 result = await hass.async_add_executor_job(data_store.leaf.start_charging)
130 _LOGGER.debug(
"Start charging sent, request updated data in 1 minute")
132 data_store.next_update = check_charge_at
134 hass, data_store.async_update_data, check_charge_at
138 _LOGGER.debug(
"Vin %s not recognised for update", vin)
140 def setup_leaf(car_config: dict[str, Any]) ->
None:
142 _LOGGER.debug(
"Logging into You+Nissan")
144 username: str = car_config[CONF_USERNAME]
145 password: str = car_config[CONF_PASSWORD]
146 region: str = car_config[CONF_REGION]
151 sess = Session(username, password, region)
152 leaf = sess.get_leaf()
155 "Unable to fetch car details..."
156 " do you actually have a Leaf connected to your account?"
159 except CarwingsError:
161 "An unknown error occurred while connecting to Nissan: %s",
167 "WARNING: This may poll your Leaf too often, and drain the 12V"
168 " battery. If you drain your cars 12V battery it WILL NOT START"
169 " as the drive train battery won't connect."
170 " Don't set the intervals too low"
174 hass.data[DATA_LEAF][leaf.vin] = data_store
176 for platform
in PLATFORMS:
180 hass, data_store.async_update_data,
utcnow() + INITIAL_UPDATE
183 hass.data[DATA_LEAF] = {}
184 for car
in config[DOMAIN]:
187 hass.services.register(
188 DOMAIN, SERVICE_UPDATE_LEAF, async_handle_update, schema=UPDATE_LEAF_SCHEMA
190 hass.services.register(
192 SERVICE_START_CHARGE_LEAF,
193 async_handle_start_charge,
194 schema=START_CHARGE_LEAF_SCHEMA,
201 battery_info: CarwingsLatestBatteryStatusResponse,
202 ) -> datetime |
None:
203 """Extract the server date from the battery response."""
207 battery_info.answer[
"BatteryStatusRecords"][
"OperationDateAndTime"],
214 """Nissan Leaf Data Store."""
217 self, hass: HomeAssistant, leaf: Leaf, car_config: dict[str, Any]
219 """Initialise the data store."""
224 self.data: dict[str, Any] = {}
225 self.data[DATA_CLIMATE] =
None
226 self.data[DATA_BATTERY] =
None
227 self.data[DATA_CHARGING] =
None
228 self.data[DATA_RANGE_AC] =
None
229 self.data[DATA_RANGE_AC_OFF] =
None
230 self.data[DATA_PLUGGED_IN] =
None
231 self.
next_updatenext_update: datetime |
None =
None
232 self.
last_checklast_check: datetime |
None =
None
240 """Update data from nissan leaf."""
252 _LOGGER.debug(
"Next update=%s", self.
next_updatenext_update)
260 """Calculate when the next update should occur."""
261 base_interval = self.
car_configcar_config[CONF_INTERVAL]
262 climate_interval = self.
car_configcar_config[CONF_CLIMATE_INTERVAL]
263 charging_interval = self.
car_configcar_config[CONF_CHARGING_INTERVAL]
272 and self.data[DATA_CHARGING]
is False
273 and self.data[DATA_BATTERY] <= RESTRICTED_BATTERY
276 "Low battery so restricting refresh frequency (%s)", self.
leafleaf.nickname
278 interval = RESTRICTED_INTERVAL
280 intervals = [base_interval]
282 if self.data[DATA_CHARGING]:
283 intervals.append(charging_interval)
285 if self.data[DATA_CLIMATE]:
286 intervals.append(climate_interval)
288 interval =
min(intervals)
290 return utcnow() + interval
293 """Refresh the leaf data and update the datastore."""
295 _LOGGER.debug(
"Refresh currently in progress for %s", self.
leafleaf.nickname)
298 _LOGGER.debug(
"Updating Nissan Leaf Data")
305 if server_response
is not None:
306 _LOGGER.debug(
"Server Response: %s", server_response.__dict__)
308 if server_response.answer[
"status"] == HTTPStatus.OK:
309 self.data[DATA_BATTERY] = server_response.battery_percent
314 if hasattr(server_response,
"cruising_range_ac_on_km"):
315 self.data[DATA_RANGE_AC] = server_response.cruising_range_ac_on_km
317 self.data[DATA_RANGE_AC] =
None
319 if hasattr(server_response,
"cruising_range_ac_off_km"):
320 self.data[DATA_RANGE_AC_OFF] = (
321 server_response.cruising_range_ac_off_km
324 self.data[DATA_RANGE_AC_OFF] =
None
326 self.data[DATA_PLUGGED_IN] = server_response.is_connected
327 self.data[DATA_CHARGING] = server_response.is_charging
332 if server_response
is not None:
335 if climate_response
is not None:
337 "Got climate data for Leaf: %s", climate_response.__dict__
339 self.data[DATA_CLIMATE] = climate_response.is_hvac_running
341 except CarwingsError:
342 _LOGGER.error(
"Error fetching climate info")
349 ) -> CarwingsLatestBatteryStatusResponse:
350 """Request battery update from Nissan servers."""
353 _LOGGER.debug(
"Requesting battery update, %s", self.
leafleaf.vin)
354 start_date: datetime |
None =
None
356 start_server_info = await self.
hasshass.async_add_executor_job(
357 self.
leafleaf.get_latest_battery_status
360 _LOGGER.debug(
"Battery status check returned nothing")
362 if not start_server_info:
363 _LOGGER.debug(
"Battery status check failed")
366 await asyncio.sleep(1)
367 request = await self.
hasshass.async_add_executor_job(self.
leafleaf.request_update)
369 _LOGGER.error(
"Battery update request failed")
372 for attempt
in range(MAX_RESPONSE_ATTEMPTS):
374 "Waiting %s seconds for battery update (%s) (%s)",
379 await asyncio.sleep(PYCARWINGS2_SLEEP)
384 check_result_info = await self.
hasshass.async_add_executor_job(
385 self.
leafleaf.get_status_from_update, request
388 if check_result_info
is not None:
391 server_info = await self.
hasshass.async_add_executor_job(
392 self.
leafleaf.get_latest_battery_status
394 if not start_date
or (
400 await asyncio.sleep(1)
401 request = await self.
hasshass.async_add_executor_job(
402 self.
leafleaf.request_update
405 _LOGGER.error(
"Battery update request failed")
409 "%s attempts exceeded return latest data from server",
410 MAX_RESPONSE_ATTEMPTS,
414 server_info = await self.
hasshass.async_add_executor_job(
415 self.
leafleaf.get_latest_battery_status
417 except CarwingsError:
418 _LOGGER.error(
"An error occurred getting battery status")
420 except (KeyError, TypeError):
421 _LOGGER.error(
"An error occurred parsing response from server")
427 ) -> CarwingsLatestClimateControlStatusResponse:
428 """Request climate data from Nissan servers."""
430 return await self.
hasshass.async_add_executor_job(
431 self.
leafleaf.get_latest_hvac_status
433 except CarwingsError:
435 "An error occurred communicating with the car %s", self.
leafleaf.vin
440 """Set climate control mode via Nissan servers."""
441 climate_result =
None
443 _LOGGER.debug(
"Requesting climate turn on for %s", self.
leafleaf.vin)
444 set_function = self.
leafleaf.start_climate_control
445 result_function = self.
leafleaf.get_start_climate_control_result
447 _LOGGER.debug(
"Requesting climate turn off for %s", self.
leafleaf.vin)
448 set_function = self.
leafleaf.stop_climate_control
449 result_function = self.
leafleaf.get_stop_climate_control_result
451 request = await self.
hasshass.async_add_executor_job(set_function)
452 for attempt
in range(MAX_RESPONSE_ATTEMPTS):
455 "Climate data not in yet (%s) (%s). Waiting (%s) seconds",
460 await asyncio.sleep(PYCARWINGS2_SLEEP)
462 climate_result = await self.
hasshass.async_add_executor_job(
463 result_function, request
466 if climate_result
is not None:
469 if climate_result
is not None:
470 _LOGGER.debug(
"Climate result: %s", climate_result.__dict__)
472 return bool(climate_result.is_hvac_running) == toggle
474 _LOGGER.debug(
"Climate result not returned by Nissan servers")
478 """Request to start charging the car. Used by the button platform."""
479 await self.
hasshass.async_add_executor_job(self.
leafleaf.start_charging)
483 """Set the next update to be triggered in a minute."""
CarwingsLatestBatteryStatusResponse async_get_battery(self)
None schedule_update(self)
None async_start_charging(self)
bool async_set_climate(self, bool toggle)
datetime get_next_interval(self)
CarwingsLatestClimateControlStatusResponse async_get_climate(self)
None async_update_data(self, datetime now)
None __init__(self, HomeAssistant hass, Leaf leaf, dict[str, Any] car_config)
None async_refresh_data(self, datetime now)
datetime|None _extract_start_date(CarwingsLatestBatteryStatusResponse battery_info)
bool setup(HomeAssistant hass, ConfigType config)
None load_platform(core.HomeAssistant hass, Platform|str component, str platform, DiscoveryInfoType|None discovered, ConfigType hass_config)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)