2019-02-14 15:01:46 +00:00
|
|
|
"""Implement the Google Smart Home traits."""
|
2018-09-21 08:51:46 +00:00
|
|
|
import logging
|
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
from homeassistant.components import (
|
2019-04-03 17:20:56 +00:00
|
|
|
binary_sensor,
|
2019-03-23 16:16:43 +00:00
|
|
|
camera,
|
2018-03-08 22:39:10 +00:00
|
|
|
cover,
|
|
|
|
group,
|
|
|
|
fan,
|
2018-03-10 03:38:33 +00:00
|
|
|
input_boolean,
|
2018-03-08 22:39:10 +00:00
|
|
|
media_player,
|
|
|
|
light,
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
lock,
|
2018-03-08 22:39:10 +00:00
|
|
|
scene,
|
|
|
|
script,
|
|
|
|
switch,
|
2018-10-26 21:02:07 +00:00
|
|
|
vacuum,
|
2018-03-08 22:39:10 +00:00
|
|
|
)
|
2019-02-14 19:34:43 +00:00
|
|
|
from homeassistant.components.climate import const as climate
|
2018-03-08 22:39:10 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
ATTR_ENTITY_ID,
|
2019-04-19 21:50:21 +00:00
|
|
|
ATTR_DEVICE_CLASS,
|
2018-03-08 22:39:10 +00:00
|
|
|
SERVICE_TURN_OFF,
|
|
|
|
SERVICE_TURN_ON,
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
STATE_LOCKED,
|
2018-03-08 22:39:10 +00:00
|
|
|
STATE_OFF,
|
2019-03-21 17:57:42 +00:00
|
|
|
STATE_ON,
|
2018-03-08 22:39:10 +00:00
|
|
|
TEMP_CELSIUS,
|
|
|
|
TEMP_FAHRENHEIT,
|
2018-10-26 21:02:07 +00:00
|
|
|
ATTR_SUPPORTED_FEATURES,
|
2019-02-14 19:34:43 +00:00
|
|
|
ATTR_TEMPERATURE,
|
2019-04-03 11:53:44 +00:00
|
|
|
ATTR_ASSUMED_STATE,
|
2019-04-15 02:52:00 +00:00
|
|
|
STATE_UNKNOWN,
|
2018-03-08 22:39:10 +00:00
|
|
|
)
|
2018-11-11 21:02:33 +00:00
|
|
|
from homeassistant.core import DOMAIN as HA_DOMAIN
|
2018-03-08 22:39:10 +00:00
|
|
|
from homeassistant.util import color as color_util, temperature as temp_util
|
2019-04-15 02:52:00 +00:00
|
|
|
from .const import (
|
|
|
|
ERR_VALUE_OUT_OF_RANGE,
|
|
|
|
ERR_NOT_SUPPORTED,
|
|
|
|
ERR_FUNCTION_NOT_SUPPORTED,
|
2019-04-19 21:50:21 +00:00
|
|
|
ERR_CHALLENGE_NOT_SETUP,
|
|
|
|
CHALLENGE_ACK_NEEDED,
|
|
|
|
CHALLENGE_PIN_NEEDED,
|
|
|
|
CHALLENGE_FAILED_PIN_NEEDED,
|
2019-04-15 02:52:00 +00:00
|
|
|
)
|
2019-04-19 21:50:21 +00:00
|
|
|
from .error import SmartHomeError, ChallengeNeeded
|
2018-03-08 22:39:10 +00:00
|
|
|
|
2018-09-21 08:51:46 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
PREFIX_TRAITS = 'action.devices.traits.'
|
2019-03-23 16:16:43 +00:00
|
|
|
TRAIT_CAMERA_STREAM = PREFIX_TRAITS + 'CameraStream'
|
2018-03-08 22:39:10 +00:00
|
|
|
TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff'
|
2018-10-26 21:02:07 +00:00
|
|
|
TRAIT_DOCK = PREFIX_TRAITS + 'Dock'
|
|
|
|
TRAIT_STARTSTOP = PREFIX_TRAITS + 'StartStop'
|
2018-03-08 22:39:10 +00:00
|
|
|
TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness'
|
2019-04-10 03:17:13 +00:00
|
|
|
TRAIT_COLOR_SETTING = PREFIX_TRAITS + 'ColorSetting'
|
2018-03-08 22:39:10 +00:00
|
|
|
TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
|
|
|
|
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
|
2018-11-11 21:02:33 +00:00
|
|
|
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
2018-11-29 20:14:17 +00:00
|
|
|
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
2019-03-30 03:51:47 +00:00
|
|
|
TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose'
|
2019-04-24 16:08:41 +00:00
|
|
|
TRAIT_VOLUME = PREFIX_TRAITS + 'Volume'
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
PREFIX_COMMANDS = 'action.devices.commands.'
|
|
|
|
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
2019-03-23 16:16:43 +00:00
|
|
|
COMMAND_GET_CAMERA_STREAM = PREFIX_COMMANDS + 'GetCameraStream'
|
2018-10-26 21:02:07 +00:00
|
|
|
COMMAND_DOCK = PREFIX_COMMANDS + 'Dock'
|
|
|
|
COMMAND_STARTSTOP = PREFIX_COMMANDS + 'StartStop'
|
|
|
|
COMMAND_PAUSEUNPAUSE = PREFIX_COMMANDS + 'PauseUnpause'
|
2018-03-08 22:39:10 +00:00
|
|
|
COMMAND_BRIGHTNESS_ABSOLUTE = PREFIX_COMMANDS + 'BrightnessAbsolute'
|
|
|
|
COMMAND_COLOR_ABSOLUTE = PREFIX_COMMANDS + 'ColorAbsolute'
|
|
|
|
COMMAND_ACTIVATE_SCENE = PREFIX_COMMANDS + 'ActivateScene'
|
|
|
|
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = (
|
|
|
|
PREFIX_COMMANDS + 'ThermostatTemperatureSetpoint')
|
|
|
|
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
|
|
|
|
PREFIX_COMMANDS + 'ThermostatTemperatureSetRange')
|
|
|
|
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
2018-11-11 21:02:33 +00:00
|
|
|
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
2018-11-29 20:14:17 +00:00
|
|
|
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
2019-03-30 03:51:47 +00:00
|
|
|
COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose'
|
2019-04-24 16:08:41 +00:00
|
|
|
COMMAND_SET_VOLUME = PREFIX_COMMANDS + 'setVolume'
|
|
|
|
COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + 'volumeRelative'
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
TRAITS = []
|
|
|
|
|
|
|
|
|
|
|
|
def register_trait(trait):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Decorate a function to register a trait."""
|
2018-03-08 22:39:10 +00:00
|
|
|
TRAITS.append(trait)
|
|
|
|
return trait
|
|
|
|
|
|
|
|
|
2018-08-22 07:17:29 +00:00
|
|
|
def _google_temp_unit(units):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Return Google temperature unit."""
|
2018-08-22 07:17:29 +00:00
|
|
|
if units == TEMP_FAHRENHEIT:
|
2018-03-08 22:39:10 +00:00
|
|
|
return 'F'
|
|
|
|
return 'C'
|
|
|
|
|
|
|
|
|
|
|
|
class _Trait:
|
|
|
|
"""Represents a Trait inside Google Assistant skill."""
|
|
|
|
|
|
|
|
commands = []
|
|
|
|
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
def __init__(self, hass, state, config):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Initialize a trait for a state."""
|
2018-08-22 07:17:29 +00:00
|
|
|
self.hass = hass
|
2018-03-08 22:39:10 +00:00
|
|
|
self.state = state
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
self.config = config
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return attributes for a sync request."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return the attributes of this trait for this entity."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def can_execute(self, command, params):
|
|
|
|
"""Test if command can be executed."""
|
|
|
|
return command in self.commands
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Execute a trait command."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class BrightnessTrait(_Trait):
|
|
|
|
"""Trait to control brightness of a device.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/brightness
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_BRIGHTNESS
|
|
|
|
commands = [
|
|
|
|
COMMAND_BRIGHTNESS_ABSOLUTE
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain == light.DOMAIN:
|
|
|
|
return features & light.SUPPORT_BRIGHTNESS
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return brightness attributes for a sync request."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return brightness query attributes."""
|
|
|
|
domain = self.state.domain
|
|
|
|
response = {}
|
|
|
|
|
|
|
|
if domain == light.DOMAIN:
|
|
|
|
brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS)
|
|
|
|
if brightness is not None:
|
|
|
|
response['brightness'] = int(100 * (brightness / 255))
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Execute a brightness command."""
|
|
|
|
domain = self.state.domain
|
|
|
|
|
|
|
|
if domain == light.DOMAIN:
|
2018-08-22 07:17:29 +00:00
|
|
|
await self.hass.services.async_call(
|
2018-03-08 22:39:10 +00:00
|
|
|
light.DOMAIN, light.SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
light.ATTR_BRIGHTNESS_PCT: params['brightness']
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
|
2019-03-23 16:16:43 +00:00
|
|
|
@register_trait
|
|
|
|
class CameraStreamTrait(_Trait):
|
|
|
|
"""Trait to stream from cameras.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/camerastream
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_CAMERA_STREAM
|
|
|
|
commands = [
|
|
|
|
COMMAND_GET_CAMERA_STREAM
|
|
|
|
]
|
|
|
|
|
|
|
|
stream_info = None
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2019-03-23 16:16:43 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain == camera.DOMAIN:
|
|
|
|
return features & camera.SUPPORT_STREAM
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return stream attributes for a sync request."""
|
|
|
|
return {
|
|
|
|
'cameraStreamSupportedProtocols': [
|
|
|
|
"hls",
|
|
|
|
],
|
|
|
|
'cameraStreamNeedAuthToken': False,
|
|
|
|
'cameraStreamNeedDrmEncryption': False,
|
|
|
|
}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return camera stream attributes."""
|
|
|
|
return self.stream_info or {}
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2019-03-23 16:16:43 +00:00
|
|
|
"""Execute a get camera stream command."""
|
|
|
|
url = await self.hass.components.camera.async_request_stream(
|
|
|
|
self.state.entity_id, 'hls')
|
|
|
|
self.stream_info = {
|
|
|
|
'cameraStreamAccessUrl': self.hass.config.api.base_url + url
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
@register_trait
|
|
|
|
class OnOffTrait(_Trait):
|
|
|
|
"""Trait to offer basic on and off functionality.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/onoff
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_ONOFF
|
|
|
|
commands = [
|
|
|
|
COMMAND_ONOFF
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
return domain in (
|
|
|
|
group.DOMAIN,
|
2018-03-10 03:38:33 +00:00
|
|
|
input_boolean.DOMAIN,
|
2018-03-08 22:39:10 +00:00
|
|
|
switch.DOMAIN,
|
|
|
|
fan.DOMAIN,
|
|
|
|
light.DOMAIN,
|
|
|
|
media_player.DOMAIN,
|
|
|
|
)
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return OnOff attributes for a sync request."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return OnOff query attributes."""
|
|
|
|
return {'on': self.state.state != STATE_OFF}
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Execute an OnOff command."""
|
|
|
|
domain = self.state.domain
|
|
|
|
|
2019-03-30 03:51:47 +00:00
|
|
|
if domain == group.DOMAIN:
|
2018-03-08 22:39:10 +00:00
|
|
|
service_domain = HA_DOMAIN
|
|
|
|
service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF
|
|
|
|
|
|
|
|
else:
|
|
|
|
service_domain = domain
|
|
|
|
service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF
|
|
|
|
|
2018-08-22 07:17:29 +00:00
|
|
|
await self.hass.services.async_call(service_domain, service, {
|
2018-03-08 22:39:10 +00:00
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
2019-04-10 03:17:13 +00:00
|
|
|
class ColorSettingTrait(_Trait):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Trait to offer color temperature functionality.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/colortemperature
|
|
|
|
"""
|
|
|
|
|
2019-04-10 03:17:13 +00:00
|
|
|
name = TRAIT_COLOR_SETTING
|
2018-03-08 22:39:10 +00:00
|
|
|
commands = [
|
|
|
|
COMMAND_COLOR_ABSOLUTE
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain != light.DOMAIN:
|
|
|
|
return False
|
|
|
|
|
2019-04-10 03:17:13 +00:00
|
|
|
return (features & light.SUPPORT_COLOR_TEMP or
|
|
|
|
features & light.SUPPORT_COLOR)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return color temperature attributes for a sync request."""
|
|
|
|
attrs = self.state.attributes
|
2019-04-10 03:17:13 +00:00
|
|
|
features = attrs.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
response = {}
|
|
|
|
|
|
|
|
if features & light.SUPPORT_COLOR:
|
2019-04-11 04:35:37 +00:00
|
|
|
response['colorModel'] = 'hsv'
|
2019-04-10 03:17:13 +00:00
|
|
|
|
|
|
|
if features & light.SUPPORT_COLOR_TEMP:
|
|
|
|
# Max Kelvin is Min Mireds K = 1000000 / mireds
|
|
|
|
# Min Kevin is Max Mireds K = 1000000 / mireds
|
2019-04-11 04:35:37 +00:00
|
|
|
response['colorTemperatureRange'] = {
|
|
|
|
'temperatureMaxK':
|
2019-04-10 03:17:13 +00:00
|
|
|
color_util.color_temperature_mired_to_kelvin(
|
2019-04-11 04:35:37 +00:00
|
|
|
attrs.get(light.ATTR_MIN_MIREDS)),
|
|
|
|
'temperatureMinK':
|
2019-04-10 03:17:13 +00:00
|
|
|
color_util.color_temperature_mired_to_kelvin(
|
2019-04-11 04:35:37 +00:00
|
|
|
attrs.get(light.ATTR_MAX_MIREDS)),
|
|
|
|
}
|
2019-04-10 03:17:13 +00:00
|
|
|
|
|
|
|
return response
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return color temperature query attributes."""
|
2019-04-10 03:17:13 +00:00
|
|
|
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
color = {}
|
|
|
|
|
|
|
|
if features & light.SUPPORT_COLOR:
|
|
|
|
color_hs = self.state.attributes.get(light.ATTR_HS_COLOR)
|
2019-04-11 04:35:37 +00:00
|
|
|
brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS, 1)
|
2019-04-10 03:17:13 +00:00
|
|
|
if color_hs is not None:
|
2019-04-11 04:35:37 +00:00
|
|
|
color['spectrumHsv'] = {
|
|
|
|
'hue': color_hs[0],
|
|
|
|
'saturation': color_hs[1]/100,
|
|
|
|
'value': brightness/255,
|
|
|
|
}
|
2019-04-10 03:17:13 +00:00
|
|
|
|
|
|
|
if features & light.SUPPORT_COLOR_TEMP:
|
|
|
|
temp = self.state.attributes.get(light.ATTR_COLOR_TEMP)
|
|
|
|
# Some faulty integrations might put 0 in here, raising exception.
|
|
|
|
if temp == 0:
|
|
|
|
_LOGGER.warning('Entity %s has incorrect color temperature %s',
|
|
|
|
self.state.entity_id, temp)
|
|
|
|
elif temp is not None:
|
2019-04-11 04:35:37 +00:00
|
|
|
color['temperatureK'] = \
|
2019-04-10 03:17:13 +00:00
|
|
|
color_util.color_temperature_mired_to_kelvin(temp)
|
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
response = {}
|
|
|
|
|
2019-04-10 03:17:13 +00:00
|
|
|
if color:
|
|
|
|
response['color'] = color
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
return response
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Execute a color temperature command."""
|
2019-04-10 03:17:13 +00:00
|
|
|
if 'temperature' in params['color']:
|
|
|
|
temp = color_util.color_temperature_kelvin_to_mired(
|
|
|
|
params['color']['temperature'])
|
|
|
|
min_temp = self.state.attributes[light.ATTR_MIN_MIREDS]
|
|
|
|
max_temp = self.state.attributes[light.ATTR_MAX_MIREDS]
|
|
|
|
|
|
|
|
if temp < min_temp or temp > max_temp:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_VALUE_OUT_OF_RANGE,
|
|
|
|
"Temperature should be between {} and {}".format(min_temp,
|
|
|
|
max_temp))
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
light.DOMAIN, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
light.ATTR_COLOR_TEMP: temp,
|
|
|
|
}, blocking=True, context=data.context)
|
|
|
|
|
|
|
|
elif 'spectrumRGB' in params['color']:
|
|
|
|
# Convert integer to hex format and left pad with 0's till length 6
|
|
|
|
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
|
|
|
|
color = color_util.color_RGB_to_hs(
|
|
|
|
*color_util.rgb_hex_to_rgb_list(hex_value))
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
light.DOMAIN, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
light.ATTR_HS_COLOR: color
|
|
|
|
}, blocking=True, context=data.context)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
2019-04-11 04:35:37 +00:00
|
|
|
elif 'spectrumHSV' in params['color']:
|
|
|
|
color = params['color']['spectrumHSV']
|
|
|
|
saturation = color['saturation'] * 100
|
|
|
|
brightness = color['value'] * 255
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
light.DOMAIN, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
light.ATTR_HS_COLOR: [color['hue'], saturation],
|
|
|
|
light.ATTR_BRIGHTNESS: brightness
|
|
|
|
}, blocking=True, context=data.context)
|
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class SceneTrait(_Trait):
|
|
|
|
"""Trait to offer scene functionality.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/scene
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_SCENE
|
|
|
|
commands = [
|
|
|
|
COMMAND_ACTIVATE_SCENE
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
return domain in (scene.DOMAIN, script.DOMAIN)
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return scene attributes for a sync request."""
|
|
|
|
# Neither supported domain can support sceneReversible
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return scene query attributes."""
|
|
|
|
return {}
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Execute a scene command."""
|
|
|
|
# Don't block for scripts as they can be slow.
|
2018-08-22 07:17:29 +00:00
|
|
|
await self.hass.services.async_call(
|
|
|
|
self.state.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=self.state.domain != script.DOMAIN,
|
|
|
|
context=data.context)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
|
2018-10-26 21:02:07 +00:00
|
|
|
@register_trait
|
|
|
|
class DockTrait(_Trait):
|
|
|
|
"""Trait to offer dock functionality.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/dock
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_DOCK
|
|
|
|
commands = [
|
|
|
|
COMMAND_DOCK
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-10-26 21:02:07 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
return domain == vacuum.DOMAIN
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return dock attributes for a sync request."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return dock query attributes."""
|
|
|
|
return {'isDocked': self.state.state == vacuum.STATE_DOCKED}
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-10-26 21:02:07 +00:00
|
|
|
"""Execute a dock command."""
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-10-26 21:02:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class StartStopTrait(_Trait):
|
|
|
|
"""Trait to offer StartStop functionality.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/startstop
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_STARTSTOP
|
|
|
|
commands = [
|
|
|
|
COMMAND_STARTSTOP,
|
|
|
|
COMMAND_PAUSEUNPAUSE
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-10-26 21:02:07 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
return domain == vacuum.DOMAIN
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return StartStop attributes for a sync request."""
|
|
|
|
return {'pausable':
|
|
|
|
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
& vacuum.SUPPORT_PAUSE != 0}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return StartStop query attributes."""
|
|
|
|
return {
|
|
|
|
'isRunning': self.state.state == vacuum.STATE_CLEANING,
|
|
|
|
'isPaused': self.state.state == vacuum.STATE_PAUSED,
|
|
|
|
}
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-10-26 21:02:07 +00:00
|
|
|
"""Execute a StartStop command."""
|
|
|
|
if command == COMMAND_STARTSTOP:
|
|
|
|
if params['start']:
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
self.state.domain, vacuum.SERVICE_START, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-10-26 21:02:07 +00:00
|
|
|
else:
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
self.state.domain, vacuum.SERVICE_STOP, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-10-26 21:02:07 +00:00
|
|
|
elif command == COMMAND_PAUSEUNPAUSE:
|
|
|
|
if params['pause']:
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
self.state.domain, vacuum.SERVICE_PAUSE, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-10-26 21:02:07 +00:00
|
|
|
else:
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
self.state.domain, vacuum.SERVICE_START, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-10-26 21:02:07 +00:00
|
|
|
|
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
@register_trait
|
|
|
|
class TemperatureSettingTrait(_Trait):
|
|
|
|
"""Trait to offer handling both temperature point and modes functionality.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/temperaturesetting
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_TEMPERATURE_SETTING
|
|
|
|
commands = [
|
|
|
|
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
|
|
|
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
|
|
|
COMMAND_THERMOSTAT_SET_MODE,
|
|
|
|
]
|
|
|
|
# We do not support "on" as we are unable to know how to restore
|
|
|
|
# the last mode.
|
|
|
|
hass_to_google = {
|
|
|
|
climate.STATE_HEAT: 'heat',
|
|
|
|
climate.STATE_COOL: 'cool',
|
2019-02-14 19:34:43 +00:00
|
|
|
STATE_OFF: 'off',
|
2018-03-08 22:39:10 +00:00
|
|
|
climate.STATE_AUTO: 'heatcool',
|
2018-12-07 06:15:04 +00:00
|
|
|
climate.STATE_FAN_ONLY: 'fan-only',
|
|
|
|
climate.STATE_DRY: 'dry',
|
|
|
|
climate.STATE_ECO: 'eco'
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
|
|
|
google_to_hass = {value: key for key, value in hass_to_google.items()}
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain != climate.DOMAIN:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return features & climate.SUPPORT_OPERATION_MODE
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return temperature point and modes attributes for a sync request."""
|
|
|
|
modes = []
|
2019-03-21 17:57:42 +00:00
|
|
|
supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
|
|
|
|
|
|
|
if supported & climate.SUPPORT_ON_OFF != 0:
|
|
|
|
modes.append(STATE_OFF)
|
|
|
|
modes.append(STATE_ON)
|
|
|
|
|
|
|
|
if supported & climate.SUPPORT_OPERATION_MODE != 0:
|
|
|
|
for mode in self.state.attributes.get(climate.ATTR_OPERATION_LIST,
|
|
|
|
[]):
|
|
|
|
google_mode = self.hass_to_google.get(mode)
|
|
|
|
if google_mode and google_mode not in modes:
|
|
|
|
modes.append(google_mode)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
'availableThermostatModes': ','.join(modes),
|
2018-08-22 07:17:29 +00:00
|
|
|
'thermostatTemperatureUnit': _google_temp_unit(
|
|
|
|
self.hass.config.units.temperature_unit)
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return temperature point and modes query attributes."""
|
|
|
|
attrs = self.state.attributes
|
|
|
|
response = {}
|
|
|
|
|
|
|
|
operation = attrs.get(climate.ATTR_OPERATION_MODE)
|
2019-03-21 17:57:42 +00:00
|
|
|
supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
|
|
|
|
|
|
|
if (supported & climate.SUPPORT_ON_OFF
|
|
|
|
and self.state.state == STATE_OFF):
|
|
|
|
response['thermostatMode'] = 'off'
|
|
|
|
elif (supported & climate.SUPPORT_OPERATION_MODE and
|
|
|
|
operation in self.hass_to_google):
|
2018-03-08 22:39:10 +00:00
|
|
|
response['thermostatMode'] = self.hass_to_google[operation]
|
2019-03-21 17:57:42 +00:00
|
|
|
elif supported & climate.SUPPORT_ON_OFF:
|
|
|
|
response['thermostatMode'] = 'on'
|
2018-03-08 22:39:10 +00:00
|
|
|
|
2018-08-22 07:17:29 +00:00
|
|
|
unit = self.hass.config.units.temperature_unit
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
|
|
|
|
if current_temp is not None:
|
|
|
|
response['thermostatTemperatureAmbient'] = \
|
|
|
|
round(temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1)
|
|
|
|
|
|
|
|
current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
|
|
|
|
if current_humidity is not None:
|
|
|
|
response['thermostatHumidityAmbient'] = current_humidity
|
|
|
|
|
2019-03-22 20:42:56 +00:00
|
|
|
if operation == climate.STATE_AUTO:
|
|
|
|
if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
|
|
|
|
supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
|
|
|
|
response['thermostatTemperatureSetpointHigh'] = \
|
|
|
|
round(temp_util.convert(
|
|
|
|
attrs[climate.ATTR_TARGET_TEMP_HIGH],
|
|
|
|
unit, TEMP_CELSIUS), 1)
|
|
|
|
response['thermostatTemperatureSetpointLow'] = \
|
|
|
|
round(temp_util.convert(
|
|
|
|
attrs[climate.ATTR_TARGET_TEMP_LOW],
|
|
|
|
unit, TEMP_CELSIUS), 1)
|
|
|
|
else:
|
|
|
|
target_temp = attrs.get(ATTR_TEMPERATURE)
|
|
|
|
if target_temp is not None:
|
|
|
|
target_temp = round(
|
|
|
|
temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
|
|
|
|
response['thermostatTemperatureSetpointHigh'] = target_temp
|
|
|
|
response['thermostatTemperatureSetpointLow'] = target_temp
|
2018-03-08 22:39:10 +00:00
|
|
|
else:
|
2019-02-14 19:34:43 +00:00
|
|
|
target_temp = attrs.get(ATTR_TEMPERATURE)
|
2018-03-08 22:39:10 +00:00
|
|
|
if target_temp is not None:
|
|
|
|
response['thermostatTemperatureSetpoint'] = round(
|
|
|
|
temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Execute a temperature point or mode command."""
|
|
|
|
# All sent in temperatures are always in Celsius
|
2018-08-22 07:17:29 +00:00
|
|
|
unit = self.hass.config.units.temperature_unit
|
2018-03-08 22:39:10 +00:00
|
|
|
min_temp = self.state.attributes[climate.ATTR_MIN_TEMP]
|
|
|
|
max_temp = self.state.attributes[climate.ATTR_MAX_TEMP]
|
|
|
|
|
|
|
|
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
|
2018-12-10 11:31:52 +00:00
|
|
|
temp = temp_util.convert(
|
|
|
|
params['thermostatTemperatureSetpoint'], TEMP_CELSIUS,
|
|
|
|
unit)
|
|
|
|
if unit == TEMP_FAHRENHEIT:
|
|
|
|
temp = round(temp)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
if temp < min_temp or temp > max_temp:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_VALUE_OUT_OF_RANGE,
|
|
|
|
"Temperature should be between {} and {}".format(min_temp,
|
|
|
|
max_temp))
|
|
|
|
|
2018-08-22 07:17:29 +00:00
|
|
|
await self.hass.services.async_call(
|
2018-03-08 22:39:10 +00:00
|
|
|
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
2019-02-14 19:34:43 +00:00
|
|
|
ATTR_TEMPERATURE: temp
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
|
|
|
|
temp_high = temp_util.convert(
|
|
|
|
params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS,
|
|
|
|
unit)
|
2018-12-10 11:31:52 +00:00
|
|
|
if unit == TEMP_FAHRENHEIT:
|
|
|
|
temp_high = round(temp_high)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
if temp_high < min_temp or temp_high > max_temp:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_VALUE_OUT_OF_RANGE,
|
|
|
|
"Upper bound for temperature range should be between "
|
|
|
|
"{} and {}".format(min_temp, max_temp))
|
|
|
|
|
|
|
|
temp_low = temp_util.convert(
|
2018-12-10 11:31:52 +00:00
|
|
|
params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS,
|
|
|
|
unit)
|
|
|
|
if unit == TEMP_FAHRENHEIT:
|
|
|
|
temp_low = round(temp_low)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
if temp_low < min_temp or temp_low > max_temp:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_VALUE_OUT_OF_RANGE,
|
|
|
|
"Lower bound for temperature range should be between "
|
|
|
|
"{} and {}".format(min_temp, max_temp))
|
|
|
|
|
2019-03-22 20:42:56 +00:00
|
|
|
supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
|
|
|
svc_data = {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
}
|
|
|
|
|
|
|
|
if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
|
|
|
|
supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
|
|
|
|
svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
|
|
|
svc_data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
|
|
|
else:
|
|
|
|
svc_data[ATTR_TEMPERATURE] = (temp_high + temp_low) / 2
|
|
|
|
|
2018-08-22 07:17:29 +00:00
|
|
|
await self.hass.services.async_call(
|
2019-03-22 20:42:56 +00:00
|
|
|
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, svc_data,
|
|
|
|
blocking=True, context=data.context)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
elif command == COMMAND_THERMOSTAT_SET_MODE:
|
2019-03-21 17:57:42 +00:00
|
|
|
target_mode = params['thermostatMode']
|
|
|
|
supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
|
|
|
|
|
|
|
if (target_mode in [STATE_ON, STATE_OFF] and
|
|
|
|
supported & climate.SUPPORT_ON_OFF):
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
climate.DOMAIN,
|
|
|
|
(SERVICE_TURN_ON
|
|
|
|
if target_mode == STATE_ON
|
|
|
|
else SERVICE_TURN_OFF),
|
2019-03-22 20:42:56 +00:00
|
|
|
{ATTR_ENTITY_ID: self.state.entity_id},
|
|
|
|
blocking=True, context=data.context)
|
2019-03-21 17:57:42 +00:00
|
|
|
elif supported & climate.SUPPORT_OPERATION_MODE:
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
climate.ATTR_OPERATION_MODE:
|
|
|
|
self.google_to_hass[target_mode],
|
|
|
|
}, blocking=True, context=data.context)
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class LockUnlockTrait(_Trait):
|
|
|
|
"""Trait to lock or unlock a lock.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/lockunlock
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_LOCKUNLOCK
|
|
|
|
commands = [
|
|
|
|
COMMAND_LOCKUNLOCK
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
return domain == lock.DOMAIN
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return LockUnlock attributes for a sync request."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return LockUnlock query attributes."""
|
|
|
|
return {'isLocked': self.state.state == STATE_LOCKED}
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
"""Execute an LockUnlock command."""
|
|
|
|
if params['lock']:
|
|
|
|
service = lock.SERVICE_LOCK
|
|
|
|
else:
|
2019-05-08 14:55:30 +00:00
|
|
|
_verify_pin_challenge(data, challenge)
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
service = lock.SERVICE_UNLOCK
|
|
|
|
|
|
|
|
await self.hass.services.async_call(lock.DOMAIN, service, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-11-11 21:02:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class FanSpeedTrait(_Trait):
|
|
|
|
"""Trait to control speed of Fan.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/fanspeed
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_FANSPEED
|
|
|
|
commands = [
|
|
|
|
COMMAND_FANSPEED
|
|
|
|
]
|
|
|
|
|
|
|
|
speed_synonyms = {
|
|
|
|
fan.SPEED_OFF: ['stop', 'off'],
|
|
|
|
fan.SPEED_LOW: ['slow', 'low', 'slowest', 'lowest'],
|
|
|
|
fan.SPEED_MEDIUM: ['medium', 'mid', 'middle'],
|
|
|
|
fan.SPEED_HIGH: [
|
|
|
|
'high', 'max', 'fast', 'highest', 'fastest', 'maximum'
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-11-11 21:02:33 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain != fan.DOMAIN:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return features & fan.SUPPORT_SET_SPEED
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return speed point and modes attributes for a sync request."""
|
|
|
|
modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, [])
|
|
|
|
speeds = []
|
|
|
|
for mode in modes:
|
2018-11-29 21:24:53 +00:00
|
|
|
if mode not in self.speed_synonyms:
|
|
|
|
continue
|
2018-11-11 21:02:33 +00:00
|
|
|
speed = {
|
|
|
|
"speed_name": mode,
|
|
|
|
"speed_values": [{
|
|
|
|
"speed_synonym": self.speed_synonyms.get(mode),
|
|
|
|
"lang": 'en'
|
|
|
|
}]
|
|
|
|
}
|
|
|
|
speeds.append(speed)
|
|
|
|
|
|
|
|
return {
|
|
|
|
'availableFanSpeeds': {
|
|
|
|
'speeds': speeds,
|
|
|
|
'ordered': True
|
|
|
|
},
|
|
|
|
"reversible": bool(self.state.attributes.get(
|
|
|
|
ATTR_SUPPORTED_FEATURES, 0) & fan.SUPPORT_DIRECTION)
|
|
|
|
}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return speed point and modes query attributes."""
|
|
|
|
attrs = self.state.attributes
|
|
|
|
response = {}
|
|
|
|
|
|
|
|
speed = attrs.get(fan.ATTR_SPEED)
|
|
|
|
if speed is not None:
|
|
|
|
response['on'] = speed != fan.SPEED_OFF
|
|
|
|
response['online'] = True
|
|
|
|
response['currentFanSpeedSetting'] = speed
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-11-11 21:02:33 +00:00
|
|
|
"""Execute an SetFanSpeed command."""
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
fan.DOMAIN, fan.SERVICE_SET_SPEED, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
fan.ATTR_SPEED: params['fanSpeed']
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2018-11-29 20:14:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class ModesTrait(_Trait):
|
|
|
|
"""Trait to set modes.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/modes
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_MODES
|
|
|
|
commands = [
|
|
|
|
COMMAND_MODES
|
|
|
|
]
|
|
|
|
|
|
|
|
# Google requires specific mode names and settings. Here is the full list.
|
|
|
|
# https://developers.google.com/actions/reference/smarthome/traits/modes
|
|
|
|
# All settings are mapped here as of 2018-11-28 and can be used for other
|
|
|
|
# entity types.
|
|
|
|
|
|
|
|
HA_TO_GOOGLE = {
|
|
|
|
media_player.ATTR_INPUT_SOURCE: "input source",
|
|
|
|
}
|
|
|
|
SUPPORTED_MODE_SETTINGS = {
|
|
|
|
'xsmall': [
|
|
|
|
'xsmall', 'extra small', 'min', 'minimum', 'tiny', 'xs'],
|
|
|
|
'small': ['small', 'half'],
|
|
|
|
'large': ['large', 'big', 'full'],
|
|
|
|
'xlarge': ['extra large', 'xlarge', 'xl'],
|
|
|
|
'Cool': ['cool', 'rapid cool', 'rapid cooling'],
|
|
|
|
'Heat': ['heat'], 'Low': ['low'],
|
|
|
|
'Medium': ['medium', 'med', 'mid', 'half'],
|
|
|
|
'High': ['high'],
|
|
|
|
'Auto': ['auto', 'automatic'],
|
|
|
|
'Bake': ['bake'], 'Roast': ['roast'],
|
|
|
|
'Convection Bake': ['convection bake', 'convect bake'],
|
|
|
|
'Convection Roast': ['convection roast', 'convect roast'],
|
|
|
|
'Favorite': ['favorite'],
|
|
|
|
'Broil': ['broil'],
|
|
|
|
'Warm': ['warm'],
|
|
|
|
'Off': ['off'],
|
|
|
|
'On': ['on'],
|
|
|
|
'Normal': [
|
|
|
|
'normal', 'normal mode', 'normal setting', 'standard',
|
|
|
|
'schedule', 'original', 'default', 'old settings'
|
|
|
|
],
|
|
|
|
'None': ['none'],
|
|
|
|
'Tap Cold': ['tap cold'],
|
|
|
|
'Cold Warm': ['cold warm'],
|
|
|
|
'Hot': ['hot'],
|
|
|
|
'Extra Hot': ['extra hot'],
|
|
|
|
'Eco': ['eco'],
|
|
|
|
'Wool': ['wool', 'fleece'],
|
|
|
|
'Turbo': ['turbo'],
|
|
|
|
'Rinse': ['rinse', 'rinsing', 'rinse wash'],
|
|
|
|
'Away': ['away', 'holiday'],
|
|
|
|
'maximum': ['maximum'],
|
|
|
|
'media player': ['media player'],
|
|
|
|
'chromecast': ['chromecast'],
|
|
|
|
'tv': [
|
|
|
|
'tv', 'television', 'tv position', 'television position',
|
|
|
|
'watching tv', 'watching tv position', 'entertainment',
|
|
|
|
'entertainment position'
|
|
|
|
],
|
|
|
|
'am fm': ['am fm', 'am radio', 'fm radio'],
|
|
|
|
'internet radio': ['internet radio'],
|
|
|
|
'satellite': ['satellite'],
|
|
|
|
'game console': ['game console'],
|
|
|
|
'antifrost': ['antifrost', 'anti-frost'],
|
|
|
|
'boost': ['boost'],
|
|
|
|
'Clock': ['clock'],
|
|
|
|
'Message': ['message'],
|
|
|
|
'Messages': ['messages'],
|
|
|
|
'News': ['news'],
|
|
|
|
'Disco': ['disco'],
|
|
|
|
'antifreeze': ['antifreeze', 'anti-freeze', 'anti freeze'],
|
|
|
|
'balanced': ['balanced', 'normal'],
|
|
|
|
'swing': ['swing'],
|
|
|
|
'media': ['media', 'media mode'],
|
|
|
|
'panic': ['panic'],
|
|
|
|
'ring': ['ring'],
|
|
|
|
'frozen': ['frozen', 'rapid frozen', 'rapid freeze'],
|
|
|
|
'cotton': ['cotton', 'cottons'],
|
|
|
|
'blend': ['blend', 'mix'],
|
|
|
|
'baby wash': ['baby wash'],
|
|
|
|
'synthetics': ['synthetic', 'synthetics', 'compose'],
|
|
|
|
'hygiene': ['hygiene', 'sterilization'],
|
|
|
|
'smart': ['smart', 'intelligent', 'intelligence'],
|
|
|
|
'comfortable': ['comfortable', 'comfort'],
|
|
|
|
'manual': ['manual'],
|
|
|
|
'energy saving': ['energy saving'],
|
|
|
|
'sleep': ['sleep'],
|
|
|
|
'quick wash': ['quick wash', 'fast wash'],
|
|
|
|
'cold': ['cold'],
|
|
|
|
'airsupply': ['airsupply', 'air supply'],
|
|
|
|
'dehumidification': ['dehumidication', 'dehumidify'],
|
|
|
|
'game': ['game', 'game mode']
|
|
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2018-11-29 20:14:17 +00:00
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain != media_player.DOMAIN:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return features & media_player.SUPPORT_SELECT_SOURCE
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return mode attributes for a sync request."""
|
|
|
|
sources_list = self.state.attributes.get(
|
|
|
|
media_player.ATTR_INPUT_SOURCE_LIST, [])
|
|
|
|
modes = []
|
|
|
|
sources = {}
|
|
|
|
|
|
|
|
if sources_list:
|
|
|
|
sources = {
|
|
|
|
"name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE),
|
|
|
|
"name_values": [{
|
|
|
|
"name_synonym": ['input source'],
|
|
|
|
"lang": "en"
|
|
|
|
}],
|
|
|
|
"settings": [],
|
|
|
|
"ordered": False
|
|
|
|
}
|
|
|
|
for source in sources_list:
|
|
|
|
if source in self.SUPPORTED_MODE_SETTINGS:
|
|
|
|
src = source
|
|
|
|
synonyms = self.SUPPORTED_MODE_SETTINGS.get(src)
|
|
|
|
elif source.lower() in self.SUPPORTED_MODE_SETTINGS:
|
|
|
|
src = source.lower()
|
|
|
|
synonyms = self.SUPPORTED_MODE_SETTINGS.get(src)
|
|
|
|
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
|
|
|
|
sources['settings'].append(
|
|
|
|
{
|
|
|
|
"setting_name": src,
|
|
|
|
"setting_values": [{
|
|
|
|
"setting_synonym": synonyms,
|
|
|
|
"lang": "en"
|
|
|
|
}]
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if sources:
|
|
|
|
modes.append(sources)
|
|
|
|
payload = {'availableModes': modes}
|
|
|
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return current modes."""
|
|
|
|
attrs = self.state.attributes
|
|
|
|
response = {}
|
|
|
|
mode_settings = {}
|
|
|
|
|
|
|
|
if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST):
|
|
|
|
mode_settings.update({
|
|
|
|
media_player.ATTR_INPUT_SOURCE: attrs.get(
|
|
|
|
media_player.ATTR_INPUT_SOURCE)
|
|
|
|
})
|
|
|
|
if mode_settings:
|
|
|
|
response['on'] = self.state.state != STATE_OFF
|
|
|
|
response['online'] = True
|
|
|
|
response['currentModeSettings'] = mode_settings
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2018-11-29 20:14:17 +00:00
|
|
|
"""Execute an SetModes command."""
|
|
|
|
settings = params.get('updateModeSettings')
|
|
|
|
requested_source = settings.get(
|
|
|
|
self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE))
|
|
|
|
|
|
|
|
if requested_source:
|
|
|
|
for src in self.state.attributes.get(
|
|
|
|
media_player.ATTR_INPUT_SOURCE_LIST):
|
|
|
|
if src.lower() == requested_source.lower():
|
|
|
|
source = src
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
media_player.DOMAIN,
|
|
|
|
media_player.SERVICE_SELECT_SOURCE, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
media_player.ATTR_INPUT_SOURCE: source
|
2019-03-06 04:00:53 +00:00
|
|
|
}, blocking=True, context=data.context)
|
2019-03-30 03:51:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
@register_trait
|
|
|
|
class OpenCloseTrait(_Trait):
|
|
|
|
"""Trait to open and close a cover.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/openclose
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_OPENCLOSE
|
|
|
|
commands = [
|
|
|
|
COMMAND_OPENCLOSE
|
|
|
|
]
|
|
|
|
|
2019-04-28 19:09:20 +00:00
|
|
|
override_position = None
|
|
|
|
|
2019-03-30 03:51:47 +00:00
|
|
|
@staticmethod
|
2019-04-03 17:20:56 +00:00
|
|
|
def supported(domain, features, device_class):
|
2019-03-30 03:51:47 +00:00
|
|
|
"""Test if state is supported."""
|
2019-04-03 17:20:56 +00:00
|
|
|
if domain == cover.DOMAIN:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return domain == binary_sensor.DOMAIN and device_class in (
|
|
|
|
binary_sensor.DEVICE_CLASS_DOOR,
|
|
|
|
binary_sensor.DEVICE_CLASS_GARAGE_DOOR,
|
|
|
|
binary_sensor.DEVICE_CLASS_LOCK,
|
|
|
|
binary_sensor.DEVICE_CLASS_OPENING,
|
|
|
|
binary_sensor.DEVICE_CLASS_WINDOW,
|
|
|
|
)
|
2019-03-30 03:51:47 +00:00
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return opening direction."""
|
2019-04-28 19:09:20 +00:00
|
|
|
response = {}
|
2019-04-03 17:20:56 +00:00
|
|
|
if self.state.domain == binary_sensor.DOMAIN:
|
2019-04-28 19:09:20 +00:00
|
|
|
response['queryOnlyOpenClose'] = True
|
|
|
|
return response
|
2019-03-30 03:51:47 +00:00
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return state query attributes."""
|
|
|
|
domain = self.state.domain
|
|
|
|
response = {}
|
|
|
|
|
2019-04-28 19:09:20 +00:00
|
|
|
if self.override_position is not None:
|
|
|
|
response['openPercent'] = self.override_position
|
|
|
|
|
|
|
|
elif domain == cover.DOMAIN:
|
|
|
|
# When it's an assumed state, we will return that querying state
|
|
|
|
# is not supported.
|
2019-04-03 11:53:44 +00:00
|
|
|
if self.state.attributes.get(ATTR_ASSUMED_STATE):
|
2019-04-15 02:52:00 +00:00
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_NOT_SUPPORTED,
|
|
|
|
'Querying state is not supported')
|
2019-04-03 11:53:44 +00:00
|
|
|
|
2019-04-15 02:52:00 +00:00
|
|
|
if self.state.state == STATE_UNKNOWN:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_NOT_SUPPORTED,
|
|
|
|
'Querying state is not supported')
|
|
|
|
|
2019-04-28 19:09:20 +00:00
|
|
|
position = self.override_position or self.state.attributes.get(
|
2019-04-15 02:52:00 +00:00
|
|
|
cover.ATTR_CURRENT_POSITION
|
|
|
|
)
|
|
|
|
|
|
|
|
if position is not None:
|
|
|
|
response['openPercent'] = position
|
|
|
|
elif self.state.state != cover.STATE_CLOSED:
|
|
|
|
response['openPercent'] = 100
|
|
|
|
else:
|
|
|
|
response['openPercent'] = 0
|
2019-03-30 03:51:47 +00:00
|
|
|
|
2019-04-03 17:20:56 +00:00
|
|
|
elif domain == binary_sensor.DOMAIN:
|
|
|
|
if self.state.state == STATE_ON:
|
|
|
|
response['openPercent'] = 100
|
|
|
|
else:
|
|
|
|
response['openPercent'] = 0
|
|
|
|
|
2019-03-30 03:51:47 +00:00
|
|
|
return response
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
async def execute(self, command, data, params, challenge):
|
2019-03-30 03:51:47 +00:00
|
|
|
"""Execute an Open, close, Set position command."""
|
|
|
|
domain = self.state.domain
|
|
|
|
|
|
|
|
if domain == cover.DOMAIN:
|
2019-05-08 14:55:30 +00:00
|
|
|
svc_params = {ATTR_ENTITY_ID: self.state.entity_id}
|
2019-04-19 21:50:21 +00:00
|
|
|
|
2019-04-15 02:52:00 +00:00
|
|
|
if params['openPercent'] == 0:
|
2019-05-08 14:55:30 +00:00
|
|
|
service = cover.SERVICE_CLOSE_COVER
|
|
|
|
should_verify = False
|
2019-04-15 02:52:00 +00:00
|
|
|
elif params['openPercent'] == 100:
|
2019-05-08 14:55:30 +00:00
|
|
|
service = cover.SERVICE_OPEN_COVER
|
|
|
|
should_verify = True
|
2019-04-28 19:09:20 +00:00
|
|
|
elif (self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) &
|
|
|
|
cover.SUPPORT_SET_POSITION):
|
2019-05-08 14:55:30 +00:00
|
|
|
service = cover.SERVICE_SET_COVER_POSITION
|
|
|
|
should_verify = True
|
|
|
|
svc_params[cover.ATTR_POSITION] = params['openPercent']
|
2019-03-30 03:51:47 +00:00
|
|
|
else:
|
2019-04-15 02:52:00 +00:00
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_FUNCTION_NOT_SUPPORTED,
|
|
|
|
'Setting a position is not supported')
|
2019-04-19 21:50:21 +00:00
|
|
|
|
2019-05-08 14:55:30 +00:00
|
|
|
if (should_verify and
|
|
|
|
self.state.attributes.get(ATTR_DEVICE_CLASS)
|
|
|
|
in (cover.DEVICE_CLASS_DOOR,
|
|
|
|
cover.DEVICE_CLASS_GARAGE)):
|
|
|
|
_verify_pin_challenge(data, challenge)
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
cover.DOMAIN, service, svc_params,
|
|
|
|
blocking=True, context=data.context)
|
|
|
|
|
2019-04-28 19:09:20 +00:00
|
|
|
if (self.state.attributes.get(ATTR_ASSUMED_STATE) or
|
|
|
|
self.state.state == STATE_UNKNOWN):
|
|
|
|
self.override_position = params['openPercent']
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
|
2019-04-24 16:08:41 +00:00
|
|
|
@register_trait
|
|
|
|
class VolumeTrait(_Trait):
|
|
|
|
"""Trait to control brightness of a device.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/traits/volume
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = TRAIT_VOLUME
|
|
|
|
commands = [
|
|
|
|
COMMAND_SET_VOLUME,
|
|
|
|
COMMAND_VOLUME_RELATIVE,
|
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def supported(domain, features, device_class):
|
|
|
|
"""Test if state is supported."""
|
|
|
|
if domain == media_player.DOMAIN:
|
|
|
|
return features & media_player.SUPPORT_VOLUME_SET
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def sync_attributes(self):
|
|
|
|
"""Return brightness attributes for a sync request."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def query_attributes(self):
|
|
|
|
"""Return brightness query attributes."""
|
|
|
|
response = {}
|
|
|
|
|
|
|
|
level = self.state.attributes.get(
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
|
|
|
muted = self.state.attributes.get(
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_MUTED)
|
|
|
|
if level is not None:
|
|
|
|
# Convert 0.0-1.0 to 0-100
|
|
|
|
response['currentVolume'] = int(level * 100)
|
|
|
|
response['isMuted'] = bool(muted)
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
async def _execute_set_volume(self, data, params):
|
|
|
|
level = params['volumeLevel']
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
media_player.DOMAIN,
|
|
|
|
media_player.SERVICE_VOLUME_SET, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL:
|
|
|
|
level / 100
|
|
|
|
}, blocking=True, context=data.context)
|
|
|
|
|
|
|
|
async def _execute_volume_relative(self, data, params):
|
|
|
|
# This could also support up/down commands using relativeSteps
|
|
|
|
relative = params['volumeRelativeLevel']
|
|
|
|
current = self.state.attributes.get(
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
|
|
|
|
|
|
|
await self.hass.services.async_call(
|
|
|
|
media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, {
|
|
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL:
|
|
|
|
current + relative / 100
|
|
|
|
}, blocking=True, context=data.context)
|
|
|
|
|
|
|
|
async def execute(self, command, data, params, challenge):
|
|
|
|
"""Execute a brightness command."""
|
|
|
|
if command == COMMAND_SET_VOLUME:
|
|
|
|
await self._execute_set_volume(data, params)
|
|
|
|
elif command == COMMAND_VOLUME_RELATIVE:
|
|
|
|
await self._execute_volume_relative(data, params)
|
|
|
|
else:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_NOT_SUPPORTED, 'Command not supported')
|
|
|
|
|
|
|
|
|
2019-04-19 21:50:21 +00:00
|
|
|
def _verify_pin_challenge(data, challenge):
|
|
|
|
"""Verify a pin challenge."""
|
|
|
|
if not data.config.secure_devices_pin:
|
|
|
|
raise SmartHomeError(
|
|
|
|
ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up')
|
|
|
|
|
|
|
|
if not challenge:
|
|
|
|
raise ChallengeNeeded(CHALLENGE_PIN_NEEDED)
|
|
|
|
|
|
|
|
pin = challenge.get('pin')
|
|
|
|
|
|
|
|
if pin != data.config.secure_devices_pin:
|
|
|
|
raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED)
|
|
|
|
|
|
|
|
|
|
|
|
def _verify_ack_challenge(data, challenge):
|
|
|
|
"""Verify a pin challenge."""
|
|
|
|
if not challenge or not challenge.get('ack'):
|
|
|
|
raise ChallengeNeeded(CHALLENGE_ACK_NEEDED)
|