"""Schema for vendor's TCC v2 API - for GET config of user's Installation. The convention for JSON keys is camelCase, but the API appears to be case-insensitive. """ from __future__ import annotations from typing import TYPE_CHECKING, Final, Literal, NotRequired, TypedDict import voluptuous as vol from _evohome.helpers import noop, redact from .const import ( REGEX_DHW_ID, REGEX_SYSTEM_ID, REGEX_ZONE_ID, S2_ALLOWED_FAN_MODES, S2_ALLOWED_MODES, S2_ALLOWED_SETPOINT_MODES, S2_ALLOWED_STATES, S2_ALLOWED_SYSTEM_MODES, S2_CAN_BE_PERMANENT, S2_CAN_BE_TEMPORARY, S2_CAN_CONTROL_COOL, S2_CAN_CONTROL_HEAT, S2_CITY, S2_COUNTRY, S2_CRC, S2_CURRENT_OFFSET_MINUTES, S2_DHW, S2_DHW_ID, S2_DHW_STATE_CAPABILITIES_RESPONSE, S2_DISPLAY_NAME, S2_DURATION, S2_FAN_MODE, S2_FIRSTNAME, S2_GATEWAY_ID, S2_GATEWAY_INFO, S2_GATEWAYS, S2_IS_CANCELABLE, S2_IS_CHANGEABLE, S2_IS_WI_FI, S2_LASTNAME, S2_LOCATION_ID, S2_LOCATION_INFO, S2_LOCATION_OWNER, S2_LOCATION_TYPE, S2_MAC, S2_MAX_COOL_SETPOINT, S2_MAX_DURATION, S2_MAX_HEAT_SETPOINT, S2_MAX_SWITCHPOINTS_PER_DAY, S2_MIN_COOL_SETPOINT, S2_MIN_DURATION, S2_MIN_HEAT_SETPOINT, S2_MIN_SWITCHPOINTS_PER_DAY, S2_MODEL_TYPE, S2_NAME, S2_OFFSET_MINUTES, S2_PERIOD, S2_POSTCODE, S2_SCHEDULE_CAPABILITIES, S2_SCHEDULE_CAPABILITIES_RESPONSE, S2_SETPOINT_CAPABILITIES, S2_SETPOINT_DEADBAND, S2_SETPOINT_VALUE_RESOLUTION, S2_STREET_ADDRESS, S2_SUPPORTS_DAYLIGHT_SAVING, S2_SYSTEM_ID, S2_SYSTEM_MODE, S2_TEMPERATURE_CONTROL_SYSTEMS, S2_TIME_ZONE, S2_TIME_ZONE_ID, S2_TIMING_MODE, S2_TIMING_RESOLUTION, S2_USE_DAYLIGHT_SAVE_SWITCHING, S2_USER_ID, S2_USERNAME, S2_VACATION_HOLD_CAPABILITIES, S2_VALUE_RESOLUTION, S2_ZONE_ID, S2_ZONE_TYPE, S2_ZONES, DhwState, FanMode, LocationType, SystemMode, TcsModelType, ZoneMode, ZoneModelType, ZoneType, ) if TYPE_CHECKING: from collections.abc import Callable # These are best guess MAX_HEAT_SETPOINT_LOWER: Final = 21.0 MAX_HEAT_SETPOINT_UPPER: Final = 35.0 MIN_HEAT_SETPOINT_LOWER: Final = 4.5 MIN_HEAT_SETPOINT_UPPER: Final = 21.0 # GET /location/installationInfo?userId={user_id} returns list of these dicts class TccLocConfigResponseT(TypedDict): """Response to GET /locations?userId={user_id}&allData=True The response is a list of these dicts. """ locationInfo: TccLocConfigEntryT gateways: list[TccGwyConfigResponseT] class TccLocConfigEntryT(TypedDict): """Location configuration information.""" locationId: str name: str streetAddress: str city: str state: str country: str postcode: str type: str locationType: LocationType useDaylightSaveSwitching: bool timeZone: TccTimeZoneInfoT locationOwner: TccLocationOwnerInfoT class TccTimeZoneInfoT(TypedDict): """Time zone information.""" timeZoneId: str displayName: str offsetMinutes: int currentOffsetMinutes: int supportsDaylightSaving: bool class TccLocationOwnerInfoT(TypedDict): userId: str username: str firstname: str lastname: str class TccGwyConfigResponseT(TypedDict): gatewayInfo: TccGwyConfigEntryT temperatureControlSystems: list[TccTcsConfigResponseT] class TccGwyConfigEntryT(TypedDict): gatewayId: str mac: str crc: str isWiFi: bool class TccTcsConfigEntryT(TypedDict): systemId: str modelType: TcsModelType allowedSystemModes: list[TccAllowedSystemModeResponseT] class TccAllowedSystemModeResponseT(TypedDict): systemMode: SystemMode canBePermanent: Literal[True] canBeTemporary: bool maxDuration: NotRequired[str] timingResolution: NotRequired[str] timingMode: NotRequired[str] class TccTcsConfigResponseT(TccTcsConfigEntryT): # system_id: str # model_type: str # allowed_system_modes: list[dict[str, Any]] zones: list[TccZonConfigResponseT] dhw: NotRequired[TccDhwConfigResponseT] class TccZonConfigResponseT(TypedDict): zoneId: str modelType: ZoneModelType name: str setpointCapabilities: TccZonSetpointCapabilitiesResponseT scheduleCapabilities: TccZonScheduleCapabilitiesResponseT zoneType: ZoneType allowedFanModes: list[str] class TccZonScheduleCapabilitiesResponseT(TypedDict): pass class TccZonSetpointCapabilitiesResponseT(TypedDict): allowedSetpointModes: list[ZoneMode] canControlCool: bool canControlHeat: bool maxHeatSetpoint: float minHeatSetpoint: float valueResolution: float maxDuration: str timingResolution: str class TccZonConfigEntryT(TccZonConfigResponseT): pass class TccDhwConfigResponseT(TypedDict): dhwId: str scheduleCapabilitiesResponse: TccDhwScheduleCapabilitiesResponseT dhwStateCapabilitiesResponse: TccDhwStateCapabilitiesResponseT class TccDhwScheduleCapabilitiesResponseT(TypedDict): pass class TccDhwStateCapabilitiesResponseT(TypedDict): allowedStates: list[DhwState] allowedModes: list[ZoneMode] maxDuration: str timingResolution: str class TccDhwConfigEntryT(TccDhwConfigResponseT): pass def factory_system_mode_perm(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the permanent system modes schema.""" return vol.Schema( { vol.Required(fnc(S2_SYSTEM_MODE)): vol.Any( str(SystemMode.AUTO), str(SystemMode.AUTO_WITH_RESET), str(SystemMode.HEATING_OFF), str(SystemMode.OFF), # not evohome str(SystemMode.HEAT), # not evohome str(SystemMode.COOL), # not evohome ), vol.Required(fnc(S2_CAN_BE_PERMANENT)): True, vol.Required(fnc(S2_CAN_BE_TEMPORARY)): False, }, extra=vol.PREVENT_EXTRA, ) def factory_system_mode_temp(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the temporary system modes schema.""" return vol.Schema( { vol.Required(fnc(S2_SYSTEM_MODE)): vol.Any( str(SystemMode.AUTO_WITH_ECO), str(SystemMode.AWAY), str(SystemMode.CUSTOM), str(SystemMode.DAY_OFF), ), vol.Required(fnc(S2_CAN_BE_PERMANENT)): True, vol.Required(fnc(S2_CAN_BE_TEMPORARY)): True, vol.Required(fnc(S2_MAX_DURATION)): str, # "99.00:00:00" vol.Required(fnc(S2_TIMING_RESOLUTION)): str, # "1.00:00:00" vol.Required(fnc(S2_TIMING_MODE)): vol.Any(S2_DURATION, S2_PERIOD), }, extra=vol.PREVENT_EXTRA, ) def factory_schedule_capabilities_response( fnc: Callable[[str], str] = noop, ) -> vol.Schema: """Factory for the schedule_capabilities_response schema.""" return vol.Schema( { vol.Required(fnc(S2_MAX_SWITCHPOINTS_PER_DAY)): int, # 6 vol.Required(fnc(S2_MIN_SWITCHPOINTS_PER_DAY)): int, # 1 vol.Required(fnc(S2_TIMING_RESOLUTION)): vol.Datetime( format="00:%M:00" ), # "00:10:00" }, extra=vol.PREVENT_EXTRA, ) def factory_dhw(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the DHW schema.""" SCH_DHW_STATE_CAPABILITIES_RESPONSE: Final = vol.Schema( { # TODO: list should be a non-empty *subset* of all possible DhwState(s) vol.Required(fnc(S2_ALLOWED_STATES)): [m.value for m in DhwState], # TODO: list should be a non-empty *subset* of all possible ZoneMode(s) vol.Required(fnc(S2_ALLOWED_MODES)): [m.value for m in ZoneMode], vol.Required(fnc(S2_MAX_DURATION)): str, vol.Required(fnc(S2_TIMING_RESOLUTION)): vol.Datetime(format="00:%M:00"), }, extra=vol.PREVENT_EXTRA, ) return vol.Schema( { vol.Required(fnc(S2_DHW_ID)): vol.Match(REGEX_DHW_ID), vol.Required( fnc(S2_DHW_STATE_CAPABILITIES_RESPONSE) ): SCH_DHW_STATE_CAPABILITIES_RESPONSE, vol.Required( fnc(S2_SCHEDULE_CAPABILITIES_RESPONSE) ): factory_schedule_capabilities_response(fnc), }, extra=vol.PREVENT_EXTRA, ) def factory_zone(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the zone schema.""" SCH_FAN_MODE: Final = vol.Schema( # noqa: F841 { vol.Required(fnc(S2_FAN_MODE)): vol.In(FanMode), }, extra=vol.PREVENT_EXTRA, ) SCH_VACATION_HOLD_CAPABILITIES: Final = vol.Schema( { vol.Required(fnc(S2_IS_CHANGEABLE)): bool, vol.Required(fnc(S2_IS_CANCELABLE)): bool, vol.Optional(fnc(S2_MAX_DURATION)): str, vol.Optional(fnc(S2_MIN_DURATION)): str, vol.Optional(fnc(S2_TIMING_RESOLUTION)): vol.Datetime(format="00:%M:00"), }, extra=vol.PREVENT_EXTRA, ) SCH_SETPOINT_CAPABILITIES: Final = vol.Schema( # min/max as per evohome { vol.Required(fnc(S2_CAN_CONTROL_HEAT)): bool, vol.Required(fnc(S2_MAX_HEAT_SETPOINT)): vol.All( float, vol.Range(min=MAX_HEAT_SETPOINT_LOWER, max=MAX_HEAT_SETPOINT_UPPER), ), vol.Required(fnc(S2_MIN_HEAT_SETPOINT)): vol.All( float, vol.Range(min=MIN_HEAT_SETPOINT_LOWER, max=MIN_HEAT_SETPOINT_UPPER), ), vol.Required(fnc(S2_CAN_CONTROL_COOL)): bool, vol.Optional(fnc(S2_MAX_COOL_SETPOINT)): float, # TODO vol.Optional(fnc(S2_MIN_COOL_SETPOINT)): float, # TODO # TODO: list should be a non-empty *subset* of all possible ZoneMode(s) vol.Required(fnc(S2_ALLOWED_SETPOINT_MODES)): [m.value for m in ZoneMode], vol.Required(fnc(S2_VALUE_RESOLUTION)): float, # 0.5 vol.Required(fnc(S2_MAX_DURATION)): str, # "1.00:00:00" vol.Required(fnc(S2_TIMING_RESOLUTION)): vol.Datetime( format="00:%M:00" ), # "00:10:00" vol.Optional( fnc(S2_VACATION_HOLD_CAPABILITIES) ): SCH_VACATION_HOLD_CAPABILITIES, # non-evohome # vol.Optional((S2_ALLOWED_FAN_MODES)): dict, # non-evohome vol.Optional(fnc(S2_SETPOINT_DEADBAND)): float, # non-evohome }, extra=vol.PREVENT_EXTRA, ) SCH_SCHEDULE_CAPABILITIES = factory_schedule_capabilities_response(fnc).extend( { vol.Required(fnc(S2_SETPOINT_VALUE_RESOLUTION)): float, }, extra=vol.PREVENT_EXTRA, ) # schedule_capabilities is required for evo, optional for FocusProWifiRetail return vol.Schema( { vol.Required(fnc(S2_ZONE_ID)): vol.Match(REGEX_ZONE_ID), vol.Required(fnc(S2_MODEL_TYPE)): vol.In(ZoneModelType), vol.Required(fnc(S2_NAME)): str, vol.Required(fnc(S2_SETPOINT_CAPABILITIES)): SCH_SETPOINT_CAPABILITIES, vol.Optional(fnc(S2_SCHEDULE_CAPABILITIES)): SCH_SCHEDULE_CAPABILITIES, vol.Required(fnc(S2_ZONE_TYPE)): vol.In(ZoneType), vol.Optional(fnc(S2_ALLOWED_FAN_MODES)): list, # FocusProWifiRetail }, extra=vol.PREVENT_EXTRA, ) def factory_tcs(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the TCS schema.""" SCH_ALLOWED_SYSTEM_MODES: Final = vol.Any( factory_system_mode_perm(fnc), factory_system_mode_temp(fnc) ) return vol.Schema( { vol.Required(fnc(S2_SYSTEM_ID)): vol.Match(REGEX_SYSTEM_ID), vol.Required(fnc(S2_MODEL_TYPE)): vol.In(TcsModelType), vol.Required(fnc(S2_ALLOWED_SYSTEM_MODES)): [SCH_ALLOWED_SYSTEM_MODES], vol.Required(fnc(S2_ZONES)): vol.All( [factory_zone(fnc)], vol.Length(min=1, max=12) ), vol.Optional(fnc(S2_DHW)): factory_dhw(fnc), }, extra=vol.PREVENT_EXTRA, ) def factory_gateway(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the gateway schema.""" SCH_GATEWAY_INFO: Final = vol.Schema( { vol.Required(fnc(S2_GATEWAY_ID)): str, vol.Required(fnc(S2_MAC)): str, vol.Required(fnc(S2_CRC)): vol.All(str, redact), vol.Required(fnc(S2_IS_WI_FI)): bool, }, extra=vol.PREVENT_EXTRA, ) return vol.Schema( { vol.Required(fnc(S2_GATEWAY_INFO)): SCH_GATEWAY_INFO, vol.Required(fnc(S2_TEMPERATURE_CONTROL_SYSTEMS)): [factory_tcs(fnc)], }, extra=vol.PREVENT_EXTRA, ) def factory_time_zone(fnc: Callable[[str], str] = noop) -> vol.Schema: """Factory for the time zone schema.""" return vol.Schema( { vol.Required(fnc(S2_TIME_ZONE_ID)): str, vol.Required(fnc(S2_DISPLAY_NAME)): str, vol.Required(fnc(S2_OFFSET_MINUTES)): int, vol.Required(fnc(S2_CURRENT_OFFSET_MINUTES)): int, vol.Required(fnc(S2_SUPPORTS_DAYLIGHT_SAVING)): bool, }, extra=vol.PREVENT_EXTRA, ) def factory_location_installation_info( fnc: Callable[[str], str] = noop, ) -> vol.Schema: """Factory for the location (config) schema.""" SCH_LOCATION_OWNER: Final = vol.Schema( { vol.Required(fnc(S2_USER_ID)): str, vol.Required(fnc(S2_USERNAME)): vol.All(vol.Email(), redact), # pyright: ignore[reportCallIssue] vol.Required(fnc(S2_FIRSTNAME)): str, vol.Required(fnc(S2_LASTNAME)): vol.All(str, redact), }, extra=vol.PREVENT_EXTRA, ) SCH_LOCATION_INFO: Final = vol.Schema( { vol.Required(fnc(S2_LOCATION_ID)): str, vol.Required(fnc(S2_NAME)): str, # e.g. "My Home" vol.Required(fnc(S2_STREET_ADDRESS)): vol.All(str, redact), vol.Required(fnc(S2_CITY)): vol.All(str, redact), vol.Required(fnc(S2_COUNTRY)): str, vol.Required(fnc(S2_POSTCODE)): vol.All(str, redact), vol.Required(fnc(S2_LOCATION_TYPE)): vol.In(LocationType), # "Residential" vol.Required(fnc(S2_USE_DAYLIGHT_SAVE_SWITCHING)): bool, vol.Required(fnc(S2_TIME_ZONE)): factory_time_zone(fnc), vol.Required(fnc(S2_LOCATION_OWNER)): SCH_LOCATION_OWNER, }, extra=vol.PREVENT_EXTRA, ) return vol.Schema( { vol.Required(fnc(S2_LOCATION_INFO)): SCH_LOCATION_INFO, vol.Required(fnc(S2_GATEWAYS)): [factory_gateway(fnc)], }, extra=vol.PREVENT_EXTRA, ) def factory_user_locations_installation_info( fnc: Callable[[str], str] = noop, ) -> vol.Schema: """Factory for the user locations (config) schema.""" return vol.Schema( [factory_location_installation_info(fnc)], extra=vol.PREVENT_EXTRA, )