diff --git a/custom_components/volkswagencarnet/__init__.py b/custom_components/volkswagencarnet/__init__.py index 10e462b..4a479ba 100755 --- a/custom_components/volkswagencarnet/__init__.py +++ b/custom_components/volkswagencarnet/__init__.py @@ -31,6 +31,7 @@ Switch, DoorLock, Position, + Number, TrunkLock, ) from volkswagencarnet.vw_vehicle import Vehicle @@ -55,20 +56,13 @@ DEFAULT_DEBUG, CONF_CONVERT, CONF_IMPERIAL_UNITS, - SERVICE_SET_TIMER_BASIC_SETTINGS, - SERVICE_UPDATE_SCHEDULE, - SERVICE_UPDATE_PROFILE, SERVICE_SET_CHARGER_MAX_CURRENT, CONF_AVAILABLE_RESOURCES, CONF_NO_CONVERSION, ) from .services import ( - SchedulerService, ChargerService, - SERVICE_SET_TIMER_BASIC_SETTINGS_SCHEMA, SERVICE_SET_CHARGER_MAX_CURRENT_SCHEMA, - SERVICE_UPDATE_SCHEDULE_SCHEMA, - SERVICE_UPDATE_PROFILE_SCHEMA, ) from .util import get_convert_conf @@ -77,9 +71,6 @@ def unload_services(hass: HomeAssistant): """Unload the services from HA.""" - hass.services.async_remove(DOMAIN, SERVICE_SET_TIMER_BASIC_SETTINGS) - hass.services.async_remove(DOMAIN, SERVICE_UPDATE_SCHEDULE) - hass.services.async_remove(DOMAIN, SERVICE_UPDATE_PROFILE) hass.services.async_remove(DOMAIN, SERVICE_SET_CHARGER_MAX_CURRENT) @@ -88,25 +79,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def register_services(): cs = ChargerService(hass) - ss = SchedulerService(hass) - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_SET_TIMER_BASIC_SETTINGS, - service_func=ss.set_timer_basic_settings, - schema=SERVICE_SET_TIMER_BASIC_SETTINGS_SCHEMA, - ) - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_UPDATE_SCHEDULE, - service_func=ss.update_schedule, - schema=SERVICE_UPDATE_SCHEDULE_SCHEMA, - ) - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_UPDATE_PROFILE, - service_func=ss.update_profile, - schema=SERVICE_UPDATE_PROFILE_SCHEMA, - ) hass.services.async_register( domain=DOMAIN, service=SERVICE_SET_CHARGER_MAX_CURRENT, @@ -345,6 +317,8 @@ def async_write_ha_state(self) -> None: or str(self.state or STATE_UNKNOWN) != str(prev.state) or self.component == "climate" ): + if self.component == "climate": + self._update_state() super().async_write_ha_state() else: _LOGGER.debug(f"{self.name}: state not changed ('{prev.state}' == '{self.state}'), skipping update.") @@ -378,7 +352,7 @@ async def async_added_to_hass(self) -> None: @property def instrument( self, - ) -> Union[BinarySensor, Climate, DoorLock, Position, Sensor, Switch, TrunkLock, Instrument]: + ) -> Union[BinarySensor, Climate, DoorLock, Position, Sensor, Switch, TrunkLock, Number, Instrument]: """Return corresponding instrument.""" return self.data.instrument(self.vin, self.component, self.attribute) diff --git a/custom_components/volkswagencarnet/const.py b/custom_components/volkswagencarnet/const.py index 9ab4195..c37c7b2 100644 --- a/custom_components/volkswagencarnet/const.py +++ b/custom_components/volkswagencarnet/const.py @@ -44,6 +44,7 @@ "device_tracker": "device_tracker", "switch": "switch", "climate": "climate", + "number": "number", } SERVICE_SET_TIMER_BASIC_SETTINGS = "set_timer_basic_settings" diff --git a/custom_components/volkswagencarnet/number.py b/custom_components/volkswagencarnet/number.py new file mode 100644 index 0000000..89541aa --- /dev/null +++ b/custom_components/volkswagencarnet/number.py @@ -0,0 +1,93 @@ +"""Number support for Volkswagen We Connect integration.""" + +import logging +from typing import Union + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory + +from . import VolkswagenEntity, VolkswagenData +from .const import DATA_KEY, DATA, DOMAIN, UPDATE_CALLBACK + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass: HomeAssistant, config: ConfigEntry, 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): + """Set up the Volkswagen number.""" + data = hass.data[DOMAIN][entry.entry_id][DATA] + coordinator = data.coordinator + if coordinator.data is not None: + async_add_devices( + VolkswagenNumber( + 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 == "number") + ) + + return True + + +class VolkswagenNumber(VolkswagenEntity, NumberEntity): + """Representation of a Volkswagen number.""" + + def __init__(self, data: VolkswagenData, vin: str, component: str, attribute: str, callback=None): + """Initialize switch.""" + super().__init__(data, vin, component, attribute, callback) + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + if self.instrument.min_value: + return self.instrument.min_value + return None + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + if self.instrument.max_value: + return self.instrument.max_value + return None + + @property + def native_step(self) -> float: + if self.instrument.native_step: + return self.instrument.native_step + return None + + @property + def native_unit_of_measurement(self) -> str: + if self.instrument.unit: + return self.instrument.unit + return "" + + @property + def entity_category(self) -> Union[EntityCategory, str, None]: + """Return entity category.""" + if self.instrument.entity_type == "diag": + return EntityCategory.DIAGNOSTIC + if self.instrument.entity_type == "config": + return EntityCategory.CONFIG + + @property + def native_value(self) -> float: + """Return the entity value to represent the entity state.""" + return self.instrument.state + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + _LOGGER.debug("Update current value to %s." % value) + await self.instrument.set_value(value) + # self.notify_updated() diff --git a/custom_components/volkswagencarnet/services.py b/custom_components/volkswagencarnet/services.py index 4b19ca3..04e5dfd 100644 --- a/custom_components/volkswagencarnet/services.py +++ b/custom_components/volkswagencarnet/services.py @@ -1,13 +1,9 @@ """Services exposed to Home Assistant.""" import logging -from datetime import datetime, timezone - -import pytz import voluptuous as vol from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from volkswagencarnet.vw_timer import Timer, TimerData from .util import get_coordinator_by_device_id, get_vehicle, validate_charge_max_current @@ -21,225 +17,6 @@ extra=vol.ALLOW_EXTRA, # FIXME, should not be needed ) -SERVICE_UPDATE_SCHEDULE_SCHEMA = vol.Schema( - { - vol.Required("device_id"): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Required("timer_id"): vol.In([1, 2, 3]), - vol.Optional("charging_profile"): vol.All(cv.positive_int, vol.Range(min_included=1, max_included=10)), - vol.Optional("enabled"): vol.All(cv.boolean), - vol.Optional("frequency"): vol.In(["cyclic", "single"]), - vol.Optional("departure_time"): vol.All(cv.string), - vol.Optional("departure_datetime"): vol.All(cv.string), - vol.Optional("weekday_mask"): vol.All(cv.string, vol.Length(min=7, max=7)), - }, - extra=vol.ALLOW_EXTRA, # FIXME, should not be needed -) - -SERVICE_SET_TIMER_BASIC_SETTINGS_SCHEMA = vol.Schema( - { - vol.Optional("device_id"): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Optional("min_level"): vol.In([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]), - vol.Optional("target_temperature_celsius"): vol.Any(cv.string, cv.positive_int), - vol.Optional("target_temperature_fahrenheit"): vol.Any(cv.string, cv.positive_int), - }, - extra=vol.ALLOW_EXTRA, # FIXME, should not be needed -) - -SERVICE_UPDATE_PROFILE_SCHEMA = vol.Schema( - { - vol.Required("device_id"): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Required("profile_id"): vol.All(cv.positive_int, vol.Range(min_included=1, max_included=10)), - vol.Optional("profile_name"): vol.All(cv.string), - vol.Optional("charging"): vol.All(cv.boolean), - vol.Optional("climatisation"): vol.All(cv.boolean), - vol.Optional("target_level"): vol.In( - [ - 0, - 10, - 20, - 30, - 40, - 50, - 60, - 70, - 80, - 90, - 100, - "0", - "10", - "20", - "30", - "40", - "50", - "60", - "70", - "80", - "90", - "100", - ] - ), - vol.Optional("charge_max_current"): vol.In([5, 10, 13, 16, 32, "5", "10", "13", "16", "32", "reduced", "max"]), - vol.Optional("night_rate"): vol.All(cv.boolean), - vol.Optional("night_rate_start"): vol.All(cv.string), - vol.Optional("night_rate_end"): vol.All(cv.string), - }, - extra=vol.ALLOW_EXTRA, # FIXME, should not be needed -) - - -class SchedulerService: - """Schedule services class.""" - - def __init__(self, hass: HomeAssistant): - """Init.""" - self.hass: HomeAssistant = hass - - async def set_timer_basic_settings(self, service_call: ServiceCall) -> bool: - """Service for configuring basic settings.""" - c = await get_coordinator_by_device_id(self.hass, service_call.data.get("device_id")) - v = get_vehicle(c) - - # parse service call - tt = service_call.data.get("target_temperature", None) - ml = service_call.data.get("min_level", None) - # hs = service_call.data.get("heater_source", None) - res = True - - # update timers accordingly (not working) - # if hs is not None: - # _LOGGER.debug(f"Setting heater source to {hs}") - # # get timers - # t = await c.connection.getTimers(c.vin) - # t.timersAndProfiles.timerBasicSetting.set_heater_source(hs) - # res = await c.connection._setDepartureTimer(c.vin, t.timersAndProfiles, "setHeaterSource") - # # res = await v.set_departure_timer_heater_source(hs) - # _LOGGER.debug(f"set heater source returned {res}") - if tt is not None: - _LOGGER.debug(f"Setting target temperature to {tt} {self.hass.config.units.temperature_unit}") - # get timers - t = await c.connection.getTimers(c.vin) - if self.hass.config.units.is_metric: - t.timersAndProfiles.timerBasicSetting.set_target_temperature_celsius(float(tt)) - else: - t.timersAndProfiles.timerBasicSetting.set_target_temperature_fahrenheit(int(tt)) - # send command to volkswagencarnet - res = res and await v.set_climatisation_temp(t.timersAndProfiles.timerBasicSetting.targetTemperature) - if ml is not None: - _LOGGER.debug(f"Setting minimum charge level to {ml}%") - # send charge limit command to volkswagencarnet - res = res and await v.set_charge_min_level(int(ml)) - - self.hass.add_job(c.async_request_refresh) - - return res - - async def update_schedule(self, service_call: ServiceCall) -> bool: - """Service for updating departure schedules.""" - c = await get_coordinator_by_device_id(self.hass, service_call.data.get("device_id")) - v = get_vehicle(c) - - data: TimerData = await c.connection.getTimers(c.vin) - if data is None: - raise Exception("No timers found") - - timer_id = int(service_call.data.get("timer_id", -1)) - charging_profile = service_call.data.get("charging_profile", None) - enabled = service_call.data.get("enabled", None) - frequency = service_call.data.get("frequency", None) - departure_time = service_call.data.get("departure_time", None) - departure_datetime = service_call.data.get("departure_datetime", None) - weekday_mask = service_call.data.get("weekday_mask", None) - - timers: dict[int, Timer] = { - 1: data.get_schedule(1), - 2: data.get_schedule(2), - 3: data.get_schedule(3), - } - if frequency is not None: - timers[timer_id].timerFrequency = frequency - if frequency == "single": - if isinstance(departure_datetime, int): - time = datetime.fromtimestamp(departure_datetime) - elif isinstance(departure_datetime, str): - time = datetime.fromisoformat(departure_datetime) - else: - time = departure_datetime - if time.tzinfo is None: - time.replace(tzinfo=pytz.timezone(self.hass.config.time_zone)) - time = time.astimezone(timezone.utc) - timers[timer_id].departureDateTime = time.strftime("%Y-%m-%dT%H:%M") - timers[timer_id].departureTimeOfDay = time.strftime("%H:%M") - elif frequency == "cyclic": - timers[timer_id].departureDateTime = None - timers[timer_id].departureTimeOfDay = self.time_to_utc(departure_time) - timers[timer_id].departureWeekdayMask = weekday_mask - else: - raise Exception(f"Invalid frequency: {frequency}") - - if charging_profile is not None: - timers[timer_id].profileID = charging_profile - - if enabled is not None: - timers[timer_id].timerProgrammedStatus = "programmed" if enabled else "notProgrammed" - - _LOGGER.debug(f"Updating timer {timers[timer_id].json_updated['timer']}") - data.timersAndProfiles.timerList.timer = [timers[1], timers[2], timers[3]] - res = await v.set_schedule(data) - self.hass.add_job(c.async_request_refresh) - return res - - async def update_profile(self, service_call: ServiceCall) -> bool: - """Service for updating charging profiles (locations).""" - c = await get_coordinator_by_device_id(self.hass, service_call.data.get("device_id")) - v = get_vehicle(coordinator=c) - - data: TimerData = await c.connection.getTimers(c.vin) - if data is None: - raise Exception("No profiles found") - - profile_id = int(service_call.data.get("profile_id", -1)) - profile_name = service_call.data.get("profile_name", None) - charging = service_call.data.get("charging", None) - charge_max_current = service_call.data.get("charge_max_current", None) - climatisation = service_call.data.get("climatisation", None) - target_level = service_call.data.get("target_level", None) - night_rate = service_call.data.get("night_rate", None) - night_rate_start = service_call.data.get("night_rate_start", None) - night_rate_end = service_call.data.get("night_rate_end", None) - # heater_source = service_call.data.get("heater_source", None) - - # update timers accordingly - charge_max_current = validate_charge_max_current(charge_max_current) - - profile = data.get_profile(profile_id) - profile.profileName = profile_name if profile_name is not None else profile.profileName - profile.operationCharging = charging if charging is not None else profile.operationCharging - profile.chargeMaxCurrent = charge_max_current if charge_max_current is not None else profile.chargeMaxCurrent - profile.operationClimatisation = climatisation if climatisation is not None else profile.operationClimatisation - profile.targetChargeLevel = target_level if target_level is not None else profile.targetChargeLevel - profile.nightRateActive = night_rate if night_rate is not None else profile.nightRateActive - - if night_rate_start is not None: - profile.nightRateTimeStart = self.time_to_utc(night_rate_start) - if night_rate_end is not None: - profile.nightRateTimeEnd = self.time_to_utc(night_rate_end) - # if heater_source is not None: - # data.timersAndProfiles.timerBasicSetting.set_heater_source(heater_source) - - _LOGGER.debug(f"Updating profile {profile.profileID}: {profile.profileName}") - res = await v.set_schedule(data) - self.hass.add_job(c.async_request_refresh) - return res - - def time_to_utc(self, time_string: str) -> str: - """Convert a local time string to UTC equivalent.""" - tz = pytz.timezone(self.hass.config.time_zone) - target = tz.normalize(datetime.now().replace(tzinfo=tz)).replace( - hour=int(time_string[0:2]), minute=int(time_string[3:5]) - ) - ret = target.astimezone(pytz.utc) - return ret.strftime("%H:%M") - class ChargerService: """Charger services class.""" diff --git a/custom_components/volkswagencarnet/switch.py b/custom_components/volkswagencarnet/switch.py index 57365ac..773b098 100644 --- a/custom_components/volkswagencarnet/switch.py +++ b/custom_components/volkswagencarnet/switch.py @@ -1,11 +1,8 @@ """Support for Volkswagen WeConnect Platform.""" import logging -import re -from datetime import datetime, timezone from typing import Any, Union -import pytz from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import ToggleEntity, EntityCategory from volkswagencarnet.vw_dashboard import Instrument @@ -159,20 +156,4 @@ def entity_category(self) -> Union[EntityCategory, str, None]: def extra_state_attributes(self) -> dict[str, Any]: """Return extra state attributes.""" attribs = super(VolkswagenSwitch, self).extra_state_attributes - if "departure_time" in attribs: - if re.match(r"^\d\d:\d\d$", attribs["departure_time"]): - d = datetime.now() - d = d.replace( - hour=int(attribs["departure_time"][0:2]), - minute=int(attribs["departure_time"][3:5]), - second=0, - microsecond=0, - tzinfo=timezone.utc, - ).astimezone(pytz.timezone(self.hass.config.time_zone)) - attribs["departure_time"] = d.strftime("%H:%M") - else: - d = datetime.strptime(attribs["departure_time"], "%Y-%m-%dT%H:%M").replace( - tzinfo=timezone.utc, second=0, microsecond=0 - ) - attribs["departure_time"] = d return attribs