1 """Implement the Google Smart Home traits."""
3 from __future__
import annotations
5 from abc
import ABC, abstractmethod
6 from datetime
import datetime, timedelta
37 AlarmControlPanelEntityFeature,
38 AlarmControlPanelState,
58 ATTR_SUPPORTED_FEATURES,
60 CAST_APP_ID_HOMEASSISTANT_MEDIA,
61 SERVICE_ALARM_ARM_AWAY,
62 SERVICE_ALARM_ARM_CUSTOM_BYPASS,
63 SERVICE_ALARM_ARM_HOME,
64 SERVICE_ALARM_ARM_NIGHT,
66 SERVICE_ALARM_TRIGGER,
84 ordered_list_item_to_percentage,
85 percentage_to_ordered_list_item,
90 CHALLENGE_FAILED_PIN_NEEDED,
95 ERR_CHALLENGE_NOT_SETUP,
96 ERR_FUNCTION_NOT_SUPPORTED,
97 ERR_NO_AVAILABLE_CHANNEL,
99 ERR_UNSUPPORTED_INPUT,
100 ERR_VALUE_OUT_OF_RANGE,
103 from .error
import ChallengeNeeded, SmartHomeError
105 _LOGGER = logging.getLogger(__name__)
107 PREFIX_TRAITS =
"action.devices.traits."
108 TRAIT_ARM_DISARM = f
"{PREFIX_TRAITS}ArmDisarm"
109 TRAIT_BRIGHTNESS = f
"{PREFIX_TRAITS}Brightness"
110 TRAIT_CAMERA_STREAM = f
"{PREFIX_TRAITS}CameraStream"
111 TRAIT_CHANNEL = f
"{PREFIX_TRAITS}Channel"
112 TRAIT_COLOR_SETTING = f
"{PREFIX_TRAITS}ColorSetting"
113 TRAIT_DOCK = f
"{PREFIX_TRAITS}Dock"
114 TRAIT_ENERGY_STORAGE = f
"{PREFIX_TRAITS}EnergyStorage"
115 TRAIT_FAN_SPEED = f
"{PREFIX_TRAITS}FanSpeed"
116 TRAIT_HUMIDITY_SETTING = f
"{PREFIX_TRAITS}HumiditySetting"
117 TRAIT_INPUT_SELECTOR = f
"{PREFIX_TRAITS}InputSelector"
118 TRAIT_LOCATOR = f
"{PREFIX_TRAITS}Locator"
119 TRAIT_LOCK_UNLOCK = f
"{PREFIX_TRAITS}LockUnlock"
120 TRAIT_MEDIA_STATE = f
"{PREFIX_TRAITS}MediaState"
121 TRAIT_MODES = f
"{PREFIX_TRAITS}Modes"
122 TRAIT_OBJECT_DETECTION = f
"{PREFIX_TRAITS}ObjectDetection"
123 TRAIT_ON_OFF = f
"{PREFIX_TRAITS}OnOff"
124 TRAIT_OPEN_CLOSE = f
"{PREFIX_TRAITS}OpenClose"
125 TRAIT_SCENE = f
"{PREFIX_TRAITS}Scene"
126 TRAIT_SENSOR_STATE = f
"{PREFIX_TRAITS}SensorState"
127 TRAIT_START_STOP = f
"{PREFIX_TRAITS}StartStop"
128 TRAIT_TEMPERATURE_CONTROL = f
"{PREFIX_TRAITS}TemperatureControl"
129 TRAIT_TEMPERATURE_SETTING = f
"{PREFIX_TRAITS}TemperatureSetting"
130 TRAIT_TRANSPORT_CONTROL = f
"{PREFIX_TRAITS}TransportControl"
131 TRAIT_VOLUME = f
"{PREFIX_TRAITS}Volume"
133 PREFIX_COMMANDS =
"action.devices.commands."
134 COMMAND_ACTIVATE_SCENE = f
"{PREFIX_COMMANDS}ActivateScene"
135 COMMAND_ARM_DISARM = f
"{PREFIX_COMMANDS}ArmDisarm"
136 COMMAND_BRIGHTNESS_ABSOLUTE = f
"{PREFIX_COMMANDS}BrightnessAbsolute"
137 COMMAND_CHARGE = f
"{PREFIX_COMMANDS}Charge"
138 COMMAND_COLOR_ABSOLUTE = f
"{PREFIX_COMMANDS}ColorAbsolute"
139 COMMAND_DOCK = f
"{PREFIX_COMMANDS}Dock"
140 COMMAND_GET_CAMERA_STREAM = f
"{PREFIX_COMMANDS}GetCameraStream"
141 COMMAND_LOCK_UNLOCK = f
"{PREFIX_COMMANDS}LockUnlock"
142 COMMAND_LOCATE = f
"{PREFIX_COMMANDS}Locate"
143 COMMAND_NEXT_INPUT = f
"{PREFIX_COMMANDS}NextInput"
144 COMMAND_MEDIA_NEXT = f
"{PREFIX_COMMANDS}mediaNext"
145 COMMAND_MEDIA_PAUSE = f
"{PREFIX_COMMANDS}mediaPause"
146 COMMAND_MEDIA_PREVIOUS = f
"{PREFIX_COMMANDS}mediaPrevious"
147 COMMAND_MEDIA_RESUME = f
"{PREFIX_COMMANDS}mediaResume"
148 COMMAND_MEDIA_SEEK_RELATIVE = f
"{PREFIX_COMMANDS}mediaSeekRelative"
149 COMMAND_MEDIA_SEEK_TO_POSITION = f
"{PREFIX_COMMANDS}mediaSeekToPosition"
150 COMMAND_MEDIA_SHUFFLE = f
"{PREFIX_COMMANDS}mediaShuffle"
151 COMMAND_MEDIA_STOP = f
"{PREFIX_COMMANDS}mediaStop"
152 COMMAND_MUTE = f
"{PREFIX_COMMANDS}mute"
153 COMMAND_OPEN_CLOSE = f
"{PREFIX_COMMANDS}OpenClose"
154 COMMAND_ON_OFF = f
"{PREFIX_COMMANDS}OnOff"
155 COMMAND_OPEN_CLOSE_RELATIVE = f
"{PREFIX_COMMANDS}OpenCloseRelative"
156 COMMAND_PAUSE_UNPAUSE = f
"{PREFIX_COMMANDS}PauseUnpause"
157 COMMAND_REVERSE = f
"{PREFIX_COMMANDS}Reverse"
158 COMMAND_PREVIOUS_INPUT = f
"{PREFIX_COMMANDS}PreviousInput"
159 COMMAND_SELECT_CHANNEL = f
"{PREFIX_COMMANDS}selectChannel"
160 COMMAND_SET_TEMPERATURE = f
"{PREFIX_COMMANDS}SetTemperature"
161 COMMAND_SET_FAN_SPEED = f
"{PREFIX_COMMANDS}SetFanSpeed"
162 COMMAND_SET_FAN_SPEED_RELATIVE = f
"{PREFIX_COMMANDS}SetFanSpeedRelative"
163 COMMAND_SET_HUMIDITY = f
"{PREFIX_COMMANDS}SetHumidity"
164 COMMAND_SET_INPUT = f
"{PREFIX_COMMANDS}SetInput"
165 COMMAND_SET_MODES = f
"{PREFIX_COMMANDS}SetModes"
166 COMMAND_SET_VOLUME = f
"{PREFIX_COMMANDS}setVolume"
167 COMMAND_START_STOP = f
"{PREFIX_COMMANDS}StartStop"
168 COMMAND_THERMOSTAT_SET_MODE = f
"{PREFIX_COMMANDS}ThermostatSetMode"
169 COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = (
170 f
"{PREFIX_COMMANDS}ThermostatTemperatureSetpoint"
172 COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
173 f
"{PREFIX_COMMANDS}ThermostatTemperatureSetRange"
175 COMMAND_VOLUME_RELATIVE = f
"{PREFIX_COMMANDS}volumeRelative"
177 TRAITS: list[type[_Trait]] = []
179 FAN_SPEED_MAX_SPEED_COUNT = 5
181 COVER_VALVE_STATES = {
183 "closed": cover.STATE_CLOSED,
184 "closing": cover.STATE_CLOSING,
185 "open": cover.STATE_OPEN,
186 "opening": cover.STATE_OPENING,
189 "closed": valve.STATE_CLOSED,
190 "closing": valve.STATE_CLOSING,
191 "open": valve.STATE_OPEN,
192 "opening": valve.STATE_OPENING,
196 SERVICE_STOP_COVER_VALVE = {
197 cover.DOMAIN: cover.SERVICE_STOP_COVER,
198 valve.DOMAIN: valve.SERVICE_STOP_VALVE,
200 SERVICE_OPEN_COVER_VALVE = {
201 cover.DOMAIN: cover.SERVICE_OPEN_COVER,
202 valve.DOMAIN: valve.SERVICE_OPEN_VALVE,
204 SERVICE_CLOSE_COVER_VALVE = {
205 cover.DOMAIN: cover.SERVICE_CLOSE_COVER,
206 valve.DOMAIN: valve.SERVICE_CLOSE_VALVE,
208 SERVICE_TOGGLE_COVER_VALVE = {
209 cover.DOMAIN: cover.SERVICE_TOGGLE,
210 valve.DOMAIN: valve.SERVICE_TOGGLE,
212 SERVICE_SET_POSITION_COVER_VALVE = {
213 cover.DOMAIN: cover.SERVICE_SET_COVER_POSITION,
214 valve.DOMAIN: valve.SERVICE_SET_VALVE_POSITION,
217 COVER_VALVE_CURRENT_POSITION = {
218 cover.DOMAIN: cover.ATTR_CURRENT_POSITION,
219 valve.DOMAIN: valve.ATTR_CURRENT_POSITION,
222 COVER_VALVE_POSITION = {
223 cover.DOMAIN: cover.ATTR_POSITION,
224 valve.DOMAIN: valve.ATTR_POSITION,
227 COVER_VALVE_SET_POSITION_FEATURE = {
228 cover.DOMAIN: CoverEntityFeature.SET_POSITION,
229 valve.DOMAIN: ValveEntityFeature.SET_POSITION,
231 COVER_VALVE_STOP_FEATURE = {
232 cover.DOMAIN: CoverEntityFeature.STOP,
233 valve.DOMAIN: ValveEntityFeature.STOP,
236 COVER_VALVE_DOMAINS = {cover.DOMAIN, valve.DOMAIN}
238 FRIENDLY_DOMAIN = {cover.DOMAIN:
"Cover", valve.DOMAIN:
"Valve"}
241 def register_trait[_TraitT: _Trait](trait: type[_TraitT]) -> type[_TraitT]:
242 """Decorate a class to register a trait."""
248 """Return Google temperature unit."""
249 if units == UnitOfTemperature.FAHRENHEIT:
255 """Return the next item in an item list starting at given value.
257 If selected is missing in items, None is returned
262 index = items.index(selected)
266 next_item = 0
if index == len(items) - 1
else index + 1
267 return items[next_item]
271 """Represents a Trait inside Google Assistant skill."""
274 commands: list[str] = []
278 """Return if the trait might ask for 2FA."""
283 def supported(domain, features, device_class, attributes):
284 """Test if state is supported."""
286 def __init__(self, hass: HomeAssistant, state, config) ->
None:
287 """Initialize a trait for a state."""
293 """Return attributes for a sync request."""
294 raise NotImplementedError
297 """Add options for the sync request."""
301 """Return the attributes of this trait for this entity."""
302 raise NotImplementedError
305 """Return notifications payload."""
308 """Test if command can be executed."""
309 return command
in self.commands
311 async
def execute(self, command, data, params, challenge):
312 """Execute a trait command."""
313 raise NotImplementedError
318 """Trait to control brightness of a device.
320 https://developers.google.com/actions/smarthome/traits/brightness
323 name = TRAIT_BRIGHTNESS
324 commands = [COMMAND_BRIGHTNESS_ABSOLUTE]
327 def supported(domain, features, device_class, attributes):
328 """Test if state is supported."""
329 if domain == light.DOMAIN:
330 color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES)
331 return light.brightness_supported(color_modes)
336 """Return brightness attributes for a sync request."""
340 """Return brightness query attributes."""
341 domain = self.
statestate.domain
344 if domain == light.DOMAIN:
345 brightness = self.
statestate.attributes.get(light.ATTR_BRIGHTNESS)
346 if brightness
is not None:
347 response[
"brightness"] = round(100 * (brightness / 255))
351 async
def execute(self, command, data, params, challenge):
352 """Execute a brightness command."""
353 if self.
statestate.domain == light.DOMAIN:
354 await self.
hasshass.services.async_call(
356 light.SERVICE_TURN_ON,
358 ATTR_ENTITY_ID: self.
statestate.entity_id,
359 light.ATTR_BRIGHTNESS_PCT: params[
"brightness"],
361 blocking=
not self.
configconfig.should_report_state,
362 context=data.context,
368 """Trait to stream from cameras.
370 https://developers.google.com/actions/smarthome/traits/camerastream
373 name = TRAIT_CAMERA_STREAM
374 commands = [COMMAND_GET_CAMERA_STREAM]
376 stream_info: dict[str, str] |
None =
None
380 """Test if state is supported."""
381 if domain == camera.DOMAIN:
382 return features & CameraEntityFeature.STREAM
387 """Return stream attributes for a sync request."""
389 "cameraStreamSupportedProtocols": [
"hls"],
390 "cameraStreamNeedAuthToken":
False,
391 "cameraStreamNeedDrmEncryption":
False,
395 """Return camera stream attributes."""
398 async
def execute(self, command, data, params, challenge):
399 """Execute a get camera stream command."""
400 url = await camera.async_request_stream(self.
hasshass, self.
statestate.entity_id,
"hls")
402 "cameraStreamAccessUrl": f
"{get_url(self.hass)}{url}",
403 "cameraStreamReceiverAppId": CAST_APP_ID_HOMEASSISTANT_MEDIA,
409 """Trait to object detection.
411 https://developers.google.com/actions/smarthome/traits/objectdetection
414 name = TRAIT_OBJECT_DETECTION
418 def supported(domain, features, device_class, _) -> bool:
419 """Test if state is supported."""
421 domain == event.DOMAIN
and device_class == event.EventDeviceClass.DOORBELL
425 """Return ObjectDetection attributes for a sync request."""
429 """Add options for the sync request."""
430 return {
"notificationSupportedByAgent":
True}
433 """Return ObjectDetection query attributes."""
437 """Return notifications payload."""
439 if self.
statestate.state
in {STATE_UNKNOWN, STATE_UNAVAILABLE}:
443 time_stamp: datetime = datetime.fromisoformat(self.
statestate.state)
457 "detectionTimestamp":
int(time_stamp.timestamp() * 1000),
461 async
def execute(self, command, data, params, challenge):
462 """Execute an ObjectDetection command."""
467 """Trait to offer basic on and off functionality.
469 https://developers.google.com/actions/smarthome/traits/onoff
473 commands = [COMMAND_ON_OFF]
477 """Test if state is supported."""
478 if domain == water_heater.DOMAIN
and features & WaterHeaterEntityFeature.ON_OFF:
481 if domain == climate.DOMAIN
and features & (
482 ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
488 input_boolean.DOMAIN,
497 """Return OnOff attributes for a sync request."""
498 if self.
statestate.attributes.get(ATTR_ASSUMED_STATE,
False):
499 return {
"commandOnlyOnOff":
True}
503 """Return OnOff query attributes."""
504 return {
"on": self.
statestate.state
not in (STATE_OFF, STATE_UNKNOWN)}
506 async
def execute(self, command, data, params, challenge):
507 """Execute an OnOff command."""
508 if (domain := self.
statestate.domain) == group.DOMAIN:
509 service_domain = HOMEASSISTANT_DOMAIN
510 service = SERVICE_TURN_ON
if params[
"on"]
else SERVICE_TURN_OFF
513 service_domain = domain
514 service = SERVICE_TURN_ON
if params[
"on"]
else SERVICE_TURN_OFF
516 await self.
hasshass.services.async_call(
519 {ATTR_ENTITY_ID: self.
statestate.entity_id},
520 blocking=
not self.
configconfig.should_report_state,
521 context=data.context,
527 """Trait to offer color temperature functionality.
529 https://developers.google.com/actions/smarthome/traits/colortemperature
532 name = TRAIT_COLOR_SETTING
533 commands = [COMMAND_COLOR_ABSOLUTE]
536 def supported(domain, features, device_class, attributes):
537 """Test if state is supported."""
538 if domain != light.DOMAIN:
541 color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES)
542 return light.color_temp_supported(color_modes)
or light.color_supported(
547 """Return color temperature attributes for a sync request."""
548 attrs = self.
statestate.attributes
549 color_modes = attrs.get(light.ATTR_SUPPORTED_COLOR_MODES)
550 response: dict[str, Any] = {}
552 if light.color_supported(color_modes):
553 response[
"colorModel"] =
"hsv"
555 if light.color_temp_supported(color_modes):
558 response[
"colorTemperatureRange"] = {
559 "temperatureMaxK": color_util.color_temperature_mired_to_kelvin(
560 attrs.get(light.ATTR_MIN_MIREDS)
562 "temperatureMinK": color_util.color_temperature_mired_to_kelvin(
563 attrs.get(light.ATTR_MAX_MIREDS)
570 """Return color temperature query attributes."""
571 color_mode = self.
statestate.attributes.get(light.ATTR_COLOR_MODE)
573 color: dict[str, Any] = {}
575 if light.color_supported([color_mode]):
576 color_hs = self.
statestate.attributes.get(light.ATTR_HS_COLOR)
577 brightness = self.
statestate.attributes.get(light.ATTR_BRIGHTNESS, 1)
578 if color_hs
is not None:
579 color[
"spectrumHsv"] = {
581 "saturation": color_hs[1] / 100,
582 "value": brightness / 255,
585 if light.color_temp_supported([color_mode]):
586 temp = self.
statestate.attributes.get(light.ATTR_COLOR_TEMP)
590 "Entity %s has incorrect color temperature %s",
591 self.
statestate.entity_id,
594 elif temp
is not None:
595 color[
"temperatureK"] = color_util.color_temperature_mired_to_kelvin(
602 response[
"color"] = color
606 async
def execute(self, command, data, params, challenge):
607 """Execute a color temperature command."""
608 if "temperature" in params[
"color"]:
609 temp = color_util.color_temperature_kelvin_to_mired(
610 params[
"color"][
"temperature"]
612 min_temp = self.
statestate.attributes[light.ATTR_MIN_MIREDS]
613 max_temp = self.
statestate.attributes[light.ATTR_MAX_MIREDS]
615 if temp < min_temp
or temp > max_temp:
617 ERR_VALUE_OUT_OF_RANGE,
618 f
"Temperature should be between {min_temp} and {max_temp}",
621 await self.
hasshass.services.async_call(
624 {ATTR_ENTITY_ID: self.
statestate.entity_id, light.ATTR_COLOR_TEMP: temp},
625 blocking=
not self.
configconfig.should_report_state,
626 context=data.context,
629 elif "spectrumRGB" in params[
"color"]:
631 hex_value = f
"{params['color']['spectrumRGB']:06x}"
632 color = color_util.color_RGB_to_hs(
633 *color_util.rgb_hex_to_rgb_list(hex_value)
636 await self.
hasshass.services.async_call(
639 {ATTR_ENTITY_ID: self.
statestate.entity_id, light.ATTR_HS_COLOR: color},
640 blocking=
not self.
configconfig.should_report_state,
641 context=data.context,
644 elif "spectrumHSV" in params[
"color"]:
645 color = params[
"color"][
"spectrumHSV"]
646 saturation = color[
"saturation"] * 100
647 brightness = color[
"value"] * 255
649 await self.
hasshass.services.async_call(
653 ATTR_ENTITY_ID: self.
statestate.entity_id,
654 light.ATTR_HS_COLOR: [color[
"hue"], saturation],
655 light.ATTR_BRIGHTNESS: brightness,
657 blocking=
not self.
configconfig.should_report_state,
658 context=data.context,
664 """Trait to offer scene functionality.
666 https://developers.google.com/actions/smarthome/traits/scene
670 commands = [COMMAND_ACTIVATE_SCENE]
674 """Test if state is supported."""
683 """Return scene attributes for a sync request."""
688 """Return scene query attributes."""
691 async
def execute(self, command, data, params, challenge):
692 """Execute a scene command."""
693 service = SERVICE_TURN_ON
694 if self.
statestate.domain == button.DOMAIN:
695 service = button.SERVICE_PRESS
696 elif self.
statestate.domain == input_button.DOMAIN:
697 service = input_button.SERVICE_PRESS
700 await self.
hasshass.services.async_call(
701 self.
statestate.domain,
703 {ATTR_ENTITY_ID: self.
statestate.entity_id},
704 blocking=(
not self.
configconfig.should_report_state)
705 and self.
statestate.domain
706 not in (button.DOMAIN, input_button.DOMAIN, script.DOMAIN),
707 context=data.context,
713 """Trait to offer dock functionality.
715 https://developers.google.com/actions/smarthome/traits/dock
719 commands = [COMMAND_DOCK]
723 """Test if state is supported."""
724 return domain == vacuum.DOMAIN
727 """Return dock attributes for a sync request."""
731 """Return dock query attributes."""
732 return {
"isDocked": self.
statestate.state == vacuum.STATE_DOCKED}
734 async
def execute(self, command, data, params, challenge):
735 """Execute a dock command."""
736 await self.
hasshass.services.async_call(
737 self.
statestate.domain,
738 vacuum.SERVICE_RETURN_TO_BASE,
739 {ATTR_ENTITY_ID: self.
statestate.entity_id},
740 blocking=
not self.
configconfig.should_report_state,
741 context=data.context,
747 """Trait to offer locate functionality.
749 https://developers.google.com/actions/smarthome/traits/locator
753 commands = [COMMAND_LOCATE]
757 """Test if state is supported."""
758 return domain == vacuum.DOMAIN
and features & VacuumEntityFeature.LOCATE
761 """Return locator attributes for a sync request."""
765 """Return locator query attributes."""
768 async
def execute(self, command, data, params, challenge):
769 """Execute a locate command."""
770 if params.get(
"silence",
False):
772 ERR_FUNCTION_NOT_SUPPORTED,
773 "Silencing a Locate request is not yet supported",
776 await self.
hasshass.services.async_call(
777 self.
statestate.domain,
778 vacuum.SERVICE_LOCATE,
779 {ATTR_ENTITY_ID: self.
statestate.entity_id},
780 blocking=
not self.
configconfig.should_report_state,
781 context=data.context,
787 """Trait to offer EnergyStorage functionality.
789 https://developers.google.com/actions/smarthome/traits/energystorage
792 name = TRAIT_ENERGY_STORAGE
793 commands = [COMMAND_CHARGE]
797 """Test if state is supported."""
798 return domain == vacuum.DOMAIN
and features & VacuumEntityFeature.BATTERY
801 """Return EnergyStorage attributes for a sync request."""
803 "isRechargeable":
True,
804 "queryOnlyEnergyStorage":
True,
808 """Return EnergyStorage query attributes."""
809 battery_level = self.
statestate.attributes.get(ATTR_BATTERY_LEVEL)
810 if battery_level
is None:
812 if battery_level == 100:
813 descriptive_capacity_remaining =
"FULL"
814 elif 75 <= battery_level < 100:
815 descriptive_capacity_remaining =
"HIGH"
816 elif 50 <= battery_level < 75:
817 descriptive_capacity_remaining =
"MEDIUM"
818 elif 25 <= battery_level < 50:
819 descriptive_capacity_remaining =
"LOW"
820 elif 0 <= battery_level < 25:
821 descriptive_capacity_remaining =
"CRITICALLY_LOW"
823 "descriptiveCapacityRemaining": descriptive_capacity_remaining,
824 "capacityRemaining": [{
"rawValue": battery_level,
"unit":
"PERCENTAGE"}],
825 "capacityUntilFull": [
826 {
"rawValue": 100 - battery_level,
"unit":
"PERCENTAGE"}
828 "isCharging": self.
statestate.state == vacuum.STATE_DOCKED,
829 "isPluggedIn": self.
statestate.state == vacuum.STATE_DOCKED,
832 async
def execute(self, command, data, params, challenge):
833 """Execute a dock command."""
835 ERR_FUNCTION_NOT_SUPPORTED,
836 "Controlling charging of a vacuum is not yet supported",
842 """Trait to offer StartStop functionality.
844 https://developers.google.com/actions/smarthome/traits/startstop
847 name = TRAIT_START_STOP
848 commands = [COMMAND_START_STOP, COMMAND_PAUSE_UNPAUSE]
852 """Test if state is supported."""
853 if domain == vacuum.DOMAIN:
857 domain
in COVER_VALVE_DOMAINS
858 and features & COVER_VALVE_STOP_FEATURE[domain]
865 """Return StartStop attributes for a sync request."""
866 domain = self.
statestate.domain
867 if domain == vacuum.DOMAIN:
869 "pausable": self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
870 & VacuumEntityFeature.PAUSE
873 if domain
in COVER_VALVE_DOMAINS:
876 raise NotImplementedError(f
"Unsupported domain {domain}")
879 """Return StartStop query attributes."""
880 domain = self.
statestate.domain
881 state = self.
statestate.state
883 if domain == vacuum.DOMAIN:
885 "isRunning": state == vacuum.STATE_CLEANING,
886 "isPaused": state == vacuum.STATE_PAUSED,
889 if domain
in COVER_VALVE_DOMAINS:
893 COVER_VALVE_STATES[domain][
"closing"],
894 COVER_VALVE_STATES[domain][
"opening"],
898 raise NotImplementedError(f
"Unsupported domain {domain}")
900 async
def execute(self, command, data, params, challenge):
901 """Execute a StartStop command."""
902 domain = self.
statestate.domain
903 if domain == vacuum.DOMAIN:
904 await self.
_execute_vacuum_execute_vacuum(command, data, params, challenge)
906 if domain
in COVER_VALVE_DOMAINS:
911 """Execute a StartStop command."""
912 if command == COMMAND_START_STOP:
914 await self.
hasshass.services.async_call(
915 self.
statestate.domain,
916 vacuum.SERVICE_START,
917 {ATTR_ENTITY_ID: self.
statestate.entity_id},
918 blocking=
not self.
configconfig.should_report_state,
919 context=data.context,
922 await self.
hasshass.services.async_call(
923 self.
statestate.domain,
925 {ATTR_ENTITY_ID: self.
statestate.entity_id},
926 blocking=
not self.
configconfig.should_report_state,
927 context=data.context,
929 elif command == COMMAND_PAUSE_UNPAUSE:
931 await self.
hasshass.services.async_call(
932 self.
statestate.domain,
933 vacuum.SERVICE_PAUSE,
934 {ATTR_ENTITY_ID: self.
statestate.entity_id},
935 blocking=
not self.
configconfig.should_report_state,
936 context=data.context,
939 await self.
hasshass.services.async_call(
940 self.
statestate.domain,
941 vacuum.SERVICE_START,
942 {ATTR_ENTITY_ID: self.
statestate.entity_id},
943 blocking=
not self.
configconfig.should_report_state,
944 context=data.context,
948 """Execute a StartStop command."""
949 domain = self.
statestate.domain
950 if command == COMMAND_START_STOP:
951 if params[
"start"]
is False:
952 if self.
statestate.state
in (
953 COVER_VALVE_STATES[domain][
"closing"],
954 COVER_VALVE_STATES[domain][
"opening"],
955 )
or self.
statestate.attributes.get(ATTR_ASSUMED_STATE):
956 await self.
hasshass.services.async_call(
958 SERVICE_STOP_COVER_VALVE[domain],
959 {ATTR_ENTITY_ID: self.
statestate.entity_id},
960 blocking=
not self.
configconfig.should_report_state,
961 context=data.context,
966 f
"{FRIENDLY_DOMAIN[domain]} is already stopped",
969 await self.
hasshass.services.async_call(
971 SERVICE_TOGGLE_COVER_VALVE[domain],
972 {ATTR_ENTITY_ID: self.
statestate.entity_id},
973 blocking=
not self.
configconfig.should_report_state,
974 context=data.context,
978 ERR_NOT_SUPPORTED, f
"Command {command} is not supported"
984 """Trait for devices (other than thermostats) that support controlling temperature.
986 Control the target temperature of water heaters.
987 Offers a workaround for Temperature sensors by setting queryOnlyTemperatureControl
990 https://developers.google.com/assistant/smarthome/traits/temperaturecontrol
993 name = TRAIT_TEMPERATURE_CONTROL
996 COMMAND_SET_TEMPERATURE,
1001 """Test if state is supported."""
1003 domain == water_heater.DOMAIN
1004 and features & WaterHeaterEntityFeature.TARGET_TEMPERATURE
1006 domain == sensor.DOMAIN
1007 and device_class == sensor.SensorDeviceClass.TEMPERATURE
1011 """Return temperature attributes for a sync request."""
1013 domain = self.
statestate.domain
1014 attrs = self.
statestate.attributes
1015 unit = self.
hasshass.config.units.temperature_unit
1018 if domain == water_heater.DOMAIN:
1020 TemperatureConverter.convert(
1021 float(attrs[water_heater.ATTR_MIN_TEMP]),
1023 UnitOfTemperature.CELSIUS,
1027 TemperatureConverter.convert(
1028 float(attrs[water_heater.ATTR_MAX_TEMP]),
1030 UnitOfTemperature.CELSIUS,
1033 response[
"temperatureRange"] = {
1034 "minThresholdCelsius": min_temp,
1035 "maxThresholdCelsius": max_temp,
1038 response[
"queryOnlyTemperatureControl"] =
True
1039 response[
"temperatureRange"] = {
1040 "minThresholdCelsius": -100,
1041 "maxThresholdCelsius": 100,
1047 """Return temperature states."""
1049 domain = self.
statestate.domain
1050 unit = self.
hasshass.config.units.temperature_unit
1051 if domain == water_heater.DOMAIN:
1052 target_temp = self.
statestate.attributes[water_heater.ATTR_TEMPERATURE]
1053 current_temp = self.
statestate.attributes[water_heater.ATTR_CURRENT_TEMPERATURE]
1054 if target_temp
not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
1055 response[
"temperatureSetpointCelsius"] = round(
1056 TemperatureConverter.convert(
1059 UnitOfTemperature.CELSIUS,
1063 if current_temp
is not None:
1064 response[
"temperatureAmbientCelsius"] = round(
1065 TemperatureConverter.convert(
1066 float(current_temp),
1068 UnitOfTemperature.CELSIUS,
1075 current_temp = self.
statestate.state
1076 if current_temp
not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
1078 TemperatureConverter.convert(
1079 float(current_temp), unit, UnitOfTemperature.CELSIUS
1083 response[
"temperatureSetpointCelsius"] = temp
1084 response[
"temperatureAmbientCelsius"] = temp
1088 async
def execute(self, command, data, params, challenge):
1089 """Execute a temperature point or mode command."""
1091 domain = self.
statestate.domain
1092 unit = self.
hasshass.config.units.temperature_unit
1094 if domain == water_heater.DOMAIN
and command == COMMAND_SET_TEMPERATURE:
1095 min_temp = self.
statestate.attributes[water_heater.ATTR_MIN_TEMP]
1096 max_temp = self.
statestate.attributes[water_heater.ATTR_MAX_TEMP]
1097 temp = TemperatureConverter.convert(
1098 params[
"temperature"], UnitOfTemperature.CELSIUS, unit
1100 if unit == UnitOfTemperature.FAHRENHEIT:
1102 if temp < min_temp
or temp > max_temp:
1104 ERR_VALUE_OUT_OF_RANGE,
1105 f
"Temperature should be between {min_temp} and {max_temp}",
1108 await self.
hasshass.services.async_call(
1109 water_heater.DOMAIN,
1110 water_heater.SERVICE_SET_TEMPERATURE,
1111 {ATTR_ENTITY_ID: self.
statestate.entity_id, ATTR_TEMPERATURE: temp},
1112 blocking=
not self.
configconfig.should_report_state,
1113 context=data.context,
1117 raise SmartHomeError(ERR_NOT_SUPPORTED, f
"Execute is not supported by {domain}")
1122 """Trait to offer handling both temperature point and modes functionality.
1124 https://developers.google.com/actions/smarthome/traits/temperaturesetting
1127 name = TRAIT_TEMPERATURE_SETTING
1129 COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
1130 COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
1131 COMMAND_THERMOSTAT_SET_MODE,
1136 climate.HVACMode.HEAT:
"heat",
1137 climate.HVACMode.COOL:
"cool",
1138 climate.HVACMode.OFF:
"off",
1139 climate.HVACMode.AUTO:
"auto",
1140 climate.HVACMode.HEAT_COOL:
"heatcool",
1141 climate.HVACMode.FAN_ONLY:
"fan-only",
1142 climate.HVACMode.DRY:
"dry",
1144 google_to_hvac = {value: key
for key, value
in hvac_to_google.items()}
1146 preset_to_google = {climate.PRESET_ECO:
"eco"}
1147 google_to_preset = {value: key
for key, value
in preset_to_google.items()}
1151 """Test if state is supported."""
1152 return domain == climate.DOMAIN
1156 """Return supported Google modes."""
1158 attrs = self.
statestate.attributes
1160 for mode
in attrs.get(climate.ATTR_HVAC_MODES)
or []:
1162 if google_mode
and google_mode
not in modes:
1163 modes.append(google_mode)
1165 for preset
in attrs.get(climate.ATTR_PRESET_MODES)
or []:
1167 if google_mode
and google_mode
not in modes:
1168 modes.append(google_mode)
1173 """Return temperature point and modes attributes for a sync request."""
1175 attrs = self.
statestate.attributes
1176 unit = self.
hasshass.config.units.temperature_unit
1180 TemperatureConverter.convert(
1181 float(attrs[climate.ATTR_MIN_TEMP]),
1183 UnitOfTemperature.CELSIUS,
1187 TemperatureConverter.convert(
1188 float(attrs[climate.ATTR_MAX_TEMP]),
1190 UnitOfTemperature.CELSIUS,
1193 response[
"thermostatTemperatureRange"] = {
1194 "minThresholdCelsius": min_temp,
1195 "maxThresholdCelsius": max_temp,
1205 modes.append(
"heat")
1207 if "off" in modes
and any(
1208 mode
in modes
for mode
in (
"heatcool",
"heat",
"cool")
1211 response[
"availableThermostatModes"] = modes
1216 """Return temperature point and modes query attributes."""
1217 response: dict[str, Any] = {}
1218 attrs = self.
statestate.attributes
1219 unit = self.
hasshass.config.units.temperature_unit
1221 operation = self.
statestate.state
1222 preset = attrs.get(climate.ATTR_PRESET_MODE)
1223 supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0)
1228 response[
"thermostatMode"] = self.
hvac_to_googlehvac_to_google.
get(operation,
"none")
1230 current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
1231 if current_temp
is not None:
1232 response[
"thermostatTemperatureAmbient"] = round(
1233 TemperatureConverter.convert(
1234 current_temp, unit, UnitOfTemperature.CELSIUS
1239 current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
1240 if current_humidity
is not None:
1241 response[
"thermostatHumidityAmbient"] = current_humidity
1243 if operation
in (climate.HVACMode.AUTO, climate.HVACMode.HEAT_COOL):
1244 if supported & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
1245 response[
"thermostatTemperatureSetpointHigh"] = round(
1246 TemperatureConverter.convert(
1247 attrs[climate.ATTR_TARGET_TEMP_HIGH],
1249 UnitOfTemperature.CELSIUS,
1253 response[
"thermostatTemperatureSetpointLow"] = round(
1254 TemperatureConverter.convert(
1255 attrs[climate.ATTR_TARGET_TEMP_LOW],
1257 UnitOfTemperature.CELSIUS,
1261 elif (target_temp := attrs.get(ATTR_TEMPERATURE))
is not None:
1262 target_temp = round(
1263 TemperatureConverter.convert(
1264 target_temp, unit, UnitOfTemperature.CELSIUS
1268 response[
"thermostatTemperatureSetpointHigh"] = target_temp
1269 response[
"thermostatTemperatureSetpointLow"] = target_temp
1270 elif (target_temp := attrs.get(ATTR_TEMPERATURE))
is not None:
1271 response[
"thermostatTemperatureSetpoint"] = round(
1272 TemperatureConverter.convert(
1273 target_temp, unit, UnitOfTemperature.CELSIUS
1280 async
def execute(self, command, data, params, challenge):
1281 """Execute a temperature point or mode command."""
1283 unit = self.
hasshass.config.units.temperature_unit
1284 min_temp = self.
statestate.attributes[climate.ATTR_MIN_TEMP]
1285 max_temp = self.
statestate.attributes[climate.ATTR_MAX_TEMP]
1287 if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
1288 temp = TemperatureConverter.convert(
1289 params[
"thermostatTemperatureSetpoint"], UnitOfTemperature.CELSIUS, unit
1291 if unit == UnitOfTemperature.FAHRENHEIT:
1294 if temp < min_temp
or temp > max_temp:
1296 ERR_VALUE_OUT_OF_RANGE,
1297 f
"Temperature should be between {min_temp} and {max_temp}",
1300 await self.
hasshass.services.async_call(
1302 climate.SERVICE_SET_TEMPERATURE,
1303 {ATTR_ENTITY_ID: self.
statestate.entity_id, ATTR_TEMPERATURE: temp},
1304 blocking=
not self.
configconfig.should_report_state,
1305 context=data.context,
1308 elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
1309 temp_high = TemperatureConverter.convert(
1310 params[
"thermostatTemperatureSetpointHigh"],
1311 UnitOfTemperature.CELSIUS,
1314 if unit == UnitOfTemperature.FAHRENHEIT:
1315 temp_high = round(temp_high)
1317 if temp_high < min_temp
or temp_high > max_temp:
1319 ERR_VALUE_OUT_OF_RANGE,
1321 "Upper bound for temperature range should be between "
1322 f
"{min_temp} and {max_temp}"
1326 temp_low = TemperatureConverter.convert(
1327 params[
"thermostatTemperatureSetpointLow"],
1328 UnitOfTemperature.CELSIUS,
1331 if unit == UnitOfTemperature.FAHRENHEIT:
1332 temp_low = round(temp_low)
1334 if temp_low < min_temp
or temp_low > max_temp:
1336 ERR_VALUE_OUT_OF_RANGE,
1338 "Lower bound for temperature range should be between "
1339 f
"{min_temp} and {max_temp}"
1343 supported = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES)
1344 svc_data = {ATTR_ENTITY_ID: self.
statestate.entity_id}
1346 if supported & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
1347 svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
1348 svc_data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
1350 svc_data[ATTR_TEMPERATURE] = (temp_high + temp_low) / 2
1352 await self.
hasshass.services.async_call(
1354 climate.SERVICE_SET_TEMPERATURE,
1356 blocking=
not self.
configconfig.should_report_state,
1357 context=data.context,
1360 elif command == COMMAND_THERMOSTAT_SET_MODE:
1361 target_mode = params[
"thermostatMode"]
1362 supported = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES)
1364 if target_mode ==
"on":
1365 await self.
hasshass.services.async_call(
1368 {ATTR_ENTITY_ID: self.
statestate.entity_id},
1369 blocking=
not self.
configconfig.should_report_state,
1370 context=data.context,
1374 if target_mode ==
"off":
1375 await self.
hasshass.services.async_call(
1378 {ATTR_ENTITY_ID: self.
statestate.entity_id},
1379 blocking=
not self.
configconfig.should_report_state,
1380 context=data.context,
1385 await self.
hasshass.services.async_call(
1387 climate.SERVICE_SET_PRESET_MODE,
1389 climate.ATTR_PRESET_MODE: self.
google_to_presetgoogle_to_preset[target_mode],
1390 ATTR_ENTITY_ID: self.
statestate.entity_id,
1392 blocking=
not self.
configconfig.should_report_state,
1393 context=data.context,
1397 await self.
hasshass.services.async_call(
1399 climate.SERVICE_SET_HVAC_MODE,
1401 ATTR_ENTITY_ID: self.
statestate.entity_id,
1402 climate.ATTR_HVAC_MODE: self.
google_to_hvacgoogle_to_hvac[target_mode],
1404 blocking=
not self.
configconfig.should_report_state,
1405 context=data.context,
1411 """Trait to offer humidity setting functionality.
1413 https://developers.google.com/actions/smarthome/traits/humiditysetting
1416 name = TRAIT_HUMIDITY_SETTING
1417 commands = [COMMAND_SET_HUMIDITY]
1421 """Test if state is supported."""
1422 if domain == humidifier.DOMAIN:
1426 domain == sensor.DOMAIN
1427 and device_class == sensor.SensorDeviceClass.HUMIDITY
1431 """Return humidity attributes for a sync request."""
1432 response: dict[str, Any] = {}
1433 attrs = self.
statestate.attributes
1434 domain = self.
statestate.domain
1436 if domain == sensor.DOMAIN:
1437 device_class = attrs.get(ATTR_DEVICE_CLASS)
1438 if device_class == sensor.SensorDeviceClass.HUMIDITY:
1439 response[
"queryOnlyHumiditySetting"] =
True
1441 elif domain == humidifier.DOMAIN:
1442 response[
"humiditySetpointRange"] = {
1443 "minPercent": round(
1444 float(self.
statestate.attributes[humidifier.ATTR_MIN_HUMIDITY])
1446 "maxPercent": round(
1447 float(self.
statestate.attributes[humidifier.ATTR_MAX_HUMIDITY])
1454 """Return humidity query attributes."""
1456 attrs = self.
statestate.attributes
1457 domain = self.
statestate.domain
1459 if domain == sensor.DOMAIN:
1460 device_class = attrs.get(ATTR_DEVICE_CLASS)
1461 if device_class == sensor.SensorDeviceClass.HUMIDITY:
1462 humidity_state = self.
statestate.state
1463 if humidity_state
not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
1464 response[
"humidityAmbientPercent"] = round(
float(humidity_state))
1466 elif domain == humidifier.DOMAIN:
1467 target_humidity: int |
None = attrs.get(humidifier.ATTR_HUMIDITY)
1468 if target_humidity
is not None:
1469 response[
"humiditySetpointPercent"] = target_humidity
1470 current_humidity: int |
None = attrs.get(humidifier.ATTR_CURRENT_HUMIDITY)
1471 if current_humidity
is not None:
1472 response[
"humidityAmbientPercent"] = current_humidity
1476 async
def execute(self, command, data, params, challenge):
1477 """Execute a humidity command."""
1478 if self.
statestate.domain == sensor.DOMAIN:
1480 ERR_NOT_SUPPORTED,
"Execute is not supported by sensor"
1483 if command == COMMAND_SET_HUMIDITY:
1484 await self.
hasshass.services.async_call(
1486 humidifier.SERVICE_SET_HUMIDITY,
1488 ATTR_ENTITY_ID: self.
statestate.entity_id,
1489 humidifier.ATTR_HUMIDITY: params[
"humidity"],
1491 blocking=
not self.
configconfig.should_report_state,
1492 context=data.context,
1498 """Trait to lock or unlock a lock.
1500 https://developers.google.com/actions/smarthome/traits/lockunlock
1503 name = TRAIT_LOCK_UNLOCK
1504 commands = [COMMAND_LOCK_UNLOCK]
1508 """Test if state is supported."""
1509 return domain == lock.DOMAIN
1513 """Return if the trait might ask for 2FA."""
1517 """Return LockUnlock attributes for a sync request."""
1521 """Return LockUnlock query attributes."""
1522 if self.
statestate.state == LockState.JAMMED:
1523 return {
"isJammed":
True}
1526 return {
"isLocked": self.
statestate.state
in (LockState.UNLOCKING, LockState.LOCKED)}
1528 async
def execute(self, command, data, params, challenge):
1529 """Execute an LockUnlock command."""
1531 service = lock.SERVICE_LOCK
1534 service = lock.SERVICE_UNLOCK
1536 await self.
hasshass.services.async_call(
1539 {ATTR_ENTITY_ID: self.
statestate.entity_id},
1540 blocking=
not self.
configconfig.should_report_state,
1541 context=data.context,
1547 """Trait to Arm or Disarm a Security System.
1549 https://developers.google.com/actions/smarthome/traits/armdisarm
1552 name = TRAIT_ARM_DISARM
1553 commands = [COMMAND_ARM_DISARM]
1555 state_to_service = {
1556 AlarmControlPanelState.ARMED_HOME: SERVICE_ALARM_ARM_HOME,
1557 AlarmControlPanelState.ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT,
1558 AlarmControlPanelState.ARMED_AWAY: SERVICE_ALARM_ARM_AWAY,
1559 AlarmControlPanelState.ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS,
1560 AlarmControlPanelState.TRIGGERED: SERVICE_ALARM_TRIGGER,
1563 state_to_support = {
1564 AlarmControlPanelState.ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
1565 AlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
1566 AlarmControlPanelState.ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
1567 AlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
1568 AlarmControlPanelState.TRIGGERED: AlarmControlPanelEntityFeature.TRIGGER,
1570 """The list of states to support in increasing security state."""
1574 """Test if state is supported."""
1575 return domain == alarm_control_panel.DOMAIN
1579 """Return if the trait might ask for 2FA."""
1583 """Return supported states."""
1584 features = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
1587 for state, required_feature
in self.
state_to_supportstate_to_support.items()
1588 if features & required_feature != 0
1594 if AlarmControlPanelState.TRIGGERED
in states:
1595 states.remove(AlarmControlPanelState.TRIGGERED)
1603 """Return ArmDisarm attributes for a sync request."""
1609 level_synonym = [state.replace(
"_",
" ")]
1610 if state != AlarmControlPanelState.TRIGGERED:
1611 level_synonym.append(state.split(
"_")[1])
1614 "level_name": state,
1615 "level_values": [{
"level_synonym": level_synonym,
"lang":
"en"}],
1617 levels.append(level)
1619 response[
"availableArmLevels"] = {
"levels": levels,
"ordered":
True}
1623 """Return ArmDisarm query attributes."""
1624 armed_state = self.
statestate.attributes.get(
"next_state", self.
statestate.state)
1627 return {
"isArmed":
True,
"currentArmLevel": armed_state}
1633 async
def execute(self, command, data, params, challenge):
1634 """Execute an ArmDisarm command."""
1635 if params[
"arm"]
and not params.get(
"cancel"):
1638 if not (arm_level := params.get(
"armLevel")):
1641 if self.
statestate.state == arm_level:
1642 raise SmartHomeError(ERR_ALREADY_ARMED,
"System is already armed")
1643 if self.
statestate.attributes[
"code_arm_required"]:
1650 and params.get(
"cancel")
1651 and self.
statestate.state == AlarmControlPanelState.PENDING
1653 service = SERVICE_ALARM_DISARM
1655 if self.
statestate.state == AlarmControlPanelState.DISARMED:
1656 raise SmartHomeError(ERR_ALREADY_DISARMED,
"System is already disarmed")
1658 service = SERVICE_ALARM_DISARM
1660 await self.
hasshass.services.async_call(
1661 alarm_control_panel.DOMAIN,
1664 ATTR_ENTITY_ID: self.
statestate.entity_id,
1665 ATTR_CODE: data.config.secure_devices_pin,
1667 blocking=
not self.
configconfig.should_report_state,
1668 context=data.context,
1673 """Return a fan speed synonyms for a speed name."""
1674 speed_synonyms = FAN_SPEEDS.get(speed_name, [f
"{speed_name}"])
1676 "speed_name": speed_name,
1679 "speed_synonym": speed_synonyms,
1688 """Trait to control speed of Fan.
1690 https://developers.google.com/actions/smarthome/traits/fanspeed
1693 name = TRAIT_FAN_SPEED
1694 commands = [COMMAND_SET_FAN_SPEED, COMMAND_REVERSE]
1697 """Initialize a trait for a state."""
1698 super().
__init__(hass, state, config)
1699 if state.domain == fan.DOMAIN:
1701 FAN_SPEED_MAX_SPEED_COUNT,
1703 100 / (self.
statestate.attributes.get(fan.ATTR_PERCENTAGE_STEP)
or 1.0)
1707 f
"{speed}/{speed_count}" for speed
in range(1, speed_count + 1)
1712 """Test if state is supported."""
1713 if domain == fan.DOMAIN:
1714 return features & FanEntityFeature.SET_SPEED
1715 if domain == climate.DOMAIN:
1716 return features & ClimateEntityFeature.FAN_MODE
1720 """Return speed point and modes attributes for a sync request."""
1721 domain = self.
statestate.domain
1723 result: dict[str, Any] = {}
1725 if domain == fan.DOMAIN:
1727 self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
1728 & FanEntityFeature.DIRECTION
1733 "reversible": reversible,
1734 "supportsFanSpeedPercent":
True,
1741 "availableFanSpeeds": {
1750 elif domain == climate.DOMAIN:
1751 modes = self.
statestate.attributes.get(climate.ATTR_FAN_MODES)
or []
1755 "speed_values": [{
"speed_synonym": [mode],
"lang":
"en"}],
1757 speeds.append(speed)
1761 "reversible":
False,
1762 "availableFanSpeeds": {
"speeds": speeds,
"ordered":
True},
1769 """Return speed point and modes query attributes."""
1771 attrs = self.
statestate.attributes
1772 domain = self.
statestate.domain
1774 if domain == climate.DOMAIN:
1775 speed = attrs.get(climate.ATTR_FAN_MODE)
or "off"
1776 response[
"currentFanSpeedSetting"] = speed
1778 if domain == fan.DOMAIN:
1779 percent = attrs.get(fan.ATTR_PERCENTAGE)
or 0
1780 response[
"currentFanSpeedPercent"] = percent
1781 response[
"currentFanSpeedSetting"] = percentage_to_ordered_list_item(
1788 """Execute an SetFanSpeed command."""
1789 domain = self.
statestate.domain
1790 if domain == climate.DOMAIN:
1791 await self.
hasshass.services.async_call(
1793 climate.SERVICE_SET_FAN_MODE,
1795 ATTR_ENTITY_ID: self.
statestate.entity_id,
1796 climate.ATTR_FAN_MODE: params[
"fanSpeed"],
1798 blocking=
not self.
configconfig.should_report_state,
1799 context=data.context,
1802 if domain == fan.DOMAIN:
1803 if fan_speed := params.get(
"fanSpeed"):
1804 fan_speed_percent = ordered_list_item_to_percentage(
1808 fan_speed_percent = params.get(
"fanSpeedPercent")
1810 await self.
hasshass.services.async_call(
1812 fan.SERVICE_SET_PERCENTAGE,
1814 ATTR_ENTITY_ID: self.
statestate.entity_id,
1815 fan.ATTR_PERCENTAGE: fan_speed_percent,
1817 blocking=
not self.
configconfig.should_report_state,
1818 context=data.context,
1822 """Execute a Reverse command."""
1823 if self.
statestate.domain == fan.DOMAIN:
1824 if self.
statestate.attributes.get(fan.ATTR_DIRECTION) == fan.DIRECTION_FORWARD:
1825 direction = fan.DIRECTION_REVERSE
1827 direction = fan.DIRECTION_FORWARD
1829 await self.
hasshass.services.async_call(
1831 fan.SERVICE_SET_DIRECTION,
1832 {ATTR_ENTITY_ID: self.
statestate.entity_id, fan.ATTR_DIRECTION: direction},
1833 blocking=
not self.
configconfig.should_report_state,
1834 context=data.context,
1837 async
def execute(self, command, data, params, challenge):
1838 """Execute a smart home command."""
1839 if command == COMMAND_SET_FAN_SPEED:
1841 elif command == COMMAND_REVERSE:
1847 """Trait to set modes.
1849 https://developers.google.com/actions/smarthome/traits/modes
1853 commands = [COMMAND_SET_MODES]
1856 "preset mode": [
"preset mode",
"mode",
"preset"],
1857 "sound mode": [
"sound mode",
"effects"],
1858 "option": [
"option",
"setting",
"mode",
"value"],
1863 """Test if state is supported."""
1864 if domain == fan.DOMAIN
and features & FanEntityFeature.PRESET_MODE:
1867 if domain == input_select.DOMAIN:
1870 if domain == select.DOMAIN:
1873 if domain == humidifier.DOMAIN
and features & HumidifierEntityFeature.MODES:
1876 if domain == light.DOMAIN
and features & LightEntityFeature.EFFECT:
1880 domain == water_heater.DOMAIN
1881 and features & WaterHeaterEntityFeature.OPERATION_MODE
1885 if domain != media_player.DOMAIN:
1888 return features & MediaPlayerEntityFeature.SELECT_SOUND_MODE
1891 """Generate a list of modes."""
1895 {
"name_synonym": self.
SYNONYMSSYNONYMS.
get(name, [name]),
"lang":
"en"}
1900 for setting
in settings:
1901 mode[
"settings"].append(
1903 "setting_name": setting,
1906 "setting_synonym": self.
SYNONYMSSYNONYMS.
get(setting, [setting]),
1915 """Return mode attributes for a sync request."""
1918 for domain, attr, name
in (
1919 (fan.DOMAIN, fan.ATTR_PRESET_MODES,
"preset mode"),
1920 (media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST,
"sound mode"),
1921 (input_select.DOMAIN, input_select.ATTR_OPTIONS,
"option"),
1922 (select.DOMAIN, select.ATTR_OPTIONS,
"option"),
1923 (humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES,
"mode"),
1924 (light.DOMAIN, light.ATTR_EFFECT_LIST,
"effect"),
1925 (water_heater.DOMAIN, water_heater.ATTR_OPERATION_LIST,
"operation mode"),
1927 if self.
statestate.domain != domain:
1930 if (items := self.
statestate.attributes.get(attr))
is not None:
1931 modes.append(self.
_generate_generate(name, items))
1936 return {
"availableModes": modes}
1939 """Return current modes."""
1940 attrs = self.
statestate.attributes
1941 response: dict[str, Any] = {}
1944 if self.
statestate.domain == fan.DOMAIN:
1945 if fan.ATTR_PRESET_MODES
in attrs:
1946 mode_settings[
"preset mode"] = attrs.get(fan.ATTR_PRESET_MODE)
1947 elif self.
statestate.domain == media_player.DOMAIN:
1948 if media_player.ATTR_SOUND_MODE_LIST
in attrs:
1949 mode_settings[
"sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE)
1950 elif self.
statestate.domain
in (input_select.DOMAIN, select.DOMAIN):
1951 mode_settings[
"option"] = self.
statestate.state
1952 elif self.
statestate.domain == humidifier.DOMAIN:
1953 if ATTR_MODE
in attrs:
1954 mode_settings[
"mode"] = attrs.get(ATTR_MODE)
1955 elif self.
statestate.domain == water_heater.DOMAIN:
1956 if water_heater.ATTR_OPERATION_MODE
in attrs:
1957 mode_settings[
"operation mode"] = attrs.get(
1958 water_heater.ATTR_OPERATION_MODE
1960 elif self.
statestate.domain == light.DOMAIN
and (
1961 effect := attrs.get(light.ATTR_EFFECT)
1963 mode_settings[
"effect"] = effect
1966 response[
"on"] = self.
statestate.state
not in (STATE_OFF, STATE_UNKNOWN)
1967 response[
"currentModeSettings"] = mode_settings
1971 async
def execute(self, command, data, params, challenge):
1972 """Execute a SetModes command."""
1973 settings = params.get(
"updateModeSettings")
1975 if self.
statestate.domain == fan.DOMAIN:
1976 preset_mode = settings[
"preset mode"]
1977 await self.
hasshass.services.async_call(
1979 fan.SERVICE_SET_PRESET_MODE,
1981 ATTR_ENTITY_ID: self.
statestate.entity_id,
1982 fan.ATTR_PRESET_MODE: preset_mode,
1984 blocking=
not self.
configconfig.should_report_state,
1985 context=data.context,
1989 if self.
statestate.domain == input_select.DOMAIN:
1990 option = settings[
"option"]
1991 await self.
hasshass.services.async_call(
1992 input_select.DOMAIN,
1993 input_select.SERVICE_SELECT_OPTION,
1995 ATTR_ENTITY_ID: self.
statestate.entity_id,
1996 input_select.ATTR_OPTION: option,
1998 blocking=
not self.
configconfig.should_report_state,
1999 context=data.context,
2003 if self.
statestate.domain == select.DOMAIN:
2004 option = settings[
"option"]
2005 await self.
hasshass.services.async_call(
2007 select.SERVICE_SELECT_OPTION,
2009 ATTR_ENTITY_ID: self.
statestate.entity_id,
2010 select.ATTR_OPTION: option,
2012 blocking=
not self.
configconfig.should_report_state,
2013 context=data.context,
2017 if self.
statestate.domain == humidifier.DOMAIN:
2018 requested_mode = settings[
"mode"]
2019 await self.
hasshass.services.async_call(
2021 humidifier.SERVICE_SET_MODE,
2023 ATTR_MODE: requested_mode,
2024 ATTR_ENTITY_ID: self.
statestate.entity_id,
2026 blocking=
not self.
configconfig.should_report_state,
2027 context=data.context,
2031 if self.
statestate.domain == water_heater.DOMAIN:
2032 requested_mode = settings[
"operation mode"]
2033 await self.
hasshass.services.async_call(
2034 water_heater.DOMAIN,
2035 water_heater.SERVICE_SET_OPERATION_MODE,
2037 water_heater.ATTR_OPERATION_MODE: requested_mode,
2038 ATTR_ENTITY_ID: self.
statestate.entity_id,
2040 blocking=
not self.
configconfig.should_report_state,
2041 context=data.context,
2045 if self.
statestate.domain == light.DOMAIN:
2046 requested_effect = settings[
"effect"]
2047 await self.
hasshass.services.async_call(
2051 ATTR_ENTITY_ID: self.
statestate.entity_id,
2052 light.ATTR_EFFECT: requested_effect,
2054 blocking=
not self.
configconfig.should_report_state,
2055 context=data.context,
2059 if self.
statestate.domain == media_player.DOMAIN
and (
2060 sound_mode := settings.get(
"sound mode")
2062 await self.
hasshass.services.async_call(
2063 media_player.DOMAIN,
2064 media_player.SERVICE_SELECT_SOUND_MODE,
2066 ATTR_ENTITY_ID: self.
statestate.entity_id,
2067 media_player.ATTR_SOUND_MODE: sound_mode,
2069 blocking=
not self.
configconfig.should_report_state,
2070 context=data.context,
2074 "Received an Options command for unrecognised domain %s",
2075 self.
statestate.domain,
2082 """Trait to set modes.
2084 https://developers.google.com/assistant/smarthome/traits/inputselector
2087 name = TRAIT_INPUT_SELECTOR
2088 commands = [COMMAND_SET_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT]
2090 SYNONYMS: dict[str, list[str]] = {}
2094 """Test if state is supported."""
2095 if domain == media_player.DOMAIN
and (
2096 features & MediaPlayerEntityFeature.SELECT_SOURCE
2103 """Return mode attributes for a sync request."""
2104 attrs = self.
statestate.attributes
2105 sourcelist: list[str] = attrs.get(media_player.ATTR_INPUT_SOURCE_LIST)
or []
2107 {
"key": source,
"names": [{
"name_synonym": [source],
"lang":
"en"}]}
2108 for source
in sourcelist
2111 return {
"availableInputs": inputs,
"orderedInputs":
True}
2114 """Return current modes."""
2115 attrs = self.
statestate.attributes
2116 return {
"currentInput": attrs.get(media_player.ATTR_INPUT_SOURCE,
"")}
2118 async
def execute(self, command, data, params, challenge):
2119 """Execute an SetInputSource command."""
2120 sources = self.
statestate.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST)
or []
2121 source = self.
statestate.attributes.get(media_player.ATTR_INPUT_SOURCE)
2123 if command == COMMAND_SET_INPUT:
2124 requested_source = params.get(
"newInput")
2125 elif command == COMMAND_NEXT_INPUT:
2127 elif command == COMMAND_PREVIOUS_INPUT:
2132 if requested_source
not in sources:
2135 await self.
hasshass.services.async_call(
2136 media_player.DOMAIN,
2137 media_player.SERVICE_SELECT_SOURCE,
2139 ATTR_ENTITY_ID: self.
statestate.entity_id,
2140 media_player.ATTR_INPUT_SOURCE: requested_source,
2142 blocking=
not self.
configconfig.should_report_state,
2143 context=data.context,
2149 """Trait to open and close a cover.
2151 https://developers.google.com/actions/smarthome/traits/openclose
2156 cover.CoverDeviceClass.DOOR,
2157 cover.CoverDeviceClass.GARAGE,
2158 cover.CoverDeviceClass.GATE,
2161 name = TRAIT_OPEN_CLOSE
2162 commands = [COMMAND_OPEN_CLOSE, COMMAND_OPEN_CLOSE_RELATIVE]
2166 """Test if state is supported."""
2167 if domain
in COVER_VALVE_DOMAINS:
2170 return domain == binary_sensor.DOMAIN
and device_class
in (
2171 binary_sensor.BinarySensorDeviceClass.DOOR,
2172 binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
2173 binary_sensor.BinarySensorDeviceClass.LOCK,
2174 binary_sensor.BinarySensorDeviceClass.OPENING,
2175 binary_sensor.BinarySensorDeviceClass.WINDOW,
2180 """Return if the trait might ask for 2FA."""
2181 return domain == cover.DOMAIN
and device_class
in OpenCloseTrait.COVER_2FA
2184 """Return opening direction."""
2186 features = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2188 if self.
statestate.domain == binary_sensor.DOMAIN:
2189 response[
"queryOnlyOpenClose"] =
True
2190 response[
"discreteOnlyOpenClose"] =
True
2192 self.
statestate.domain == cover.DOMAIN
2193 and features & CoverEntityFeature.SET_POSITION == 0
2195 response[
"discreteOnlyOpenClose"] =
True
2198 features & CoverEntityFeature.OPEN == 0
2199 and features & CoverEntityFeature.CLOSE == 0
2201 response[
"queryOnlyOpenClose"] =
True
2203 self.
statestate.domain == valve.DOMAIN
2204 and features & ValveEntityFeature.SET_POSITION == 0
2206 response[
"discreteOnlyOpenClose"] =
True
2209 features & ValveEntityFeature.OPEN == 0
2210 and features & ValveEntityFeature.CLOSE == 0
2212 response[
"queryOnlyOpenClose"] =
True
2214 if self.
statestate.attributes.get(ATTR_ASSUMED_STATE):
2215 response[
"commandOnlyOpenClose"] =
True
2220 """Return state query attributes."""
2221 domain = self.
statestate.domain
2222 response: dict[str, Any] = {}
2228 if self.
statestate.attributes.get(ATTR_ASSUMED_STATE):
2231 if domain
in COVER_VALVE_DOMAINS:
2232 if self.
statestate.state == STATE_UNKNOWN:
2234 ERR_NOT_SUPPORTED,
"Querying state is not supported"
2237 position = self.
statestate.attributes.get(COVER_VALVE_CURRENT_POSITION[domain])
2239 if position
is not None:
2240 response[
"openPercent"] = position
2241 elif self.
statestate.state != COVER_VALVE_STATES[domain][
"closed"]:
2242 response[
"openPercent"] = 100
2244 response[
"openPercent"] = 0
2246 elif domain == binary_sensor.DOMAIN:
2247 if self.
statestate.state == STATE_ON:
2248 response[
"openPercent"] = 100
2250 response[
"openPercent"] = 0
2254 async
def execute(self, command, data, params, challenge):
2255 """Execute an Open, close, Set position command."""
2256 domain = self.
statestate.domain
2257 features = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2259 if domain
in COVER_VALVE_DOMAINS:
2260 svc_params = {ATTR_ENTITY_ID: self.
statestate.entity_id}
2261 should_verify =
False
2262 if command == COMMAND_OPEN_CLOSE_RELATIVE:
2263 position = self.
statestate.attributes.get(
2264 COVER_VALVE_CURRENT_POSITION[domain]
2266 if position
is None:
2269 "Current position not know for relative command",
2271 position =
max(0,
min(100, position + params[
"openRelativePercent"]))
2273 position = params[
"openPercent"]
2276 service = SERVICE_CLOSE_COVER_VALVE[domain]
2277 should_verify =
False
2278 elif position == 100:
2279 service = SERVICE_OPEN_COVER_VALVE[domain]
2280 should_verify =
True
2281 elif features & COVER_VALVE_SET_POSITION_FEATURE[domain]:
2282 service = SERVICE_SET_POSITION_COVER_VALVE[domain]
2284 should_verify =
True
2285 svc_params[COVER_VALVE_POSITION[domain]] = position
2288 ERR_NOT_SUPPORTED,
"No support for partial open close"
2293 and self.
statestate.attributes.get(ATTR_DEVICE_CLASS)
2294 in OpenCloseTrait.COVER_2FA
2298 await self.
hasshass.services.async_call(
2302 blocking=
not self.
configconfig.should_report_state,
2303 context=data.context,
2309 """Trait to control volume of a device.
2311 https://developers.google.com/actions/smarthome/traits/volume
2315 commands = [COMMAND_SET_VOLUME, COMMAND_VOLUME_RELATIVE, COMMAND_MUTE]
2319 """Test if trait is supported."""
2320 if domain == media_player.DOMAIN:
2322 MediaPlayerEntityFeature.VOLUME_SET
2323 | MediaPlayerEntityFeature.VOLUME_STEP
2329 """Return volume attributes for a sync request."""
2330 features = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2332 "volumeCanMuteAndUnmute": bool(
2333 features & MediaPlayerEntityFeature.VOLUME_MUTE
2335 "commandOnlyVolume": self.
statestate.attributes.get(ATTR_ASSUMED_STATE,
False),
2338 "volumeMaxLevel": 100,
2342 "levelStepSize": 10,
2346 """Return volume query attributes."""
2349 level = self.
statestate.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
2350 if level
is not None:
2352 response[
"currentVolume"] = round(level * 100)
2354 muted = self.
statestate.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED)
2355 if muted
is not None:
2356 response[
"isMuted"] = bool(muted)
2361 await self.
hasshass.services.async_call(
2362 media_player.DOMAIN,
2363 media_player.SERVICE_VOLUME_SET,
2365 ATTR_ENTITY_ID: self.
statestate.entity_id,
2366 media_player.ATTR_MEDIA_VOLUME_LEVEL: level,
2368 blocking=
not self.
configconfig.should_report_state,
2369 context=data.context,
2373 level =
max(0,
min(100, params[
"volumeLevel"]))
2376 self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2377 & MediaPlayerEntityFeature.VOLUME_SET
2384 relative = params[
"relativeSteps"]
2385 features = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2387 if features & MediaPlayerEntityFeature.VOLUME_SET:
2388 current = self.
statestate.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
2389 target =
max(0.0,
min(1.0, current + relative / 100))
2393 elif features & MediaPlayerEntityFeature.VOLUME_STEP:
2394 svc = media_player.SERVICE_VOLUME_UP
2396 svc = media_player.SERVICE_VOLUME_DOWN
2397 relative = -relative
2399 for _
in range(relative):
2400 await self.
hasshass.services.async_call(
2401 media_player.DOMAIN,
2403 {ATTR_ENTITY_ID: self.
statestate.entity_id},
2404 blocking=
not self.
configconfig.should_report_state,
2405 context=data.context,
2411 mute = params[
"mute"]
2414 self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2415 & MediaPlayerEntityFeature.VOLUME_MUTE
2419 await self.
hasshass.services.async_call(
2420 media_player.DOMAIN,
2421 media_player.SERVICE_VOLUME_MUTE,
2423 ATTR_ENTITY_ID: self.
statestate.entity_id,
2424 media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
2426 blocking=
not self.
configconfig.should_report_state,
2427 context=data.context,
2430 async
def execute(self, command, data, params, challenge):
2431 """Execute a volume command."""
2432 if command == COMMAND_SET_VOLUME:
2434 elif command == COMMAND_VOLUME_RELATIVE:
2436 elif command == COMMAND_MUTE:
2443 """Verify a pin challenge."""
2444 if not data.config.should_2fa(state):
2446 if not data.config.secure_devices_pin:
2447 raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP,
"Challenge is not set up")
2452 if challenge.get(
"pin") != data.config.secure_devices_pin:
2456 MEDIA_COMMAND_SUPPORT_MAPPING = {
2457 COMMAND_MEDIA_NEXT: MediaPlayerEntityFeature.NEXT_TRACK,
2458 COMMAND_MEDIA_PAUSE: MediaPlayerEntityFeature.PAUSE,
2459 COMMAND_MEDIA_PREVIOUS: MediaPlayerEntityFeature.PREVIOUS_TRACK,
2460 COMMAND_MEDIA_RESUME: MediaPlayerEntityFeature.PLAY,
2461 COMMAND_MEDIA_SEEK_RELATIVE: MediaPlayerEntityFeature.SEEK,
2462 COMMAND_MEDIA_SEEK_TO_POSITION: MediaPlayerEntityFeature.SEEK,
2463 COMMAND_MEDIA_SHUFFLE: MediaPlayerEntityFeature.SHUFFLE_SET,
2464 COMMAND_MEDIA_STOP: MediaPlayerEntityFeature.STOP,
2467 MEDIA_COMMAND_ATTRIBUTES = {
2468 COMMAND_MEDIA_NEXT:
"NEXT",
2469 COMMAND_MEDIA_PAUSE:
"PAUSE",
2470 COMMAND_MEDIA_PREVIOUS:
"PREVIOUS",
2471 COMMAND_MEDIA_RESUME:
"RESUME",
2472 COMMAND_MEDIA_SEEK_RELATIVE:
"SEEK_RELATIVE",
2473 COMMAND_MEDIA_SEEK_TO_POSITION:
"SEEK_TO_POSITION",
2474 COMMAND_MEDIA_SHUFFLE:
"SHUFFLE",
2475 COMMAND_MEDIA_STOP:
"STOP",
2481 """Trait to control media playback.
2483 https://developers.google.com/actions/smarthome/traits/transportcontrol
2486 name = TRAIT_TRANSPORT_CONTROL
2489 COMMAND_MEDIA_PAUSE,
2490 COMMAND_MEDIA_PREVIOUS,
2491 COMMAND_MEDIA_RESUME,
2492 COMMAND_MEDIA_SEEK_RELATIVE,
2493 COMMAND_MEDIA_SEEK_TO_POSITION,
2494 COMMAND_MEDIA_SHUFFLE,
2500 """Test if state is supported."""
2501 if domain == media_player.DOMAIN:
2502 for feature
in MEDIA_COMMAND_SUPPORT_MAPPING.values():
2503 if features & feature:
2509 """Return opening direction."""
2512 if self.
statestate.domain == media_player.DOMAIN:
2513 features = self.
statestate.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2516 for command, feature
in MEDIA_COMMAND_SUPPORT_MAPPING.items():
2517 if features & feature:
2518 support.append(MEDIA_COMMAND_ATTRIBUTES[command])
2519 response[
"transportControlSupportedCommands"] = support
2524 """Return the attributes of this trait for this entity."""
2527 async
def execute(self, command, data, params, challenge):
2528 """Execute a media command."""
2529 service_attrs = {ATTR_ENTITY_ID: self.
statestate.entity_id}
2531 if command == COMMAND_MEDIA_SEEK_RELATIVE:
2532 service = media_player.SERVICE_MEDIA_SEEK
2534 rel_position = params[
"relativePositionMs"] / 1000
2536 if self.
statestate.state == STATE_PLAYING:
2537 now = dt_util.utcnow()
2538 upd_at = self.
statestate.attributes.get(
2539 media_player.ATTR_MEDIA_POSITION_UPDATED_AT, now
2541 seconds_since = (now - upd_at).total_seconds()
2542 position = self.
statestate.attributes.get(media_player.ATTR_MEDIA_POSITION, 0)
2543 max_position = self.
statestate.attributes.get(
2544 media_player.ATTR_MEDIA_DURATION, 0
2546 service_attrs[media_player.ATTR_MEDIA_SEEK_POSITION] =
min(
2547 max(position + seconds_since + rel_position, 0), max_position
2549 elif command == COMMAND_MEDIA_SEEK_TO_POSITION:
2550 service = media_player.SERVICE_MEDIA_SEEK
2552 max_position = self.
statestate.attributes.get(
2553 media_player.ATTR_MEDIA_DURATION, 0
2555 service_attrs[media_player.ATTR_MEDIA_SEEK_POSITION] =
min(
2556 max(params[
"absPositionMs"] / 1000, 0), max_position
2558 elif command == COMMAND_MEDIA_NEXT:
2559 service = media_player.SERVICE_MEDIA_NEXT_TRACK
2560 elif command == COMMAND_MEDIA_PAUSE:
2561 service = media_player.SERVICE_MEDIA_PAUSE
2562 elif command == COMMAND_MEDIA_PREVIOUS:
2563 service = media_player.SERVICE_MEDIA_PREVIOUS_TRACK
2564 elif command == COMMAND_MEDIA_RESUME:
2565 service = media_player.SERVICE_MEDIA_PLAY
2566 elif command == COMMAND_MEDIA_SHUFFLE:
2567 service = media_player.SERVICE_SHUFFLE_SET
2570 service_attrs[media_player.ATTR_MEDIA_SHUFFLE] =
True
2571 elif command == COMMAND_MEDIA_STOP:
2572 service = media_player.SERVICE_MEDIA_STOP
2576 await self.
hasshass.services.async_call(
2577 media_player.DOMAIN,
2580 blocking=
not self.
configconfig.should_report_state,
2581 context=data.context,
2587 """Trait to get media playback state.
2589 https://developers.google.com/actions/smarthome/traits/mediastate
2592 name = TRAIT_MEDIA_STATE
2593 commands: list[str] = []
2596 STATE_OFF:
"INACTIVE",
2597 STATE_IDLE:
"STANDBY",
2598 STATE_PLAYING:
"ACTIVE",
2599 STATE_ON:
"STANDBY",
2600 STATE_PAUSED:
"STANDBY",
2601 STATE_STANDBY:
"STANDBY",
2602 STATE_UNAVAILABLE:
"INACTIVE",
2603 STATE_UNKNOWN:
"INACTIVE",
2607 STATE_OFF:
"STOPPED",
2608 STATE_IDLE:
"STOPPED",
2609 STATE_PLAYING:
"PLAYING",
2610 STATE_ON:
"STOPPED",
2611 STATE_PAUSED:
"PAUSED",
2612 STATE_STANDBY:
"STOPPED",
2613 STATE_UNAVAILABLE:
"STOPPED",
2614 STATE_UNKNOWN:
"STOPPED",
2619 """Test if state is supported."""
2620 return domain == media_player.DOMAIN
2623 """Return attributes for a sync request."""
2624 return {
"supportActivityState":
True,
"supportPlaybackState":
True}
2627 """Return the attributes of this trait for this entity."""
2636 """Trait to get media playback state.
2638 https://developers.google.com/actions/smarthome/traits/channel
2641 name = TRAIT_CHANNEL
2642 commands = [COMMAND_SELECT_CHANNEL]
2646 """Test if state is supported."""
2648 domain == media_player.DOMAIN
2649 and (features & MediaPlayerEntityFeature.PLAY_MEDIA)
2650 and device_class == media_player.MediaPlayerDeviceClass.TV
2657 """Return attributes for a sync request."""
2658 return {
"availableChannels": [],
"commandOnlyChannels":
True}
2661 """Return channel query attributes."""
2664 async
def execute(self, command, data, params, challenge):
2665 """Execute an setChannel command."""
2666 if command == COMMAND_SELECT_CHANNEL:
2667 channel_number = params.get(
"channelNumber")
2671 if not channel_number:
2673 ERR_NO_AVAILABLE_CHANNEL,
2674 "Channel is not available",
2677 await self.
hasshass.services.async_call(
2678 media_player.DOMAIN,
2679 media_player.SERVICE_PLAY_MEDIA,
2681 ATTR_ENTITY_ID: self.
statestate.entity_id,
2682 media_player.ATTR_MEDIA_CONTENT_ID: channel_number,
2683 media_player.ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL,
2685 blocking=
not self.
configconfig.should_report_state,
2686 context=data.context,
2692 """Trait to get sensor state.
2694 https://developers.google.com/actions/smarthome/traits/sensorstate
2698 sensor.SensorDeviceClass.AQI: (
"AirQuality",
"AQI"),
2699 sensor.SensorDeviceClass.CO: (
"CarbonMonoxideLevel",
"PARTS_PER_MILLION"),
2700 sensor.SensorDeviceClass.CO2: (
"CarbonDioxideLevel",
"PARTS_PER_MILLION"),
2701 sensor.SensorDeviceClass.PM25: (
"PM2.5",
"MICROGRAMS_PER_CUBIC_METER"),
2702 sensor.SensorDeviceClass.PM10: (
"PM10",
"MICROGRAMS_PER_CUBIC_METER"),
2703 sensor.SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: (
2704 "VolatileOrganicCompounds",
2705 "PARTS_PER_MILLION",
2709 binary_sensor_types = {
2710 binary_sensor.BinarySensorDeviceClass.CO: (
2711 "CarbonMonoxideLevel",
2712 [
"carbon monoxide detected",
"no carbon monoxide detected",
"unknown"],
2714 binary_sensor.BinarySensorDeviceClass.SMOKE: (
2716 [
"smoke detected",
"no smoke detected",
"unknown"],
2718 binary_sensor.BinarySensorDeviceClass.MOISTURE: (
2720 [
"leak",
"no leak",
"unknown"],
2724 name = TRAIT_SENSOR_STATE
2725 commands: list[str] = []
2728 if aqi
is None or aqi < 0:
2735 return "unhealthy for sensitive groups"
2739 return "very unhealthy"
2745 """Test if state is supported."""
2746 return (domain == sensor.DOMAIN
and device_class
in cls.
sensor_typessensor_types)
or (
2747 domain == binary_sensor.DOMAIN
and device_class
in cls.
binary_sensor_typesbinary_sensor_types
2751 """Return attributes for a sync request."""
2752 device_class = self.
statestate.attributes.get(ATTR_DEVICE_CLASS)
2754 def create_sensor_state(
2756 raw_value_unit: str |
None =
None,
2757 available_states: list[str] |
None =
None,
2758 ) -> dict[str, Any]:
2759 sensor_state: dict[str, Any] = {
2763 sensor_state[
"numericCapabilities"] = {
"rawValueUnit": raw_value_unit}
2764 if available_states:
2765 sensor_state[
"descriptiveCapabilities"] = {
2766 "availableStates": available_states
2768 return {
"sensorStatesSupported": [sensor_state]}
2770 if self.
statestate.domain == sensor.DOMAIN:
2772 if device_class
is None or sensor_data
is None:
2774 available_states: list[str] |
None =
None
2775 if device_class == sensor.SensorDeviceClass.AQI:
2776 available_states = [
2779 "unhealthy for sensitive groups",
2785 return create_sensor_state(sensor_data[0], sensor_data[1], available_states)
2787 if device_class
is None or binary_sensor_data
is None:
2789 return create_sensor_state(
2790 binary_sensor_data[0], available_states=binary_sensor_data[1]
2794 """Return the attributes of this trait for this entity."""
2795 device_class = self.
statestate.attributes.get(ATTR_DEVICE_CLASS)
2797 def create_sensor_state(
2798 name: str, raw_value: float |
None =
None, current_state: str |
None =
None
2799 ) -> dict[str, Any]:
2800 sensor_state: dict[str, Any] = {
2802 "rawValue": raw_value,
2805 sensor_state[
"currentSensorState"] = current_state
2806 return {
"currentSensorStateData": [sensor_state]}
2808 if self.
statestate.domain == sensor.DOMAIN:
2810 if device_class
is None or sensor_data
is None:
2816 if self.
statestate.state == STATE_UNKNOWN:
2818 current_state: str |
None =
None
2819 if device_class == sensor.SensorDeviceClass.AQI:
2821 return create_sensor_state(sensor_data[0], value, current_state)
2824 if device_class
is None or binary_sensor_data
is None:
2830 }[self.
statestate.state]
2831 return create_sensor_state(
2832 binary_sensor_data[0], current_state=binary_sensor_data[1][value]
dictionary state_to_support
dict[str, Any] query_attributes(self)
dictionary state_to_service
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
def might_2fa(domain, features, device_class)
def _default_arm_state(self)
def _supported_states(self)
def supported(domain, features, device_class, attributes)
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
dict[str, Any] query_attributes(self)
def execute(self, command, data, params, challenge)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, _)
def supported(domain, features, device_class, _)
dict[str, Any] query_attributes(self)
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
def execute(self, command, data, params, challenge)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, attributes)
def execute(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
dict[str, Any] query_attributes(self)
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def execute_fanspeed(self, data, params)
def supported(domain, features, device_class, _)
def execute(self, command, data, params, challenge)
dict[str, Any] sync_attributes(self)
def execute_reverse(self, data, params)
def __init__(self, hass, state, config)
dict[str, Any] query_attributes(self)
dict[str, Any] sync_attributes(self)
def supported(domain, features, device_class, _)
def execute(self, command, data, params, challenge)
dict[str, Any] query_attributes(self)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def execute(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
def supported(domain, features, device_class, _)
def might_2fa(domain, features, device_class)
dict[str, Any] query_attributes(self)
def execute(self, command, data, params, challenge)
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
def _generate(self, name, settings)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, _)
def execute(self, command, data, params, challenge)
bool supported(domain, features, device_class, _)
dict[str, Any] sync_attributes(self)
dict[str, Any]|None query_notifications(self)
dict[str, Any] sync_options(self)
dict[str, Any] query_attributes(self)
def execute(self, command, data, params, challenge)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, _)
dict[str, Any] sync_attributes(self)
dict[str, Any] sync_attributes(self)
def might_2fa(domain, features, device_class)
def execute(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, _)
dict[str, Any] query_attributes(self)
def execute(self, command, data, params, challenge)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def supported(cls, domain, features, device_class, _)
str _air_quality_description_for_aqi(self, float|None aqi)
dict[str, Any] sync_attributes(self)
dictionary binary_sensor_types
def execute(self, command, data, params, challenge)
dict[str, Any] query_attributes(self)
dict[str, Any] sync_attributes(self)
def _execute_cover_or_valve(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
def _execute_vacuum(self, command, data, params, challenge)
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
def supported(domain, features, device_class, _)
dict[str, Any] query_attributes(self)
dictionary google_to_hvac
dictionary google_to_preset
dictionary hvac_to_google
def supported(domain, features, device_class, _)
dict[str, Any] query_attributes(self)
def climate_google_modes(self)
dictionary preset_to_google
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
def execute(self, command, data, params, challenge)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, _)
dict[str, Any] sync_attributes(self)
dict[str, Any] query_attributes(self)
def supported(domain, features, device_class, _)
def _execute_set_volume(self, data, params)
def execute(self, command, data, params, challenge)
def _execute_volume_relative(self, data, params)
dict[str, Any] sync_attributes(self)
def _set_volume_absolute(self, data, level)
def _execute_mute(self, data, params)
dict[str, Any] sync_attributes(self)
def execute(self, command, data, params, challenge)
dict[str, Any] sync_options(self)
None __init__(self, HomeAssistant hass, state, config)
def supported(domain, features, device_class, attributes)
dict[str, Any] query_attributes(self)
def can_execute(self, command, params)
dict[str, Any]|None query_notifications(self)
def might_2fa(domain, features, device_class)
web.Response get(self, web.Request request, str config_key)
str|None _next_selected(list[str] items, str|None selected)
def _verify_pin_challenge(data, state, challenge)
def _google_temp_unit(units)
dict[str, Any] _get_fan_speed(str speed_name)