mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
commit
0e16f7f307
@ -145,6 +145,9 @@ omit =
|
|||||||
homeassistant/components/maxcube.py
|
homeassistant/components/maxcube.py
|
||||||
homeassistant/components/*/maxcube.py
|
homeassistant/components/*/maxcube.py
|
||||||
|
|
||||||
|
homeassistant/components/mercedesme.py
|
||||||
|
homeassistant/components/*/mercedesme.py
|
||||||
|
|
||||||
homeassistant/components/mochad.py
|
homeassistant/components/mochad.py
|
||||||
homeassistant/components/*/mochad.py
|
homeassistant/components/*/mochad.py
|
||||||
|
|
||||||
@ -377,6 +380,7 @@ omit =
|
|||||||
homeassistant/components/fan/xiaomi_miio.py
|
homeassistant/components/fan/xiaomi_miio.py
|
||||||
homeassistant/components/feedreader.py
|
homeassistant/components/feedreader.py
|
||||||
homeassistant/components/foursquare.py
|
homeassistant/components/foursquare.py
|
||||||
|
homeassistant/components/goalfeed.py
|
||||||
homeassistant/components/ifttt.py
|
homeassistant/components/ifttt.py
|
||||||
homeassistant/components/image_processing/dlib_face_detect.py
|
homeassistant/components/image_processing/dlib_face_detect.py
|
||||||
homeassistant/components/image_processing/dlib_face_identify.py
|
homeassistant/components/image_processing/dlib_face_identify.py
|
||||||
@ -435,6 +439,7 @@ omit =
|
|||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/lg_netcast.py
|
homeassistant/components/media_player/lg_netcast.py
|
||||||
homeassistant/components/media_player/liveboxplaytv.py
|
homeassistant/components/media_player/liveboxplaytv.py
|
||||||
|
homeassistant/components/media_player/mediaroom.py
|
||||||
homeassistant/components/media_player/mpchc.py
|
homeassistant/components/media_player/mpchc.py
|
||||||
homeassistant/components/media_player/mpd.py
|
homeassistant/components/media_player/mpd.py
|
||||||
homeassistant/components/media_player/nad.py
|
homeassistant/components/media_player/nad.py
|
||||||
@ -449,7 +454,6 @@ omit =
|
|||||||
homeassistant/components/media_player/roku.py
|
homeassistant/components/media_player/roku.py
|
||||||
homeassistant/components/media_player/russound_rio.py
|
homeassistant/components/media_player/russound_rio.py
|
||||||
homeassistant/components/media_player/russound_rnet.py
|
homeassistant/components/media_player/russound_rnet.py
|
||||||
homeassistant/components/media_player/samsungtv.py
|
|
||||||
homeassistant/components/media_player/snapcast.py
|
homeassistant/components/media_player/snapcast.py
|
||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
homeassistant/components/media_player/spotify.py
|
homeassistant/components/media_player/spotify.py
|
||||||
@ -506,6 +510,7 @@ omit =
|
|||||||
homeassistant/components/remember_the_milk/__init__.py
|
homeassistant/components/remember_the_milk/__init__.py
|
||||||
homeassistant/components/remote/harmony.py
|
homeassistant/components/remote/harmony.py
|
||||||
homeassistant/components/remote/itach.py
|
homeassistant/components/remote/itach.py
|
||||||
|
homeassistant/components/remote/xiaomi_miio.py
|
||||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||||
homeassistant/components/scene/lifx_cloud.py
|
homeassistant/components/scene/lifx_cloud.py
|
||||||
homeassistant/components/sensor/airvisual.py
|
homeassistant/components/sensor/airvisual.py
|
||||||
@ -592,6 +597,7 @@ omit =
|
|||||||
homeassistant/components/sensor/pi_hole.py
|
homeassistant/components/sensor/pi_hole.py
|
||||||
homeassistant/components/sensor/plex.py
|
homeassistant/components/sensor/plex.py
|
||||||
homeassistant/components/sensor/pocketcasts.py
|
homeassistant/components/sensor/pocketcasts.py
|
||||||
|
homeassistant/components/sensor/pollen.py
|
||||||
homeassistant/components/sensor/pushbullet.py
|
homeassistant/components/sensor/pushbullet.py
|
||||||
homeassistant/components/sensor/pvoutput.py
|
homeassistant/components/sensor/pvoutput.py
|
||||||
homeassistant/components/sensor/pyload.py
|
homeassistant/components/sensor/pyload.py
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -16,7 +16,9 @@ Icon
|
|||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
.idea
|
.idea
|
||||||
|
*.iml
|
||||||
|
|
||||||
# pytest
|
# pytest
|
||||||
.cache
|
.cache
|
||||||
@ -98,3 +100,6 @@ desktop.ini
|
|||||||
/home-assistant.pyproj
|
/home-assistant.pyproj
|
||||||
/home-assistant.sln
|
/home-assistant.sln
|
||||||
/.vs/*
|
/.vs/*
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
/.mypy_cache/*
|
||||||
|
@ -41,7 +41,7 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave
|
|||||||
|
|
||||||
homeassistant/components/hassio.py @home-assistant/hassio
|
homeassistant/components/hassio.py @home-assistant/hassio
|
||||||
|
|
||||||
# Indiviudal components
|
# Individual components
|
||||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||||
homeassistant/components/camera/yi.py @bachya
|
homeassistant/components/camera/yi.py @bachya
|
||||||
homeassistant/components/climate/ephember.py @ttroy50
|
homeassistant/components/climate/ephember.py @ttroy50
|
||||||
@ -61,6 +61,7 @@ homeassistant/components/sensor/airvisual.py @bachya
|
|||||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||||
|
homeassistant/components/sensor/pollen.py @bachya
|
||||||
homeassistant/components/sensor/sytadin.py @gautric
|
homeassistant/components/sensor/sytadin.py @gautric
|
||||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||||
homeassistant/components/sensor/waqi.py @andrey-git
|
homeassistant/components/sensor/waqi.py @andrey-git
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
<li><a href="https://home-assistant.io/">Homepage</a></li>
|
<li><a href="https://home-assistant.io/">Homepage</a></li>
|
||||||
<li><a href="https://community.home-assistant.io">Community Forums</a></li>
|
<li><a href="https://community.home-assistant.io">Community Forums</a></li>
|
||||||
<li><a href="https://github.com/home-assistant/home-assistant">GitHub</a></li>
|
<li><a href="https://github.com/home-assistant/home-assistant">GitHub</a></li>
|
||||||
<li><a href="https://gitter.im/home-assistant/home-assistant">Gitter</a></li>
|
<li><a href="https://discord.gg/c5DvZ4e">Discord</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -182,7 +182,8 @@ def check_pid(pid_file: str) -> None:
|
|||||||
"""Check that Home Assistant is not already running."""
|
"""Check that Home Assistant is not already running."""
|
||||||
# Check pid file
|
# Check pid file
|
||||||
try:
|
try:
|
||||||
pid = int(open(pid_file, 'r').readline())
|
with open(pid_file, 'r') as file:
|
||||||
|
pid = int(file.readline())
|
||||||
except IOError:
|
except IOError:
|
||||||
# PID File does not exist
|
# PID File does not exist
|
||||||
return
|
return
|
||||||
@ -204,7 +205,8 @@ def write_pid(pid_file: str) -> None:
|
|||||||
"""Create a PID File."""
|
"""Create a PID File."""
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
open(pid_file, 'w').write(str(pid))
|
with open(pid_file, 'w') as file:
|
||||||
|
file.write(str(pid))
|
||||||
except IOError:
|
except IOError:
|
||||||
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
|
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -133,7 +133,7 @@ def async_setup(hass, config):
|
|||||||
# have been processed. If a service does not exist it causes a 10
|
# have been processed. If a service does not exist it causes a 10
|
||||||
# second delay while we're blocking waiting for a response.
|
# second delay while we're blocking waiting for a response.
|
||||||
# But services can be registered on other HA instances that are
|
# But services can be registered on other HA instances that are
|
||||||
# listening to the bus too. So as a in between solution, we'll
|
# listening to the bus too. So as an in between solution, we'll
|
||||||
# block only if the service is defined in the current HA instance.
|
# block only if the service is defined in the current HA instance.
|
||||||
blocking = hass.services.has_service(domain, service.service)
|
blocking = hass.services.has_service(domain, service.service)
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ class Alert(ToggleEntity):
|
|||||||
yield from self.async_update_ha_state()
|
yield from self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_toggle(self):
|
def async_toggle(self, **kwargs):
|
||||||
"""Async toggle alert."""
|
"""Async toggle alert."""
|
||||||
if self._ack:
|
if self._ack:
|
||||||
return self.async_turn_on()
|
return self.async_turn_on()
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.const import (
|
|||||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||||
CONF_UNIT_OF_MEASUREMENT)
|
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -40,6 +40,7 @@ CONF_DESCRIPTION = 'description'
|
|||||||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||||
|
|
||||||
HANDLERS = Registry()
|
HANDLERS = Registry()
|
||||||
|
ENTITY_ADAPTERS = Registry()
|
||||||
|
|
||||||
|
|
||||||
class _DisplayCategory(object):
|
class _DisplayCategory(object):
|
||||||
@ -50,8 +51,8 @@ class _DisplayCategory(object):
|
|||||||
|
|
||||||
# Describes a combination of devices set to a specific state, when the
|
# Describes a combination of devices set to a specific state, when the
|
||||||
# state change must occur in a specific order. For example, a "watch
|
# state change must occur in a specific order. For example, a "watch
|
||||||
# Neflix" scene might require the: 1. TV to be powered on & 2. Input set to
|
# Netflix" scene might require the: 1. TV to be powered on & 2. Input set
|
||||||
# HDMI1. Applies to Scenes
|
# to HDMI1. Applies to Scenes
|
||||||
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
||||||
|
|
||||||
# Indicates media devices with video or photo capabilities.
|
# Indicates media devices with video or photo capabilities.
|
||||||
@ -133,10 +134,36 @@ def _capability(interface,
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class _EntityCapabilities(object):
|
class _UnsupportedInterface(Exception):
|
||||||
|
"""This entity does not support the requested Smart Home API interface."""
|
||||||
|
|
||||||
|
|
||||||
|
class _UnsupportedProperty(Exception):
|
||||||
|
"""This entity does not support the requested Smart Home API property."""
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaEntity(object):
|
||||||
|
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||||
|
|
||||||
|
The API handlers should manipulate entities only through this interface.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, config, entity):
|
def __init__(self, config, entity):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
|
self.entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||||
|
|
||||||
|
def friendly_name(self):
|
||||||
|
"""Return the Alexa API friendly name."""
|
||||||
|
return self.entity_conf.get(CONF_NAME, self.entity.name)
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
"""Return the Alexa API description."""
|
||||||
|
return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id)
|
||||||
|
|
||||||
|
def entity_id(self):
|
||||||
|
"""Return the Alexa API entity id."""
|
||||||
|
return self.entity.entity_id.replace('.', '#')
|
||||||
|
|
||||||
def display_categories(self):
|
def display_categories(self):
|
||||||
"""Return a list of display categories."""
|
"""Return a list of display categories."""
|
||||||
@ -154,17 +181,217 @@ class _EntityCapabilities(object):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def capabilities(self):
|
def get_interface(self, capability):
|
||||||
"""Return a list of supported capabilities.
|
"""Return the given _AlexaInterface.
|
||||||
|
|
||||||
If the returned list is empty, the entity will not be discovered.
|
Raises _UnsupportedInterface.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
You might find _capability() useful.
|
def interfaces(self):
|
||||||
|
"""Return a list of supported interfaces.
|
||||||
|
|
||||||
|
Used for discovery. The list should contain _AlexaInterface instances.
|
||||||
|
If the list is empty, this entity will not be discovered.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class _GenericCapabilities(_EntityCapabilities):
|
class _AlexaInterface(object):
|
||||||
|
def __init__(self, entity):
|
||||||
|
self.entity = entity
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
"""Return the Alexa API name of this interface."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def properties_supported():
|
||||||
|
"""Return what properties this entity supports."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def properties_proactively_reported():
|
||||||
|
"""Return True if properties asynchronously reported."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def properties_retrievable():
|
||||||
|
"""Return True if properties can be retrieved."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_property(name):
|
||||||
|
"""Read and return a property.
|
||||||
|
|
||||||
|
Return value should be a dict, or raise _UnsupportedProperty.
|
||||||
|
|
||||||
|
Properties can also have a timeOfSample and uncertaintyInMilliseconds,
|
||||||
|
but returning those metadata is not yet implemented.
|
||||||
|
"""
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supports_deactivation():
|
||||||
|
"""Applicable only to scenes."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def serialize_discovery(self):
|
||||||
|
"""Serialize according to the Discovery API."""
|
||||||
|
result = {
|
||||||
|
'type': 'AlexaInterface',
|
||||||
|
'interface': self.name(),
|
||||||
|
'version': '3',
|
||||||
|
'properties': {
|
||||||
|
'supported': self.properties_supported(),
|
||||||
|
'proactivelyReported': self.properties_proactively_reported(),
|
||||||
|
'retrievable': self.properties_retrievable(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# pylint: disable=assignment-from-none
|
||||||
|
supports_deactivation = self.supports_deactivation()
|
||||||
|
if supports_deactivation is not None:
|
||||||
|
result['supportsDeactivation'] = supports_deactivation
|
||||||
|
return result
|
||||||
|
|
||||||
|
def serialize_properties(self):
|
||||||
|
"""Return properties serialized for an API response."""
|
||||||
|
for prop in self.properties_supported():
|
||||||
|
prop_name = prop['name']
|
||||||
|
yield {
|
||||||
|
'name': prop_name,
|
||||||
|
'namespace': self.name(),
|
||||||
|
'value': self.get_property(prop_name),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaPowerController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.PowerController'
|
||||||
|
|
||||||
|
def properties_supported(self):
|
||||||
|
return [{'name': 'powerState'}]
|
||||||
|
|
||||||
|
def properties_retrievable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
if name != 'powerState':
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
if self.entity.state == STATE_ON:
|
||||||
|
return 'ON'
|
||||||
|
return 'OFF'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaLockController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.LockController'
|
||||||
|
|
||||||
|
def properties_supported(self):
|
||||||
|
return [{'name': 'lockState'}]
|
||||||
|
|
||||||
|
def properties_retrievable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
if name != 'lockState':
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
if self.entity.state == STATE_LOCKED:
|
||||||
|
return 'LOCKED'
|
||||||
|
elif self.entity.state == STATE_UNLOCKED:
|
||||||
|
return 'UNLOCKED'
|
||||||
|
return 'JAMMED'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaSceneController(_AlexaInterface):
|
||||||
|
def __init__(self, entity, supports_deactivation):
|
||||||
|
_AlexaInterface.__init__(self, entity)
|
||||||
|
self.supports_deactivation = lambda: supports_deactivation
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.SceneController'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaBrightnessController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.BrightnessController'
|
||||||
|
|
||||||
|
def properties_supported(self):
|
||||||
|
return [{'name': 'brightness'}]
|
||||||
|
|
||||||
|
def properties_retrievable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
if name != 'brightness':
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
return round(self.entity.attributes['brightness'] / 255.0 * 100)
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaColorController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.ColorController'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaColorTemperatureController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.ColorTemperatureController'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaPercentageController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.PercentageController'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaSpeaker(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.Speaker'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaStepSpeaker(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.StepSpeaker'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaPlaybackController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.PlaybackController'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaInputController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.InputController'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaTemperatureSensor(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.TemperatureSensor'
|
||||||
|
|
||||||
|
def properties_supported(self):
|
||||||
|
return [{'name': 'temperature'}]
|
||||||
|
|
||||||
|
def properties_retrievable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
if name != 'temperature':
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
return {
|
||||||
|
'value': float(self.entity.state),
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(alert.DOMAIN)
|
||||||
|
@ENTITY_ADAPTERS.register(automation.DOMAIN)
|
||||||
|
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
|
||||||
|
class _GenericCapabilities(_AlexaEntity):
|
||||||
"""A generic, on/off device.
|
"""A generic, on/off device.
|
||||||
|
|
||||||
The choice of last resort.
|
The choice of last resort.
|
||||||
@ -173,78 +400,87 @@ class _GenericCapabilities(_EntityCapabilities):
|
|||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.OTHER]
|
return [_DisplayCategory.OTHER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.PowerController')]
|
return [_AlexaPowerController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
class _SwitchCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(switch.DOMAIN)
|
||||||
|
class _SwitchCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SWITCH]
|
return [_DisplayCategory.SWITCH]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.PowerController')]
|
return [_AlexaPowerController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
class _CoverCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||||
|
class _CoverCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.DOOR]
|
return [_DisplayCategory.DOOR]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & cover.SUPPORT_SET_POSITION:
|
if supported & cover.SUPPORT_SET_POSITION:
|
||||||
capabilities.append(_capability('Alexa.PercentageController'))
|
yield _AlexaPercentageController(self.entity)
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _LightCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
||||||
|
class _LightCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.LIGHT]
|
return [_DisplayCategory.LIGHT]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
|
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & light.SUPPORT_BRIGHTNESS:
|
if supported & light.SUPPORT_BRIGHTNESS:
|
||||||
capabilities.append(_capability('Alexa.BrightnessController'))
|
yield _AlexaBrightnessController(self.entity)
|
||||||
if supported & light.SUPPORT_RGB_COLOR:
|
if supported & light.SUPPORT_RGB_COLOR:
|
||||||
capabilities.append(_capability('Alexa.ColorController'))
|
yield _AlexaColorController(self.entity)
|
||||||
if supported & light.SUPPORT_XY_COLOR:
|
if supported & light.SUPPORT_XY_COLOR:
|
||||||
capabilities.append(_capability('Alexa.ColorController'))
|
yield _AlexaColorController(self.entity)
|
||||||
if supported & light.SUPPORT_COLOR_TEMP:
|
if supported & light.SUPPORT_COLOR_TEMP:
|
||||||
capabilities.append(
|
yield _AlexaColorTemperatureController(self.entity)
|
||||||
_capability('Alexa.ColorTemperatureController'))
|
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _FanCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
||||||
|
class _FanCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.OTHER]
|
return [_DisplayCategory.OTHER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & fan.SUPPORT_SET_SPEED:
|
if supported & fan.SUPPORT_SET_SPEED:
|
||||||
capabilities.append(_capability('Alexa.PercentageController'))
|
yield _AlexaPercentageController(self.entity)
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _LockCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||||
|
class _LockCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SMARTLOCK]
|
return [_DisplayCategory.SMARTLOCK]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.LockController')]
|
return [_AlexaLockController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
class _MediaPlayerCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||||
|
class _MediaPlayerCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.TV]
|
return [_DisplayCategory.TV]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
|
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & media_player.SUPPORT_VOLUME_SET:
|
if supported & media_player.SUPPORT_VOLUME_SET:
|
||||||
capabilities.append(_capability('Alexa.Speaker'))
|
yield _AlexaSpeaker(self.entity)
|
||||||
|
|
||||||
|
step_volume_features = (media_player.SUPPORT_VOLUME_MUTE |
|
||||||
|
media_player.SUPPORT_VOLUME_STEP)
|
||||||
|
if supported & step_volume_features:
|
||||||
|
yield _AlexaStepSpeaker(self.entity)
|
||||||
|
|
||||||
playback_features = (media_player.SUPPORT_PLAY |
|
playback_features = (media_player.SUPPORT_PLAY |
|
||||||
media_player.SUPPORT_PAUSE |
|
media_player.SUPPORT_PAUSE |
|
||||||
@ -252,89 +488,62 @@ class _MediaPlayerCapabilities(_EntityCapabilities):
|
|||||||
media_player.SUPPORT_NEXT_TRACK |
|
media_player.SUPPORT_NEXT_TRACK |
|
||||||
media_player.SUPPORT_PREVIOUS_TRACK)
|
media_player.SUPPORT_PREVIOUS_TRACK)
|
||||||
if supported & playback_features:
|
if supported & playback_features:
|
||||||
capabilities.append(_capability('Alexa.PlaybackController'))
|
yield _AlexaPlaybackController(self.entity)
|
||||||
|
|
||||||
return capabilities
|
if supported & media_player.SUPPORT_SELECT_SOURCE:
|
||||||
|
yield _AlexaInputController(self.entity)
|
||||||
|
|
||||||
|
|
||||||
class _SceneCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(scene.DOMAIN)
|
||||||
|
class _SceneCapabilities(_AlexaEntity):
|
||||||
|
def description(self):
|
||||||
|
# Required description as per Amazon Scene docs
|
||||||
|
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||||
|
return scene_fmt.format(_AlexaEntity.description(self))
|
||||||
|
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SCENE_TRIGGER]
|
return [_DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.SceneController')]
|
return [_AlexaSceneController(self.entity,
|
||||||
|
supports_deactivation=False)]
|
||||||
|
|
||||||
|
|
||||||
class _ScriptCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
||||||
|
class _ScriptCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
||||||
return [_capability('Alexa.SceneController',
|
return [_AlexaSceneController(self.entity,
|
||||||
supports_deactivation=can_cancel)]
|
supports_deactivation=can_cancel)]
|
||||||
|
|
||||||
|
|
||||||
class _GroupCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
||||||
|
class _GroupCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SCENE_TRIGGER]
|
return [_DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.SceneController',
|
return [_AlexaSceneController(self.entity,
|
||||||
supports_deactivation=True)]
|
supports_deactivation=True)]
|
||||||
|
|
||||||
|
|
||||||
class _SensorCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||||
|
class _SensorCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
# although there are other kinds of sensors, all but temperature
|
# although there are other kinds of sensors, all but temperature
|
||||||
# sensors are currently ignored.
|
# sensors are currently ignored.
|
||||||
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = []
|
|
||||||
|
|
||||||
attrs = self.entity.attributes
|
attrs = self.entity.attributes
|
||||||
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
):
|
):
|
||||||
capabilities.append(_capability(
|
yield _AlexaTemperatureSensor(self.entity)
|
||||||
'Alexa.TemperatureSensor',
|
|
||||||
retrievable=True,
|
|
||||||
properties_supported=[{'name': 'temperature'}]))
|
|
||||||
|
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _UnknownEntityDomainError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _capabilities_for_entity(config, entity):
|
|
||||||
"""Return an _EntityCapabilities appropriate for given entity.
|
|
||||||
|
|
||||||
raises _UnknownEntityDomainError if the given domain is unsupported.
|
|
||||||
"""
|
|
||||||
if entity.domain not in _CAPABILITIES_FOR_DOMAIN:
|
|
||||||
raise _UnknownEntityDomainError()
|
|
||||||
return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity)
|
|
||||||
|
|
||||||
|
|
||||||
_CAPABILITIES_FOR_DOMAIN = {
|
|
||||||
alert.DOMAIN: _GenericCapabilities,
|
|
||||||
automation.DOMAIN: _GenericCapabilities,
|
|
||||||
cover.DOMAIN: _CoverCapabilities,
|
|
||||||
fan.DOMAIN: _FanCapabilities,
|
|
||||||
group.DOMAIN: _GroupCapabilities,
|
|
||||||
input_boolean.DOMAIN: _GenericCapabilities,
|
|
||||||
light.DOMAIN: _LightCapabilities,
|
|
||||||
lock.DOMAIN: _LockCapabilities,
|
|
||||||
media_player.DOMAIN: _MediaPlayerCapabilities,
|
|
||||||
scene.DOMAIN: _SceneCapabilities,
|
|
||||||
script.DOMAIN: _ScriptCapabilities,
|
|
||||||
switch.DOMAIN: _SwitchCapabilities,
|
|
||||||
sensor.DOMAIN: _SensorCapabilities,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _Cause(object):
|
class _Cause(object):
|
||||||
@ -468,7 +677,7 @@ def api_message(request,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# If a correlation token exsits, add it to header / Need by Async requests
|
# If a correlation token exists, add it to header / Need by Async requests
|
||||||
token = request[API_HEADER].get('correlationToken')
|
token = request[API_HEADER].get('correlationToken')
|
||||||
if token:
|
if token:
|
||||||
response[API_EVENT][API_HEADER]['correlationToken'] = token
|
response[API_EVENT][API_HEADER]['correlationToken'] = token
|
||||||
@ -511,36 +720,26 @@ def async_api_discovery(hass, config, request):
|
|||||||
entity.entity_id)
|
entity.entity_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
if entity.domain not in ENTITY_ADAPTERS:
|
||||||
entity_capabilities = _capabilities_for_entity(config, entity)
|
|
||||||
except _UnknownEntityDomainError:
|
|
||||||
continue
|
continue
|
||||||
|
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||||
entity_conf = config.entity_config.get(entity.entity_id, {})
|
|
||||||
|
|
||||||
friendly_name = entity_conf.get(CONF_NAME, entity.name)
|
|
||||||
description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
|
|
||||||
|
|
||||||
# Required description as per Amazon Scene docs
|
|
||||||
if entity.domain == scene.DOMAIN:
|
|
||||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
|
||||||
description = scene_fmt.format(description)
|
|
||||||
|
|
||||||
endpoint = {
|
endpoint = {
|
||||||
'displayCategories': entity_capabilities.display_categories(),
|
'displayCategories': alexa_entity.display_categories(),
|
||||||
'additionalApplianceDetails': {},
|
'additionalApplianceDetails': {},
|
||||||
'endpointId': entity.entity_id.replace('.', '#'),
|
'endpointId': alexa_entity.entity_id(),
|
||||||
'friendlyName': friendly_name,
|
'friendlyName': alexa_entity.friendly_name(),
|
||||||
'description': description,
|
'description': alexa_entity.description(),
|
||||||
'manufacturerName': 'Home Assistant',
|
'manufacturerName': 'Home Assistant',
|
||||||
}
|
}
|
||||||
|
|
||||||
alexa_capabilities = entity_capabilities.capabilities()
|
endpoint['capabilities'] = [
|
||||||
if not alexa_capabilities:
|
i.serialize_discovery() for i in alexa_entity.interfaces()]
|
||||||
|
|
||||||
|
if not endpoint['capabilities']:
|
||||||
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
||||||
entity.entity_id)
|
entity.entity_id)
|
||||||
continue
|
continue
|
||||||
endpoint['capabilities'] = alexa_capabilities
|
|
||||||
discovery_endpoints.append(endpoint)
|
discovery_endpoints.append(endpoint)
|
||||||
|
|
||||||
return api_message(
|
return api_message(
|
||||||
@ -624,7 +823,7 @@ def async_api_set_brightness(hass, config, request, entity):
|
|||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_adjust_brightness(hass, config, request, entity):
|
def async_api_adjust_brightness(hass, config, request, entity):
|
||||||
"""Process a adjust brightness request."""
|
"""Process an adjust brightness request."""
|
||||||
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
|
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
|
||||||
|
|
||||||
# read current state
|
# read current state
|
||||||
@ -812,7 +1011,7 @@ def async_api_set_percentage(hass, config, request, entity):
|
|||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_adjust_percentage(hass, config, request, entity):
|
def async_api_adjust_percentage(hass, config, request, entity):
|
||||||
"""Process a adjust percentage request."""
|
"""Process an adjust percentage request."""
|
||||||
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
|
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
|
||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
@ -873,7 +1072,7 @@ def async_api_lock(hass, config, request, entity):
|
|||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_unlock(hass, config, request, entity):
|
def async_api_unlock(hass, config, request, entity):
|
||||||
"""Process a unlock request."""
|
"""Process an unlock request."""
|
||||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||||
ATTR_ENTITY_ID: entity.entity_id
|
ATTR_ENTITY_ID: entity.entity_id
|
||||||
}, blocking=False)
|
}, blocking=False)
|
||||||
@ -900,11 +1099,46 @@ def async_api_set_volume(hass, config, request, entity):
|
|||||||
return api_message(request)
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
|
||||||
|
@extract_entity
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_api_select_input(hass, config, request, entity):
|
||||||
|
"""Process a set input request."""
|
||||||
|
media_input = request[API_PAYLOAD]['input']
|
||||||
|
|
||||||
|
# attempt to map the ALL UPPERCASE payload name to a source
|
||||||
|
source_list = entity.attributes[media_player.ATTR_INPUT_SOURCE_LIST] or []
|
||||||
|
for source in source_list:
|
||||||
|
# response will always be space separated, so format the source in the
|
||||||
|
# most likely way to find a match
|
||||||
|
formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
|
||||||
|
if formatted_source in media_input.lower():
|
||||||
|
media_input = source
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
msg = 'failed to map input {} to a media source on {}'.format(
|
||||||
|
media_input, entity.entity_id)
|
||||||
|
_LOGGER.error(msg)
|
||||||
|
return api_error(
|
||||||
|
request, error_type='INVALID_VALUE', error_message=msg)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
media_player.ATTR_INPUT_SOURCE: media_input,
|
||||||
|
}
|
||||||
|
|
||||||
|
yield from hass.services.async_call(
|
||||||
|
entity.domain, media_player.SERVICE_SELECT_SOURCE,
|
||||||
|
data, blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_adjust_volume(hass, config, request, entity):
|
def async_api_adjust_volume(hass, config, request, entity):
|
||||||
"""Process a adjust volume request."""
|
"""Process an adjust volume request."""
|
||||||
volume_delta = int(request[API_PAYLOAD]['volume'])
|
volume_delta = int(request[API_PAYLOAD]['volume'])
|
||||||
|
|
||||||
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||||
@ -929,6 +1163,30 @@ def async_api_adjust_volume(hass, config, request, entity):
|
|||||||
return api_message(request)
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
|
||||||
|
@extract_entity
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_api_adjust_volume_step(hass, config, request, entity):
|
||||||
|
"""Process an adjust volume step request."""
|
||||||
|
volume_step = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
|
||||||
|
|
||||||
|
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||||
|
|
||||||
|
volume = current_level + volume_step
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||||
|
}
|
||||||
|
|
||||||
|
yield from hass.services.async_call(
|
||||||
|
entity.domain, media_player.SERVICE_VOLUME_SET,
|
||||||
|
data, blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
|
||||||
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -1033,18 +1291,13 @@ def async_api_previous(hass, config, request, entity):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_reportstate(hass, config, request, entity):
|
def async_api_reportstate(hass, config, request, entity):
|
||||||
"""Process a ReportState request."""
|
"""Process a ReportState request."""
|
||||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||||
temp_property = {
|
properties = []
|
||||||
'namespace': 'Alexa.TemperatureSensor',
|
for interface in alexa_entity.interfaces():
|
||||||
'name': 'temperature',
|
properties.extend(interface.serialize_properties())
|
||||||
'value': {
|
|
||||||
'value': float(entity.state),
|
|
||||||
'scale': API_TEMP_UNITS[unit],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return api_message(
|
return api_message(
|
||||||
request,
|
request,
|
||||||
name='StateReport',
|
name='StateReport',
|
||||||
context={'properties': [temp_property]}
|
context={'properties': properties}
|
||||||
)
|
)
|
||||||
|
@ -251,7 +251,7 @@ class AndroidIPCamEntity(Entity):
|
|||||||
"""The Android device running IP Webcam."""
|
"""The Android device running IP Webcam."""
|
||||||
|
|
||||||
def __init__(self, host, ipcam):
|
def __init__(self, host, ipcam):
|
||||||
"""Initialize the data oject."""
|
"""Initialize the data object."""
|
||||||
self._host = host
|
self._host = host
|
||||||
self._ipcam = ipcam
|
self._ipcam = ipcam
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class APCUPSdData(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host, port):
|
def __init__(self, host, port):
|
||||||
"""Initialize the data oject."""
|
"""Initialize the data object."""
|
||||||
from apcaccess import status
|
from apcaccess import status
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
|
@ -47,7 +47,7 @@ def setup(hass, config):
|
|||||||
return False
|
return False
|
||||||
hass.data[DATA_ARLO] = arlo
|
hass.data[DATA_ARLO] = arlo
|
||||||
except (ConnectTimeout, HTTPError) as ex:
|
except (ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
|
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
'Error: {}<br />'
|
'Error: {}<br />'
|
||||||
'You will need to restart hass after fixing.'
|
'You will need to restart hass after fixing.'
|
||||||
|
@ -50,7 +50,6 @@ class BloomSkySensor(BinarySensorDevice):
|
|||||||
self._device_id = device['DeviceID']
|
self._device_id = device['DeviceID']
|
||||||
self._sensor_name = sensor_name
|
self._sensor_name = sensor_name
|
||||||
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
|
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
|
||||||
self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name)
|
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -58,11 +57,6 @@ class BloomSkySensor(BinarySensorDevice):
|
|||||||
"""Return the name of the BloomSky device and this sensor."""
|
"""Return the name of the BloomSky device and this sensor."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return the unique ID for this sensor."""
|
|
||||||
return self._unique_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||||
|
@ -65,6 +65,11 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return self._sensor.name
|
return self._sensor.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this sensor."""
|
||||||
|
return self._sensor.uniqueid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of the sensor."""
|
"""Return the class of the sensor."""
|
||||||
|
@ -50,11 +50,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||||||
"""Return the status of the sensor."""
|
"""Return the status of the sensor."""
|
||||||
return self._state == 'true'
|
return self._state == 'true'
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return the unique ID of this sensor."""
|
|
||||||
return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||||
|
@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the FFmpeg binary moition sensor."""
|
"""Set up the FFmpeg binary motion sensor."""
|
||||||
manager = hass.data[DATA_FFMPEG]
|
manager = hass.data[DATA_FFMPEG]
|
||||||
|
|
||||||
if not manager.async_run_test(config.get(CONF_INPUT)):
|
if not manager.async_run_test(config.get(CONF_INPUT)):
|
||||||
|
@ -118,7 +118,7 @@ class HikvisionData(object):
|
|||||||
"""Hikvision device event stream object."""
|
"""Hikvision device event stream object."""
|
||||||
|
|
||||||
def __init__(self, hass, url, port, name, username, password):
|
def __init__(self, hass, url, port, name, username, password):
|
||||||
"""Initialize the data oject."""
|
"""Initialize the data object."""
|
||||||
from pyhik.hikvision import HikCamera
|
from pyhik.hikvision import HikCamera
|
||||||
self._url = url
|
self._url = url
|
||||||
self._port = port
|
self._port = port
|
||||||
@ -212,7 +212,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return an unique ID."""
|
"""Return an unique ID."""
|
||||||
return '{}.{}'.format(self.__class__, self._id)
|
return self._id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
@ -59,5 +59,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
|||||||
self.node_device_type)
|
self.node_device_type)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update all Node data frome Hive."""
|
"""Update all Node data from Hive."""
|
||||||
self.session.core.update_data(self.node_id)
|
self.session.core.update_data(self.node_id)
|
||||||
|
@ -69,7 +69,8 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
||||||
sensor_type: str, inverting: bool, product: Element=None):
|
sensor_type: str, inverting: bool,
|
||||||
|
product: Element=None) -> None:
|
||||||
"""Initialize the IHC binary sensor."""
|
"""Initialize the IHC binary sensor."""
|
||||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||||
self._state = None
|
self._state = None
|
||||||
|
@ -83,5 +83,5 @@ class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
|||||||
@callback
|
@callback
|
||||||
def async_binarysensor_update(self, message):
|
def async_binarysensor_update(self, message):
|
||||||
"""Receive notification from transport that new data exists."""
|
"""Receive notification from transport that new data exists."""
|
||||||
_LOGGER.info("Received update calback from PLM for %s", self._address)
|
_LOGGER.info("Received update callback from PLM for %s", self._address)
|
||||||
self._hass.async_add_job(self.async_update_ha_state())
|
self._hass.async_add_job(self.async_update_ha_state())
|
||||||
|
@ -67,8 +67,8 @@ def setup_platform(hass, config: ConfigType,
|
|||||||
elif subnode_id == 2:
|
elif subnode_id == 2:
|
||||||
parent_device.add_negative_node(node)
|
parent_device.add_negative_node(node)
|
||||||
elif device_type == 'moisture':
|
elif device_type == 'moisture':
|
||||||
# Moisure nodes have a subnode 2, but we ignore it because it's
|
# Moisture nodes have a subnode 2, but we ignore it because
|
||||||
# just the inverse of the primary node.
|
# it's just the inverse of the primary node.
|
||||||
if subnode_id == 4:
|
if subnode_id == 4:
|
||||||
# Heartbeat node
|
# Heartbeat node
|
||||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||||
|
94
homeassistant/components/binary_sensor/mercedesme.py
Normal file
94
homeassistant/components/binary_sensor/mercedesme.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
Support for Mercedes cars with Mercedes ME.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.mercedesme/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||||
|
from homeassistant.components.mercedesme import (
|
||||||
|
DATA_MME, MercedesMeEntity, BINARY_SENSORS)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mercedesme']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the sensor platform."""
|
||||||
|
data = hass.data[DATA_MME].data
|
||||||
|
|
||||||
|
if not data.cars:
|
||||||
|
_LOGGER.error("No cars found. Check component log.")
|
||||||
|
return
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
for car in data.cars:
|
||||||
|
for key, value in sorted(BINARY_SENSORS.items()):
|
||||||
|
devices.append(MercedesMEBinarySensor(
|
||||||
|
data, key, value[0], car["vin"], None))
|
||||||
|
|
||||||
|
add_devices(devices, True)
|
||||||
|
|
||||||
|
|
||||||
|
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
|
||||||
|
"""Representation of a Sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return the state of the binary sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
if self._internal_name == "windowsClosed":
|
||||||
|
return {
|
||||||
|
"window_front_left": self._car["windowStatusFrontLeft"],
|
||||||
|
"window_front_right": self._car["windowStatusFrontRight"],
|
||||||
|
"window_rear_left": self._car["windowStatusRearLeft"],
|
||||||
|
"window_rear_right": self._car["windowStatusRearRight"],
|
||||||
|
"original_value": self._car[self._internal_name],
|
||||||
|
"last_update": datetime.datetime.fromtimestamp(
|
||||||
|
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"car": self._car["license"]
|
||||||
|
}
|
||||||
|
elif self._internal_name == "tireWarningLight":
|
||||||
|
return {
|
||||||
|
"front_right_tire_pressure_kpa":
|
||||||
|
self._car["frontRightTirePressureKpa"],
|
||||||
|
"front_left_tire_pressure_kpa":
|
||||||
|
self._car["frontLeftTirePressureKpa"],
|
||||||
|
"rear_right_tire_pressure_kpa":
|
||||||
|
self._car["rearRightTirePressureKpa"],
|
||||||
|
"rear_left_tire_pressure_kpa":
|
||||||
|
self._car["rearLeftTirePressureKpa"],
|
||||||
|
"original_value": self._car[self._internal_name],
|
||||||
|
"last_update": datetime.datetime.fromtimestamp(
|
||||||
|
self._car["lastUpdate"]
|
||||||
|
).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"car": self._car["license"],
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"original_value": self._car[self._internal_name],
|
||||||
|
"last_update": datetime.datetime.fromtimestamp(
|
||||||
|
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"car": self._car["license"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Fetch new state data for the sensor."""
|
||||||
|
self._car = next(
|
||||||
|
car for car in self._data.cars if car["vin"] == self._vin)
|
||||||
|
|
||||||
|
if self._internal_name == "windowsClosed":
|
||||||
|
self._state = bool(self._car[self._internal_name] == "CLOSED")
|
||||||
|
elif self._internal_name == "tireWarningLight":
|
||||||
|
self._state = bool(self._car[self._internal_name] != "INACTIVE")
|
||||||
|
else:
|
||||||
|
self._state = self._car[self._internal_name] is True
|
||||||
|
|
||||||
|
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
|
||||||
|
self._internal_name, self._state, self.is_on)
|
@ -14,8 +14,8 @@ import homeassistant.components.mqtt as mqtt
|
|||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
|
||||||
CONF_DEVICE_CLASS)
|
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS)
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
||||||
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
|
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
|
||||||
@ -24,8 +24,10 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||||
|
|
||||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||||
DEFAULT_PAYLOAD_ON = 'ON'
|
DEFAULT_PAYLOAD_ON = 'ON'
|
||||||
|
DEFAULT_FORCE_UPDATE = False
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ['mqtt']
|
||||||
|
|
||||||
@ -34,6 +36,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
|
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||||
|
|
||||||
|
|
||||||
@ -53,6 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
config.get(CONF_AVAILABILITY_TOPIC),
|
config.get(CONF_AVAILABILITY_TOPIC),
|
||||||
config.get(CONF_DEVICE_CLASS),
|
config.get(CONF_DEVICE_CLASS),
|
||||||
config.get(CONF_QOS),
|
config.get(CONF_QOS),
|
||||||
|
config.get(CONF_FORCE_UPDATE),
|
||||||
config.get(CONF_PAYLOAD_ON),
|
config.get(CONF_PAYLOAD_ON),
|
||||||
config.get(CONF_PAYLOAD_OFF),
|
config.get(CONF_PAYLOAD_OFF),
|
||||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||||
@ -65,7 +69,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||||||
"""Representation a binary sensor that is updated by MQTT."""
|
"""Representation a binary sensor that is updated by MQTT."""
|
||||||
|
|
||||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||||
qos, payload_on, payload_off, payload_available,
|
qos, force_update, payload_on, payload_off, payload_available,
|
||||||
payload_not_available, value_template):
|
payload_not_available, value_template):
|
||||||
"""Initialize the MQTT binary sensor."""
|
"""Initialize the MQTT binary sensor."""
|
||||||
super().__init__(availability_topic, qos, payload_available,
|
super().__init__(availability_topic, qos, payload_available,
|
||||||
@ -77,6 +81,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||||||
self._payload_on = payload_on
|
self._payload_on = payload_on
|
||||||
self._payload_off = payload_off
|
self._payload_off = payload_off
|
||||||
self._qos = qos
|
self._qos = qos
|
||||||
|
self._force_update = force_update
|
||||||
self._template = value_template
|
self._template = value_template
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -94,6 +99,11 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||||||
self._state = True
|
self._state = True
|
||||||
elif payload == self._payload_off:
|
elif payload == self._payload_off:
|
||||||
self._state = False
|
self._state = False
|
||||||
|
else: # Payload is not for this entity
|
||||||
|
_LOGGER.warning('No matching payload found'
|
||||||
|
' for entity: %s with state_topic: %s',
|
||||||
|
self._name, self._state_topic)
|
||||||
|
return
|
||||||
|
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@ -119,3 +129,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this sensor."""
|
"""Return the class of this sensor."""
|
||||||
return self._device_class
|
return self._device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def force_update(self):
|
||||||
|
"""Force update."""
|
||||||
|
return self._force_update
|
||||||
|
@ -131,10 +131,8 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
|||||||
self._name += ' / ' + module_name
|
self._name += ' / ' + module_name
|
||||||
self._sensor_name = sensor
|
self._sensor_name = sensor
|
||||||
self._name += ' ' + sensor
|
self._name += ' ' + sensor
|
||||||
camera_id = data.camera_data.cameraByName(
|
self._unique_id = data.camera_data.cameraByName(
|
||||||
camera=camera_name, home=home)['id']
|
camera=camera_name, home=home)['id']
|
||||||
self._unique_id = "Netatmo_binary_sensor {0} - {1}".format(
|
|
||||||
self._name, camera_id)
|
|
||||||
self._cameratype = camera_type
|
self._cameratype = camera_type
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class PilightBinarySensor(BinarySensorDevice):
|
|||||||
def _handle_code(self, call):
|
def _handle_code(self, call):
|
||||||
"""Handle received code by the pilight-daemon.
|
"""Handle received code by the pilight-daemon.
|
||||||
|
|
||||||
If the code matches the defined playload
|
If the code matches the defined payload
|
||||||
of this sensor the sensor state is changed accordingly.
|
of this sensor the sensor state is changed accordingly.
|
||||||
"""
|
"""
|
||||||
# Check if received code matches defined playoad
|
# Check if received code matches defined playoad
|
||||||
@ -162,10 +162,10 @@ class PilightTriggerSensor(BinarySensorDevice):
|
|||||||
def _handle_code(self, call):
|
def _handle_code(self, call):
|
||||||
"""Handle received code by the pilight-daemon.
|
"""Handle received code by the pilight-daemon.
|
||||||
|
|
||||||
If the code matches the defined playload
|
If the code matches the defined payload
|
||||||
of this sensor the sensor state is changed accordingly.
|
of this sensor the sensor state is changed accordingly.
|
||||||
"""
|
"""
|
||||||
# Check if received code matches defined playoad
|
# Check if received code matches defined payload
|
||||||
# True if payload is contained in received code dict
|
# True if payload is contained in received code dict
|
||||||
payload_ok = True
|
payload_ok = True
|
||||||
for key in self._payload:
|
for key in self._payload:
|
||||||
|
@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
sensor_type))
|
sensor_type))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# create an sensor for each zone managed by faucet
|
# create a sensor for each zone managed by faucet
|
||||||
for zone in raincloud.controller.faucet.zones:
|
for zone in raincloud.controller.faucet.zones:
|
||||||
sensors.append(RainCloudBinarySensor(zone, sensor_type))
|
sensors.append(RainCloudBinarySensor(zone, sensor_type))
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
|
|||||||
DEVICE_CLASSES_SCHEMA)
|
DEVICE_CLASSES_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||||
|
CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE,
|
||||||
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
|
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -29,6 +30,8 @@ CONF_DELAY_OFF = 'delay_off'
|
|||||||
|
|
||||||
SENSOR_SCHEMA = vol.Schema({
|
SENSOR_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template,
|
||||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
@ -38,11 +41,6 @@ SENSOR_SCHEMA = vol.Schema({
|
|||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
})
|
})
|
||||||
|
|
||||||
SENSOR_SCHEMA = vol.All(
|
|
||||||
cv.deprecated(ATTR_ENTITY_ID),
|
|
||||||
SENSOR_SCHEMA,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
|
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
|
||||||
})
|
})
|
||||||
@ -55,6 +53,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
|
|
||||||
for device, device_config in config[CONF_SENSORS].items():
|
for device, device_config in config[CONF_SENSORS].items():
|
||||||
value_template = device_config[CONF_VALUE_TEMPLATE]
|
value_template = device_config[CONF_VALUE_TEMPLATE]
|
||||||
|
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||||
|
entity_picture_template = device_config.get(
|
||||||
|
CONF_ENTITY_PICTURE_TEMPLATE)
|
||||||
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
|
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
|
||||||
value_template.extract_entities())
|
value_template.extract_entities())
|
||||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||||
@ -65,10 +66,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|
||||||
|
if icon_template is not None:
|
||||||
|
icon_template.hass = hass
|
||||||
|
|
||||||
|
if entity_picture_template is not None:
|
||||||
|
entity_picture_template.hass = hass
|
||||||
|
|
||||||
sensors.append(
|
sensors.append(
|
||||||
BinarySensorTemplate(
|
BinarySensorTemplate(
|
||||||
hass, device, friendly_name, device_class, value_template,
|
hass, device, friendly_name, device_class, value_template,
|
||||||
entity_ids, delay_on, delay_off)
|
icon_template, entity_picture_template, entity_ids,
|
||||||
|
delay_on, delay_off)
|
||||||
)
|
)
|
||||||
if not sensors:
|
if not sensors:
|
||||||
_LOGGER.error("No sensors added")
|
_LOGGER.error("No sensors added")
|
||||||
@ -82,7 +90,8 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
"""A virtual binary sensor that triggers from another sensor."""
|
"""A virtual binary sensor that triggers from another sensor."""
|
||||||
|
|
||||||
def __init__(self, hass, device, friendly_name, device_class,
|
def __init__(self, hass, device, friendly_name, device_class,
|
||||||
value_template, entity_ids, delay_on, delay_off):
|
value_template, icon_template, entity_picture_template,
|
||||||
|
entity_ids, delay_on, delay_off):
|
||||||
"""Initialize the Template binary sensor."""
|
"""Initialize the Template binary sensor."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = async_generate_entity_id(
|
self.entity_id = async_generate_entity_id(
|
||||||
@ -91,6 +100,10 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
self._device_class = device_class
|
self._device_class = device_class
|
||||||
self._template = value_template
|
self._template = value_template
|
||||||
self._state = None
|
self._state = None
|
||||||
|
self._icon_template = icon_template
|
||||||
|
self._entity_picture_template = entity_picture_template
|
||||||
|
self._icon = None
|
||||||
|
self._entity_picture = None
|
||||||
self._entities = entity_ids
|
self._entities = entity_ids
|
||||||
self._delay_on = delay_on
|
self._delay_on = delay_on
|
||||||
self._delay_off = delay_off
|
self._delay_off = delay_off
|
||||||
@ -119,6 +132,16 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend, if any."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_picture(self):
|
||||||
|
"""Return the entity_picture to use in the frontend, if any."""
|
||||||
|
return self._entity_picture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if sensor is on."""
|
"""Return true if sensor is on."""
|
||||||
@ -137,8 +160,9 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
@callback
|
@callback
|
||||||
def _async_render(self):
|
def _async_render(self):
|
||||||
"""Get the state of template."""
|
"""Get the state of template."""
|
||||||
|
state = None
|
||||||
try:
|
try:
|
||||||
return self._template.async_render().lower() == 'true'
|
state = (self._template.async_render().lower() == 'true')
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
if ex.args and ex.args[0].startswith(
|
if ex.args and ex.args[0].startswith(
|
||||||
"UndefinedError: 'None' has no attribute"):
|
"UndefinedError: 'None' has no attribute"):
|
||||||
@ -148,6 +172,29 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
return
|
return
|
||||||
_LOGGER.error("Could not render template %s: %s", self._name, ex)
|
_LOGGER.error("Could not render template %s: %s", self._name, ex)
|
||||||
|
|
||||||
|
for property_name, template in (
|
||||||
|
('_icon', self._icon_template),
|
||||||
|
('_entity_picture', self._entity_picture_template)):
|
||||||
|
if template is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
setattr(self, property_name, template.async_render())
|
||||||
|
except TemplateError as ex:
|
||||||
|
friendly_property_name = property_name[1:].replace('_', ' ')
|
||||||
|
if ex.args and ex.args[0].startswith(
|
||||||
|
"UndefinedError: 'None' has no attribute"):
|
||||||
|
# Common during HA startup - so just a warning
|
||||||
|
_LOGGER.warning('Could not render %s template %s,'
|
||||||
|
' the state is unknown.',
|
||||||
|
friendly_property_name, self._name)
|
||||||
|
else:
|
||||||
|
_LOGGER.error('Could not render %s template %s: %s',
|
||||||
|
friendly_property_name, self._name, ex)
|
||||||
|
return state
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_check_state(self):
|
def async_check_state(self):
|
||||||
"""Update the state from the template."""
|
"""Update the state from the template."""
|
||||||
|
@ -126,11 +126,12 @@ class ThresholdSensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def threshold_type(self):
|
def threshold_type(self):
|
||||||
"""Return the type of threshold this sensor represents."""
|
"""Return the type of threshold this sensor represents."""
|
||||||
if self._threshold_lower and self._threshold_upper:
|
if self._threshold_lower is not None and \
|
||||||
|
self._threshold_upper is not None:
|
||||||
return TYPE_RANGE
|
return TYPE_RANGE
|
||||||
elif self._threshold_lower:
|
elif self._threshold_lower is not None:
|
||||||
return TYPE_LOWER
|
return TYPE_LOWER
|
||||||
elif self._threshold_upper:
|
elif self._threshold_upper is not None:
|
||||||
return TYPE_UPPER
|
return TYPE_UPPER
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -58,11 +58,11 @@ class WemoBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the id of this WeMo device."""
|
"""Return the id of this WeMo device."""
|
||||||
return '{}.{}'.format(self.__class__, self.wemo.serialnumber)
|
return self.wemo.serialnumber
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sevice if any."""
|
"""Return the name of the service if any."""
|
||||||
return self.wemo.name
|
return self.wemo.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
|
|
||||||
in_clusters = discovery_info['in_clusters']
|
in_clusters = discovery_info['in_clusters']
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||||||
"""Initialize the ZHA binary sensor."""
|
"""Initialize the ZHA binary sensor."""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._device_class = device_class
|
self._device_class = device_class
|
||||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -78,7 +78,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
return self._device_class
|
return self._device_class
|
||||||
|
|
||||||
def cluster_command(self, aps_frame, tsn, command_id, args):
|
def cluster_command(self, tsn, command_id, args):
|
||||||
"""Handle commands received to this cluster."""
|
"""Handle commands received to this cluster."""
|
||||||
if command_id == 0:
|
if command_id == 0:
|
||||||
self._state = args[0] & 3
|
self._state = args[0] & 3
|
||||||
|
@ -174,7 +174,7 @@ class WebDavCalendarData(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_matching(vevent, search):
|
def is_matching(vevent, search):
|
||||||
"""Return if the event matches the filter critera."""
|
"""Return if the event matches the filter criteria."""
|
||||||
if search is None:
|
if search is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -411,7 +411,7 @@ class TodoistProjectData(object):
|
|||||||
|
|
||||||
The "best" event is determined by the following criteria:
|
The "best" event is determined by the following criteria:
|
||||||
* A proposed event must not be completed
|
* A proposed event must not be completed
|
||||||
* A proposed event must have a end date (otherwise we go with
|
* A proposed event must have an end date (otherwise we go with
|
||||||
the event at index 0, selected above)
|
the event at index 0, selected above)
|
||||||
* A proposed event must be on the same day or earlier as our
|
* A proposed event must be on the same day or earlier as our
|
||||||
current event
|
current event
|
||||||
|
@ -91,13 +91,13 @@ def async_snapshot(hass, filename, entity_id=None):
|
|||||||
@bind_hass
|
@bind_hass
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_get_image(hass, entity_id, timeout=10):
|
def async_get_image(hass, entity_id, timeout=10):
|
||||||
"""Fetch a image from a camera entity."""
|
"""Fetch an image from a camera entity."""
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if state is None:
|
if state is None:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"No entity '{0}' for grab a image".format(entity_id))
|
"No entity '{0}' for grab an image".format(entity_id))
|
||||||
|
|
||||||
url = "{0}{1}".format(
|
url = "{0}{1}".format(
|
||||||
hass.config.api.base_url,
|
hass.config.api.base_url,
|
||||||
|
@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discoveryy_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up Abode camera devices."""
|
"""Set up Abode camera devices."""
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
import abodepy.helpers.timeline as TIMELINE
|
import abodepy.helpers.timeline as TIMELINE
|
||||||
|
@ -4,19 +4,30 @@ Support for Canary camera.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera.canary/
|
https://home-assistant.io/components/camera.canary/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import requests
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||||
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
|
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
|
||||||
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
DEPENDENCIES = ['canary']
|
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||||
|
|
||||||
|
DEPENDENCIES = ['canary', 'ffmpeg']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_MOTION_START_TIME = "motion_start_time"
|
MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
|
||||||
ATTR_MOTION_END_TIME = "motion_end_time"
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -25,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
for location in data.locations:
|
for location in data.locations:
|
||||||
entries = data.get_motion_entries(location.location_id)
|
for device in location.devices:
|
||||||
if entries:
|
if device.is_online:
|
||||||
devices.append(CanaryCamera(data, location.location_id,
|
devices.append(
|
||||||
DEFAULT_TIMEOUT))
|
CanaryCamera(hass, data, location, device, DEFAULT_TIMEOUT,
|
||||||
|
config.get(CONF_FFMPEG_ARGUMENTS)))
|
||||||
|
|
||||||
add_devices(devices, True)
|
add_devices(devices, True)
|
||||||
|
|
||||||
@ -36,60 +48,65 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class CanaryCamera(Camera):
|
class CanaryCamera(Camera):
|
||||||
"""An implementation of a Canary security camera."""
|
"""An implementation of a Canary security camera."""
|
||||||
|
|
||||||
def __init__(self, data, location_id, timeout):
|
def __init__(self, hass, data, location, device, timeout, ffmpeg_args):
|
||||||
"""Initialize a Canary security camera."""
|
"""Initialize a Canary security camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||||
|
self._ffmpeg_arguments = ffmpeg_args
|
||||||
self._data = data
|
self._data = data
|
||||||
self._location_id = location_id
|
self._location = location
|
||||||
|
self._device = device
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
self._live_stream_session = None
|
||||||
self._location = None
|
|
||||||
self._motion_entry = None
|
|
||||||
self._image_content = None
|
|
||||||
|
|
||||||
def camera_image(self):
|
|
||||||
"""Update the status of the camera and return bytes of camera image."""
|
|
||||||
self.update()
|
|
||||||
return self._image_content
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of this device."""
|
"""Return the name of this device."""
|
||||||
return self._location.name
|
return self._device.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
"""Return true if the device is recording."""
|
"""Return true if the device is recording."""
|
||||||
return self._location.is_recording
|
return self._location.is_recording
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return device specific state attributes."""
|
|
||||||
if self._motion_entry is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
ATTR_MOTION_START_TIME: self._motion_entry.start_time,
|
|
||||||
ATTR_MOTION_END_TIME: self._motion_entry.end_time,
|
|
||||||
}
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Update the status of the camera."""
|
|
||||||
self._data.update()
|
|
||||||
self._location = self._data.get_location(self._location_id)
|
|
||||||
|
|
||||||
entries = self._data.get_motion_entries(self._location_id)
|
|
||||||
if entries:
|
|
||||||
current = entries[0]
|
|
||||||
previous = self._motion_entry
|
|
||||||
|
|
||||||
if previous is None or previous.entry_id != current.entry_id:
|
|
||||||
self._motion_entry = current
|
|
||||||
self._image_content = requests.get(
|
|
||||||
current.thumbnails[0].image_url,
|
|
||||||
timeout=self._timeout).content
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def motion_detection_enabled(self):
|
def motion_detection_enabled(self):
|
||||||
"""Return the camera motion detection status."""
|
"""Return the camera motion detection status."""
|
||||||
return not self._location.is_recording
|
return not self._location.is_recording
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_camera_image(self):
|
||||||
|
"""Return a still image response from the camera."""
|
||||||
|
self.renew_live_stream_session()
|
||||||
|
|
||||||
|
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||||
|
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
||||||
|
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||||
|
self._live_stream_session.live_stream_url,
|
||||||
|
output_format=IMAGE_JPEG,
|
||||||
|
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||||
|
return image
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def handle_async_mjpeg_stream(self, request):
|
||||||
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
|
if self._live_stream_session is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
from haffmpeg import CameraMjpeg
|
||||||
|
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||||
|
yield from stream.open_camera(
|
||||||
|
self._live_stream_session.live_stream_url,
|
||||||
|
extra_cmd=self._ffmpeg_arguments)
|
||||||
|
|
||||||
|
yield from async_aiohttp_proxy_stream(
|
||||||
|
self.hass, request, stream,
|
||||||
|
'multipart/x-mixed-replace;boundary=ffserver')
|
||||||
|
yield from stream.close()
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SESSION_RENEW)
|
||||||
|
def renew_live_stream_session(self):
|
||||||
|
"""Renew live stream session."""
|
||||||
|
self._live_stream_session = self._data.get_live_stream_session(
|
||||||
|
self._device)
|
||||||
|
@ -64,13 +64,11 @@ class NetatmoCamera(Camera):
|
|||||||
self._name = home + ' / ' + camera_name
|
self._name = home + ' / ' + camera_name
|
||||||
else:
|
else:
|
||||||
self._name = camera_name
|
self._name = camera_name
|
||||||
camera_id = data.camera_data.cameraByName(
|
|
||||||
camera=camera_name, home=home)['id']
|
|
||||||
self._unique_id = "Welcome_camera {0} - {1}".format(
|
|
||||||
self._name, camera_id)
|
|
||||||
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
|
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
|
||||||
camera=camera_name
|
camera=camera_name
|
||||||
)
|
)
|
||||||
|
self._unique_id = data.camera_data.cameraByName(
|
||||||
|
camera=camera_name, home=home)['id']
|
||||||
self._cameratype = camera_type
|
self._cameratype = camera_type
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
@ -117,5 +115,5 @@ class NetatmoCamera(Camera):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the unique ID for this sensor."""
|
"""Return the unique ID for this camera."""
|
||||||
return self._unique_id
|
return self._unique_id
|
||||||
|
@ -28,7 +28,7 @@ CONF_VERTICAL_FLIP = 'vertical_flip'
|
|||||||
|
|
||||||
DEFAULT_HORIZONTAL_FLIP = 0
|
DEFAULT_HORIZONTAL_FLIP = 0
|
||||||
DEFAULT_IMAGE_HEIGHT = 480
|
DEFAULT_IMAGE_HEIGHT = 480
|
||||||
DEFAULT_IMAGE_QUALITIY = 7
|
DEFAULT_IMAGE_QUALITY = 7
|
||||||
DEFAULT_IMAGE_ROTATION = 0
|
DEFAULT_IMAGE_ROTATION = 0
|
||||||
DEFAULT_IMAGE_WIDTH = 640
|
DEFAULT_IMAGE_WIDTH = 640
|
||||||
DEFAULT_NAME = 'Raspberry Pi Camera'
|
DEFAULT_NAME = 'Raspberry Pi Camera'
|
||||||
@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
|
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
|
||||||
vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION):
|
vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=359)),
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=359)),
|
||||||
@ -131,7 +131,7 @@ class RaspberryCamera(Camera):
|
|||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return raspstill image response."""
|
"""Return raspistill image response."""
|
||||||
with open(self._config[CONF_FILE_PATH], 'rb') as file:
|
with open(self._config[CONF_FILE_PATH], 'rb') as file:
|
||||||
return file.read()
|
return file.read()
|
||||||
|
|
||||||
|
@ -127,6 +127,9 @@ class UnifiVideoCamera(Camera):
|
|||||||
else:
|
else:
|
||||||
client_cls = uvc_camera.UVCCameraClient
|
client_cls = uvc_camera.UVCCameraClient
|
||||||
|
|
||||||
|
if caminfo['username'] is None:
|
||||||
|
caminfo['username'] = 'ubnt'
|
||||||
|
|
||||||
camera = None
|
camera = None
|
||||||
for addr in addrs:
|
for addr in addrs:
|
||||||
try:
|
try:
|
||||||
|
26
homeassistant/components/camera/xeoma.py
Executable file → Normal file
26
homeassistant/components/camera/xeoma.py
Executable file → Normal file
@ -14,7 +14,7 @@ from homeassistant.const import (
|
|||||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
|
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyxeoma==1.2']
|
REQUIREMENTS = ['pyxeoma==1.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -22,6 +22,8 @@ CONF_CAMERAS = 'cameras'
|
|||||||
CONF_HIDE = 'hide'
|
CONF_HIDE = 'hide'
|
||||||
CONF_IMAGE_NAME = 'image_name'
|
CONF_IMAGE_NAME = 'image_name'
|
||||||
CONF_NEW_VERSION = 'new_version'
|
CONF_NEW_VERSION = 'new_version'
|
||||||
|
CONF_VIEWER_PASSWORD = 'viewer_password'
|
||||||
|
CONF_VIEWER_USERNAME = 'viewer_username'
|
||||||
|
|
||||||
CAMERAS_SCHEMA = vol.Schema({
|
CAMERAS_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_IMAGE_NAME): cv.string,
|
vol.Required(CONF_IMAGE_NAME): cv.string,
|
||||||
@ -48,9 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
host = config[CONF_HOST]
|
host = config[CONF_HOST]
|
||||||
login = config.get(CONF_USERNAME)
|
login = config.get(CONF_USERNAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
new_version = config[CONF_NEW_VERSION]
|
|
||||||
|
|
||||||
xeoma = Xeoma(host, new_version, login, password)
|
xeoma = Xeoma(host, login, password)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield from xeoma.async_test_connection()
|
yield from xeoma.async_test_connection()
|
||||||
@ -59,9 +60,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
{
|
{
|
||||||
CONF_IMAGE_NAME: image_name,
|
CONF_IMAGE_NAME: image_name,
|
||||||
CONF_HIDE: False,
|
CONF_HIDE: False,
|
||||||
CONF_NAME: image_name
|
CONF_NAME: image_name,
|
||||||
|
CONF_VIEWER_USERNAME: username,
|
||||||
|
CONF_VIEWER_PASSWORD: pw
|
||||||
|
|
||||||
}
|
}
|
||||||
for image_name in discovered_image_names
|
for image_name, username, pw in discovered_image_names
|
||||||
]
|
]
|
||||||
|
|
||||||
for cam in config[CONF_CAMERAS]:
|
for cam in config[CONF_CAMERAS]:
|
||||||
@ -77,8 +81,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
|
|
||||||
cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras))
|
cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras))
|
||||||
async_add_devices(
|
async_add_devices(
|
||||||
[XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME])
|
[XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME],
|
||||||
for camera in cameras])
|
camera[CONF_VIEWER_USERNAME],
|
||||||
|
camera[CONF_VIEWER_PASSWORD]) for camera in cameras])
|
||||||
except XeomaError as err:
|
except XeomaError as err:
|
||||||
_LOGGER.error("Error: %s", err.message)
|
_LOGGER.error("Error: %s", err.message)
|
||||||
return
|
return
|
||||||
@ -87,12 +92,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
class XeomaCamera(Camera):
|
class XeomaCamera(Camera):
|
||||||
"""Implementation of a Xeoma camera."""
|
"""Implementation of a Xeoma camera."""
|
||||||
|
|
||||||
def __init__(self, xeoma, image, name):
|
def __init__(self, xeoma, image, name, username, password):
|
||||||
"""Initialize a Xeoma camera."""
|
"""Initialize a Xeoma camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._xeoma = xeoma
|
self._xeoma = xeoma
|
||||||
self._name = name
|
self._name = name
|
||||||
self._image = image
|
self._image = image
|
||||||
|
self._username = username
|
||||||
|
self._password = password
|
||||||
self._last_image = None
|
self._last_image = None
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -100,7 +107,8 @@ class XeomaCamera(Camera):
|
|||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
from pyxeoma.xeoma import XeomaError
|
from pyxeoma.xeoma import XeomaError
|
||||||
try:
|
try:
|
||||||
image = yield from self._xeoma.async_get_camera_image(self._image)
|
image = yield from self._xeoma.async_get_camera_image(
|
||||||
|
self._image, self._username, self._password)
|
||||||
self._last_image = image
|
self._last_image = image
|
||||||
except XeomaError as err:
|
except XeomaError as err:
|
||||||
_LOGGER.error("Error fetching image: %s", err.message)
|
_LOGGER.error("Error fetching image: %s", err.message)
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
|||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['py-canary==0.2.3']
|
REQUIREMENTS = ['py-canary==0.4.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -111,7 +111,18 @@ class CanaryData(object):
|
|||||||
"""Return a list of readings based on device_id."""
|
"""Return a list of readings based on device_id."""
|
||||||
return self._readings_by_device_id.get(device_id, [])
|
return self._readings_by_device_id.get(device_id, [])
|
||||||
|
|
||||||
|
def get_reading(self, device_id, sensor_type):
|
||||||
|
"""Return reading for device_id and sensor type."""
|
||||||
|
readings = self._readings_by_device_id.get(device_id, [])
|
||||||
|
return next((
|
||||||
|
reading.value for reading in readings
|
||||||
|
if reading.sensor_type == sensor_type), None)
|
||||||
|
|
||||||
def set_location_mode(self, location_id, mode_name, is_private=False):
|
def set_location_mode(self, location_id, mode_name, is_private=False):
|
||||||
"""Set location mode."""
|
"""Set location mode."""
|
||||||
self._api.set_location_mode(location_id, mode_name, is_private)
|
self._api.set_location_mode(location_id, mode_name, is_private)
|
||||||
self.update(no_throttle=True)
|
self.update(no_throttle=True)
|
||||||
|
|
||||||
|
def get_live_stream_session(self, device):
|
||||||
|
"""Return live stream session."""
|
||||||
|
return self._api.get_live_stream_session(device)
|
||||||
|
@ -183,11 +183,6 @@ class DaikinClimate(ClimateDevice):
|
|||||||
self._force_refresh = True
|
self._force_refresh = True
|
||||||
self._api.device.set(values)
|
self._api.device.set(values)
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return the ID of this AC."""
|
|
||||||
return "{}.{}".format(self.__class__, self._api.ip_address)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
|
@ -14,23 +14,19 @@ from homeassistant.components.climate import (
|
|||||||
SUPPORT_ON_OFF)
|
SUPPORT_ON_OFF)
|
||||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||||
|
|
||||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
|
SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH
|
||||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
|
|
||||||
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
|
|
||||||
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
|
|
||||||
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
|
||||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Demo climate devices."""
|
"""Set up the Demo climate devices."""
|
||||||
add_devices([
|
add_devices([
|
||||||
DemoClimate('HeatPump', 68, TEMP_FAHRENHEIT, None, None, 77,
|
DemoClimate('HeatPump', 68, TEMP_FAHRENHEIT, None, None, 77,
|
||||||
'Auto Low', None, None, 'Auto', 'heat', None, None, None),
|
None, None, None, None, 'heat', None, None,
|
||||||
|
None, True),
|
||||||
DemoClimate('Hvac', 21, TEMP_CELSIUS, True, None, 22, 'On High',
|
DemoClimate('Hvac', 21, TEMP_CELSIUS, True, None, 22, 'On High',
|
||||||
67, 54, 'Off', 'cool', False, None, None),
|
67, 54, 'Off', 'cool', False, None, None, None),
|
||||||
DemoClimate('Ecobee', None, TEMP_CELSIUS, None, None, 23, 'Auto Low',
|
DemoClimate('Ecobee', None, TEMP_CELSIUS, None, 'home', 23, 'Auto Low',
|
||||||
None, None, 'Auto', 'auto', None, 24, 21)
|
None, None, 'Auto', 'auto', None, 24, 21, None)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@ -40,9 +36,37 @@ class DemoClimate(ClimateDevice):
|
|||||||
def __init__(self, name, target_temperature, unit_of_measurement,
|
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||||
away, hold, current_temperature, current_fan_mode,
|
away, hold, current_temperature, current_fan_mode,
|
||||||
target_humidity, current_humidity, current_swing_mode,
|
target_humidity, current_humidity, current_swing_mode,
|
||||||
current_operation, aux, target_temp_high, target_temp_low):
|
current_operation, aux, target_temp_high, target_temp_low,
|
||||||
|
is_on):
|
||||||
"""Initialize the climate device."""
|
"""Initialize the climate device."""
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._support_flags = SUPPORT_FLAGS
|
||||||
|
if target_temperature is not None:
|
||||||
|
self._support_flags = \
|
||||||
|
self._support_flags | SUPPORT_TARGET_TEMPERATURE
|
||||||
|
if away is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_AWAY_MODE
|
||||||
|
if hold is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_HOLD_MODE
|
||||||
|
if current_fan_mode is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_FAN_MODE
|
||||||
|
if target_humidity is not None:
|
||||||
|
self._support_flags = \
|
||||||
|
self._support_flags | SUPPORT_TARGET_HUMIDITY
|
||||||
|
if current_swing_mode is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_SWING_MODE
|
||||||
|
if current_operation is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE
|
||||||
|
if aux is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_AUX_HEAT
|
||||||
|
if target_temp_high is not None:
|
||||||
|
self._support_flags = \
|
||||||
|
self._support_flags | SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||||
|
if target_temp_low is not None:
|
||||||
|
self._support_flags = \
|
||||||
|
self._support_flags | SUPPORT_TARGET_TEMPERATURE_LOW
|
||||||
|
if is_on is not None:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_ON_OFF
|
||||||
self._target_temperature = target_temperature
|
self._target_temperature = target_temperature
|
||||||
self._target_humidity = target_humidity
|
self._target_humidity = target_humidity
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
@ -59,12 +83,12 @@ class DemoClimate(ClimateDevice):
|
|||||||
self._swing_list = ['Auto', '1', '2', '3', 'Off']
|
self._swing_list = ['Auto', '1', '2', '3', 'Off']
|
||||||
self._target_temperature_high = target_temp_high
|
self._target_temperature_high = target_temp_high
|
||||||
self._target_temperature_low = target_temp_low
|
self._target_temperature_low = target_temp_low
|
||||||
self._on = True
|
self._on = is_on
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
return SUPPORT_FLAGS
|
return self._support_flags
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -207,7 +231,7 @@ class DemoClimate(ClimateDevice):
|
|||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_aux_heat_on(self):
|
def turn_aux_heat_on(self):
|
||||||
"""Turn auxillary heater on."""
|
"""Turn auxiliary heater on."""
|
||||||
self._aux = True
|
self._aux = True
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
@ -13,7 +13,9 @@ from homeassistant.components.climate import (
|
|||||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
|
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
|
||||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
||||||
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
|
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
|
||||||
|
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||||
|
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -46,7 +48,9 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||||
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
||||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH)
|
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||||
|
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||||
|
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||||||
TEMP_FAHRENHEIT)
|
TEMP_FAHRENHEIT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyeconet==0.0.4']
|
REQUIREMENTS = ['pyeconet==0.0.5']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
class EQ3BTSmartThermostat(ClimateDevice):
|
class EQ3BTSmartThermostat(ClimateDevice):
|
||||||
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
|
||||||
|
|
||||||
def __init__(self, _mac, _name):
|
def __init__(self, _mac, _name):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
|
@ -156,7 +156,7 @@ class GenericThermostat(ClimateDevice):
|
|||||||
# If we have no initial temperature, restore
|
# If we have no initial temperature, restore
|
||||||
if self._target_temp is None:
|
if self._target_temp is None:
|
||||||
# If we have a previously saved temperature
|
# If we have a previously saved temperature
|
||||||
if old_state.attributes[ATTR_TEMPERATURE] is None:
|
if old_state.attributes.get(ATTR_TEMPERATURE) is None:
|
||||||
if self.ac_mode:
|
if self.ac_mode:
|
||||||
self._target_temp = self.max_temp
|
self._target_temp = self.max_temp
|
||||||
else:
|
else:
|
||||||
@ -166,15 +166,15 @@ class GenericThermostat(ClimateDevice):
|
|||||||
else:
|
else:
|
||||||
self._target_temp = float(
|
self._target_temp = float(
|
||||||
old_state.attributes[ATTR_TEMPERATURE])
|
old_state.attributes[ATTR_TEMPERATURE])
|
||||||
if old_state.attributes[ATTR_AWAY_MODE] is not None:
|
if old_state.attributes.get(ATTR_AWAY_MODE) is not None:
|
||||||
self._is_away = str(
|
self._is_away = str(
|
||||||
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON
|
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON
|
||||||
if (self._initial_operation_mode is None and
|
if (self._initial_operation_mode is None and
|
||||||
old_state.attributes[ATTR_OPERATION_MODE] is not None):
|
old_state.attributes[ATTR_OPERATION_MODE] is not None):
|
||||||
self._current_operation = \
|
self._current_operation = \
|
||||||
old_state.attributes[ATTR_OPERATION_MODE]
|
old_state.attributes[ATTR_OPERATION_MODE]
|
||||||
if self._current_operation != STATE_OFF:
|
self._enabled = self._current_operation != STATE_OFF
|
||||||
self._enabled = True
|
|
||||||
else:
|
else:
|
||||||
# No previous state, try and restore defaults
|
# No previous state, try and restore defaults
|
||||||
if self._target_temp is None:
|
if self._target_temp is None:
|
||||||
@ -249,7 +249,7 @@ class GenericThermostat(ClimateDevice):
|
|||||||
else:
|
else:
|
||||||
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
|
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
|
||||||
return
|
return
|
||||||
# Ensure we updae the current operation after changing the mode
|
# Ensure we update the current operation after changing the mode
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
serport = connection.connection(ipaddress, port)
|
serport = connection.connection(ipaddress, port)
|
||||||
serport.open()
|
serport.open()
|
||||||
|
|
||||||
for thermostat, tstat in tstats.items():
|
for tstat in tstats.values():
|
||||||
add_devices([
|
add_devices([
|
||||||
HeatmiserV3Thermostat(
|
HeatmiserV3Thermostat(
|
||||||
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
||||||
|
@ -174,5 +174,5 @@ class HiveClimateEntity(ClimateDevice):
|
|||||||
entity.handle_update(self.data_updatesource)
|
entity.handle_update(self.data_updatesource)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update all Node data frome Hive."""
|
"""Update all Node data from Hive."""
|
||||||
self.session.core.update_data(self.node_id)
|
self.session.core.update_data(self.node_id)
|
||||||
|
259
homeassistant/components/climate/melissa.py
Normal file
259
homeassistant/components/climate/melissa.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
"""
|
||||||
|
Support for Melissa Climate A/C.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/climate.melissa/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
ClimateDevice, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||||
|
SUPPORT_ON_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY,
|
||||||
|
STATE_FAN_ONLY, SUPPORT_FAN_MODE
|
||||||
|
)
|
||||||
|
from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH
|
||||||
|
from homeassistant.components.melissa import DATA_MELISSA
|
||||||
|
from homeassistant.const import (
|
||||||
|
TEMP_CELSIUS, STATE_ON, STATE_OFF, STATE_IDLE, ATTR_TEMPERATURE,
|
||||||
|
PRECISION_WHOLE
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['melissa']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SUPPORT_FLAGS = (SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE |
|
||||||
|
SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE)
|
||||||
|
|
||||||
|
OP_MODES = [
|
||||||
|
STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT
|
||||||
|
]
|
||||||
|
|
||||||
|
FAN_MODES = [
|
||||||
|
STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Iterate through and add all Melissa devices."""
|
||||||
|
api = hass.data[DATA_MELISSA]
|
||||||
|
devices = api.fetch_devices().values()
|
||||||
|
|
||||||
|
all_devices = []
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
all_devices.append(MelissaClimate(
|
||||||
|
api, device['serial_number'], device))
|
||||||
|
|
||||||
|
add_devices(all_devices)
|
||||||
|
|
||||||
|
|
||||||
|
class MelissaClimate(ClimateDevice):
|
||||||
|
"""Representation of a Melissa Climate device."""
|
||||||
|
|
||||||
|
def __init__(self, api, serial_number, init_data):
|
||||||
|
"""Initialize the climate device."""
|
||||||
|
self._name = init_data['name']
|
||||||
|
self._api = api
|
||||||
|
self._serial_number = serial_number
|
||||||
|
self._data = init_data['controller_log']
|
||||||
|
self._state = None
|
||||||
|
self._cur_settings = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the thermostat, if any."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return current state."""
|
||||||
|
if self._cur_settings is not None:
|
||||||
|
return self._cur_settings[self._api.STATE] in (
|
||||||
|
self._api.STATE_ON, self._api.STATE_IDLE)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_fan_mode(self):
|
||||||
|
"""Return the current fan mode."""
|
||||||
|
if self._cur_settings is not None:
|
||||||
|
return self.melissa_fan_to_hass(
|
||||||
|
self._cur_settings[self._api.FAN])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self):
|
||||||
|
"""Return the current temperature."""
|
||||||
|
if self._data:
|
||||||
|
return self._data[self._api.TEMP]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_step(self):
|
||||||
|
"""Return the supported step of target temperature."""
|
||||||
|
return PRECISION_WHOLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self):
|
||||||
|
"""Return the current operation mode."""
|
||||||
|
if self._cur_settings is not None:
|
||||||
|
return self.melissa_op_to_hass(
|
||||||
|
self._cur_settings[self._api.MODE])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def operation_list(self):
|
||||||
|
"""Return the list of available operation modes."""
|
||||||
|
return OP_MODES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_list(self):
|
||||||
|
"""List of available fan modes."""
|
||||||
|
return FAN_MODES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self):
|
||||||
|
"""Return the temperature we try to reach."""
|
||||||
|
if self._cur_settings is not None:
|
||||||
|
return self._cur_settings[self._api.TEMP]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return current state."""
|
||||||
|
if self._cur_settings is not None:
|
||||||
|
return self.melissa_state_to_hass(
|
||||||
|
self._cur_settings[self._api.STATE])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self):
|
||||||
|
"""Return the unit of measurement which this thermostat uses."""
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_temp(self):
|
||||||
|
"""Return the minimum supported temperature for the thermostat."""
|
||||||
|
return 16
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self):
|
||||||
|
"""Return the maximum supported temperature for the thermostat."""
|
||||||
|
return 30
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
return SUPPORT_FLAGS
|
||||||
|
|
||||||
|
def set_temperature(self, **kwargs):
|
||||||
|
"""Set new target temperature."""
|
||||||
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
self.send({self._api.TEMP: temp})
|
||||||
|
|
||||||
|
def set_fan_mode(self, fan):
|
||||||
|
"""Set fan mode."""
|
||||||
|
fan_mode = self.hass_fan_to_melissa(fan)
|
||||||
|
self.send({self._api.FAN: fan_mode})
|
||||||
|
|
||||||
|
def set_operation_mode(self, operation_mode):
|
||||||
|
"""Set operation mode."""
|
||||||
|
mode = self.hass_mode_to_melissa(operation_mode)
|
||||||
|
self.send({self._api.MODE: mode})
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn on device."""
|
||||||
|
self.send({self._api.STATE: self._api.STATE_ON})
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off device."""
|
||||||
|
self.send({self._api.STATE: self._api.STATE_OFF})
|
||||||
|
|
||||||
|
def send(self, value):
|
||||||
|
"""Sending action to service."""
|
||||||
|
try:
|
||||||
|
old_value = self._cur_settings.copy()
|
||||||
|
self._cur_settings.update(value)
|
||||||
|
except AttributeError:
|
||||||
|
old_value = None
|
||||||
|
if not self._api.send(self._serial_number, self._cur_settings):
|
||||||
|
self._cur_settings = old_value
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get latest data from Melissa."""
|
||||||
|
try:
|
||||||
|
self._data = self._api.status(cached=True)[self._serial_number]
|
||||||
|
self._cur_settings = self._api.cur_settings(
|
||||||
|
self._serial_number
|
||||||
|
)['controller']['_relation']['command_log']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Unable to update entity %s', self.entity_id)
|
||||||
|
|
||||||
|
def melissa_state_to_hass(self, state):
|
||||||
|
"""Translate Melissa states to hass states."""
|
||||||
|
if state == self._api.STATE_ON:
|
||||||
|
return STATE_ON
|
||||||
|
elif state == self._api.STATE_OFF:
|
||||||
|
return STATE_OFF
|
||||||
|
elif state == self._api.STATE_IDLE:
|
||||||
|
return STATE_IDLE
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def melissa_op_to_hass(self, mode):
|
||||||
|
"""Translate Melissa modes to hass states."""
|
||||||
|
if mode == self._api.MODE_AUTO:
|
||||||
|
return STATE_AUTO
|
||||||
|
elif mode == self._api.MODE_HEAT:
|
||||||
|
return STATE_HEAT
|
||||||
|
elif mode == self._api.MODE_COOL:
|
||||||
|
return STATE_COOL
|
||||||
|
elif mode == self._api.MODE_DRY:
|
||||||
|
return STATE_DRY
|
||||||
|
elif mode == self._api.MODE_FAN:
|
||||||
|
return STATE_FAN_ONLY
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Operation mode %s could not be mapped to hass", mode)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def melissa_fan_to_hass(self, fan):
|
||||||
|
"""Translate Melissa fan modes to hass modes."""
|
||||||
|
if fan == self._api.FAN_AUTO:
|
||||||
|
return STATE_AUTO
|
||||||
|
elif fan == self._api.FAN_LOW:
|
||||||
|
return SPEED_LOW
|
||||||
|
elif fan == self._api.FAN_MEDIUM:
|
||||||
|
return SPEED_MEDIUM
|
||||||
|
elif fan == self._api.FAN_HIGH:
|
||||||
|
return SPEED_HIGH
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def hass_mode_to_melissa(self, mode):
|
||||||
|
"""Translate hass states to melissa modes."""
|
||||||
|
if mode == STATE_AUTO:
|
||||||
|
return self._api.MODE_AUTO
|
||||||
|
elif mode == STATE_HEAT:
|
||||||
|
return self._api.MODE_HEAT
|
||||||
|
elif mode == STATE_COOL:
|
||||||
|
return self._api.MODE_COOL
|
||||||
|
elif mode == STATE_DRY:
|
||||||
|
return self._api.MODE_DRY
|
||||||
|
elif mode == STATE_FAN_ONLY:
|
||||||
|
return self._api.MODE_FAN
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Melissa have no setting for %s mode", mode)
|
||||||
|
|
||||||
|
def hass_fan_to_melissa(self, fan):
|
||||||
|
"""Translate hass fan modes to melissa modes."""
|
||||||
|
if fan == STATE_AUTO:
|
||||||
|
return self._api.FAN_AUTO
|
||||||
|
elif fan == SPEED_LOW:
|
||||||
|
return self._api.FAN_LOW
|
||||||
|
elif fan == SPEED_MEDIUM:
|
||||||
|
return self._api.FAN_MEDIUM
|
||||||
|
elif fan == SPEED_HIGH:
|
||||||
|
return self._api.FAN_HIGH
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Melissa have no setting for %s fan mode", fan)
|
@ -565,7 +565,7 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
|||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_aux_heat_on(self):
|
def async_turn_aux_heat_on(self):
|
||||||
"""Turn auxillary heater on."""
|
"""Turn auxiliary heater on."""
|
||||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||||
self._payload_on, self._qos, self._retain)
|
self._payload_on, self._qos, self._retain)
|
||||||
@ -576,7 +576,7 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
|||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_aux_heat_off(self):
|
def async_turn_aux_heat_off(self):
|
||||||
"""Turn auxillary heater off."""
|
"""Turn auxiliary heater off."""
|
||||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||||
self._payload_off, self._qos, self._retain)
|
self._payload_off, self._qos, self._retain)
|
||||||
|
@ -139,8 +139,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
|||||||
self.gateway.set_child_value(
|
self.gateway.set_child_value(
|
||||||
self.node_id, self.child_id, value_type, value)
|
self.node_id, self.child_id, value_type, value)
|
||||||
if self.gateway.optimistic:
|
if self.gateway.optimistic:
|
||||||
# O
|
# Optimistically assume that device has changed state
|
||||||
# ptimistically assume that device has changed state
|
|
||||||
self._values[value_type] = value
|
self._values[value_type] = value
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
@ -97,6 +97,11 @@ class NestThermostat(ClimateDevice):
|
|||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
return SUPPORT_FLAGS
|
return SUPPORT_FLAGS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Unique ID for this device."""
|
||||||
|
return self.device.serial
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the nest, if any."""
|
"""Return the name of the nest, if any."""
|
||||||
|
@ -24,7 +24,7 @@ CONF_RELAY = 'relay'
|
|||||||
CONF_THERMOSTAT = 'thermostat'
|
CONF_THERMOSTAT = 'thermostat'
|
||||||
|
|
||||||
DEFAULT_AWAY_TEMPERATURE = 14
|
DEFAULT_AWAY_TEMPERATURE = 14
|
||||||
# # The default offeset is 2 hours (when you use the thermostat itself)
|
# # The default offset is 2 hours (when you use the thermostat itself)
|
||||||
DEFAULT_TIME_OFFSET = 7200
|
DEFAULT_TIME_OFFSET = 7200
|
||||||
# # Return cached results if last scan was less then this time ago
|
# # Return cached results if last scan was less then this time ago
|
||||||
# # NetAtmo Data is uploaded to server every hour
|
# # NetAtmo Data is uploaded to server every hour
|
||||||
|
@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class ThermostatDevice(ClimateDevice):
|
class ThermostatDevice(ClimateDevice):
|
||||||
"""Interface class for the oemthermostat modul."""
|
"""Interface class for the oemthermostat module."""
|
||||||
|
|
||||||
def __init__(self, hass, thermostat, name, away_temp):
|
def __init__(self, hass, thermostat, name, away_temp):
|
||||||
"""Initialize the device."""
|
"""Initialize the device."""
|
||||||
|
@ -249,7 +249,7 @@ class TadoClimate(ClimateDevice):
|
|||||||
data = self._store.get_data(self._data_id)
|
data = self._store.get_data(self._data_id)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
_LOGGER.debug("Recieved no data for zone %s", self.zone_name)
|
_LOGGER.debug("Received no data for zone %s", self.zone_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'sensorDataPoints' in data:
|
if 'sensorDataPoints' in data:
|
||||||
@ -294,7 +294,7 @@ class TadoClimate(ClimateDevice):
|
|||||||
|
|
||||||
overlay = False
|
overlay = False
|
||||||
overlay_data = None
|
overlay_data = None
|
||||||
termination = self._current_operation
|
termination = CONST_MODE_SMART_SCHEDULE
|
||||||
cooling = False
|
cooling = False
|
||||||
fan_speed = CONST_MODE_OFF
|
fan_speed = CONST_MODE_OFF
|
||||||
|
|
||||||
@ -317,7 +317,7 @@ class TadoClimate(ClimateDevice):
|
|||||||
fan_speed = setting_data['fanSpeed']
|
fan_speed = setting_data['fanSpeed']
|
||||||
|
|
||||||
if self._device_is_active:
|
if self._device_is_active:
|
||||||
# If you set mode manualy to off, there will be an overlay
|
# If you set mode manually to off, there will be an overlay
|
||||||
# and a termination, but we want to see the mode "OFF"
|
# and a termination, but we want to see the mode "OFF"
|
||||||
self._overlay_mode = termination
|
self._overlay_mode = termination
|
||||||
self._current_operation = termination
|
self._current_operation = termination
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.components.climate import (
|
|||||||
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
|
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pytouchline==0.6']
|
REQUIREMENTS = ['pytouchline==0.7']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
|||||||
TEMP_FAHRENHEIT)
|
TEMP_FAHRENHEIT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['venstarcolortouch==0.5']
|
REQUIREMENTS = ['venstarcolortouch==0.6']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -16,8 +16,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
|
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
|
||||||
from homeassistant.helpers import entityfilter
|
from homeassistant.helpers import entityfilter, config_validation as cv
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||||
@ -105,12 +104,7 @@ def async_setup(hass, config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start)
|
||||||
success = yield from cloud.initialize()
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
|
|
||||||
yield from http_api.async_setup(hass)
|
yield from http_api.async_setup(hass)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -192,19 +186,6 @@ class Cloud:
|
|||||||
|
|
||||||
return self._gactions_config
|
return self._gactions_config
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def initialize(self):
|
|
||||||
"""Initialize and load cloud info."""
|
|
||||||
jwt_success = yield from self._fetch_jwt_keyset()
|
|
||||||
|
|
||||||
if not jwt_success:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.hass.bus.async_listen_once(
|
|
||||||
EVENT_HOMEASSISTANT_START, self._start_cloud)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def path(self, *parts):
|
def path(self, *parts):
|
||||||
"""Get config path inside cloud dir.
|
"""Get config path inside cloud dir.
|
||||||
|
|
||||||
@ -234,8 +215,18 @@ class Cloud:
|
|||||||
'refresh_token': self.refresh_token,
|
'refresh_token': self.refresh_token,
|
||||||
}, indent=4))
|
}, indent=4))
|
||||||
|
|
||||||
def _start_cloud(self, event):
|
@asyncio.coroutine
|
||||||
|
def async_start(self, _):
|
||||||
"""Start the cloud component."""
|
"""Start the cloud component."""
|
||||||
|
success = yield from self._fetch_jwt_keyset()
|
||||||
|
|
||||||
|
# Fetching keyset can fail if internet is not up yet.
|
||||||
|
if not success:
|
||||||
|
self.hass.helpers.async_call_later(5, self.async_start)
|
||||||
|
return
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""Load config."""
|
||||||
# Ensure config dir exists
|
# Ensure config dir exists
|
||||||
path = self.hass.config.path(CONFIG_DIR)
|
path = self.hass.config.path(CONFIG_DIR)
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
@ -243,10 +234,15 @@ class Cloud:
|
|||||||
|
|
||||||
user_info = self.user_info_path
|
user_info = self.user_info_path
|
||||||
if not os.path.isfile(user_info):
|
if not os.path.isfile(user_info):
|
||||||
return
|
return None
|
||||||
|
|
||||||
with open(user_info, 'rt') as file:
|
with open(user_info, 'rt') as file:
|
||||||
info = json.loads(file.read())
|
return json.loads(file.read())
|
||||||
|
|
||||||
|
info = yield from self.hass.async_add_job(load_config)
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
return
|
||||||
|
|
||||||
# Validate tokens
|
# Validate tokens
|
||||||
try:
|
try:
|
||||||
|
@ -5,16 +5,17 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/coinbase/
|
https://home-assistant.io/components/coinbase/
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.util import Throttle
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.discovery import load_platform
|
from homeassistant.helpers.discovery import load_platform
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
REQUIREMENTS = ['coinbase==2.0.7']
|
||||||
|
|
||||||
REQUIREMENTS = ['coinbase==2.0.6']
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'coinbase'
|
DOMAIN = 'coinbase'
|
||||||
@ -46,14 +47,13 @@ def setup(hass, config):
|
|||||||
api_secret = config[DOMAIN].get(CONF_API_SECRET)
|
api_secret = config[DOMAIN].get(CONF_API_SECRET)
|
||||||
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
|
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
|
||||||
|
|
||||||
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key,
|
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(
|
||||||
api_secret)
|
api_key, api_secret)
|
||||||
|
|
||||||
if not hasattr(coinbase_data, 'accounts'):
|
if not hasattr(coinbase_data, 'accounts'):
|
||||||
return False
|
return False
|
||||||
for account in coinbase_data.accounts.data:
|
for account in coinbase_data.accounts.data:
|
||||||
load_platform(hass, 'sensor', DOMAIN,
|
load_platform(hass, 'sensor', DOMAIN, {'account': account}, config)
|
||||||
{'account': account}, config)
|
|
||||||
for currency in exchange_currencies:
|
for currency in exchange_currencies:
|
||||||
if currency not in coinbase_data.exchange_rates.rates:
|
if currency not in coinbase_data.exchange_rates.rates:
|
||||||
_LOGGER.warning("Currency %s not found", currency)
|
_LOGGER.warning("Currency %s not found", currency)
|
||||||
|
@ -68,7 +68,7 @@ class HMCover(HMDevice, CoverDevice):
|
|||||||
self._hmdevice.stop(self._channel)
|
self._hmdevice.stop(self._channel)
|
||||||
|
|
||||||
def _init_data_struct(self):
|
def _init_data_struct(self):
|
||||||
"""Generate a data dictoinary (self._data) from metadata."""
|
"""Generate a data dictionary (self._data) from metadata."""
|
||||||
self._state = "LEVEL"
|
self._state = "LEVEL"
|
||||||
self._data.update({self._state: STATE_UNKNOWN})
|
self._data.update({self._state: STATE_UNKNOWN})
|
||||||
if "LEVEL_2" in self._hmdevice.WRITENODE:
|
if "LEVEL_2" in self._hmdevice.WRITENODE:
|
||||||
|
@ -42,7 +42,7 @@ def setup_platform(hass, config: ConfigType,
|
|||||||
class ISYCoverDevice(ISYDevice, CoverDevice):
|
class ISYCoverDevice(ISYDevice, CoverDevice):
|
||||||
"""Representation of an ISY994 cover device."""
|
"""Representation of an ISY994 cover device."""
|
||||||
|
|
||||||
def __init__(self, node: object):
|
def __init__(self, node: object) -> None:
|
||||||
"""Initialize the ISY994 cover device."""
|
"""Initialize the ISY994 cover device."""
|
||||||
super().__init__(node)
|
super().__init__(node)
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_devices_config(hass, config, async_add_devices):
|
def async_add_devices_config(hass, config, async_add_devices):
|
||||||
"""Set up cover for KNX platform configured within plattform."""
|
"""Set up cover for KNX platform configured within platform."""
|
||||||
import xknx
|
import xknx
|
||||||
cover = xknx.devices.Cover(
|
cover = xknx.devices.Cover(
|
||||||
hass.data[DATA_KNX].xknx,
|
hass.data[DATA_KNX].xknx,
|
||||||
|
@ -63,7 +63,7 @@ class LutronCover(LutronDevice, CoverDevice):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Call when forcing a refresh of the device."""
|
"""Call when forcing a refresh of the device."""
|
||||||
# Reading the property (rather than last_level()) fetchs value
|
# Reading the property (rather than last_level()) fetches value
|
||||||
level = self._lutron_device.level
|
level = self._lutron_device.level
|
||||||
_LOGGER.debug("Lutron ID: %d updated to %f",
|
_LOGGER.debug("Lutron ID: %d updated to %f",
|
||||||
self._lutron_device.id, level)
|
self._lutron_device.id, level)
|
||||||
|
@ -214,16 +214,6 @@ class MqttCover(MqttAvailability, CoverDevice):
|
|||||||
|
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@callback
|
|
||||||
def availability_message_received(topic, payload, qos):
|
|
||||||
"""Handle new MQTT availability messages."""
|
|
||||||
if payload == self._payload_available:
|
|
||||||
self._available = True
|
|
||||||
elif payload == self._payload_not_available:
|
|
||||||
self._available = False
|
|
||||||
|
|
||||||
self.async_schedule_update_ha_state()
|
|
||||||
|
|
||||||
if self._state_topic is None:
|
if self._state_topic is None:
|
||||||
# Force into optimistic mode.
|
# Force into optimistic mode.
|
||||||
self._optimistic = True
|
self._optimistic = True
|
||||||
@ -232,11 +222,6 @@ class MqttCover(MqttAvailability, CoverDevice):
|
|||||||
self.hass, self._state_topic,
|
self.hass, self._state_topic,
|
||||||
state_message_received, self._qos)
|
state_message_received, self._qos)
|
||||||
|
|
||||||
if self._availability_topic is not None:
|
|
||||||
yield from mqtt.async_subscribe(
|
|
||||||
self.hass, self._availability_topic,
|
|
||||||
availability_message_received, self._qos)
|
|
||||||
|
|
||||||
if self._tilt_status_topic is None:
|
if self._tilt_status_topic is None:
|
||||||
self._tilt_optimistic = True
|
self._tilt_optimistic = True
|
||||||
else:
|
else:
|
||||||
|
@ -89,11 +89,6 @@ class RPiGPIOCover(CoverDevice):
|
|||||||
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
||||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return the ID of this cover."""
|
|
||||||
return '{}.{}'.format(self.__class__, self._name)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the cover if any."""
|
"""Return the name of the cover if any."""
|
||||||
|
@ -51,8 +51,8 @@ set_cover_tilt_position:
|
|||||||
entity_id:
|
entity_id:
|
||||||
description: Name(s) of cover(s) to set cover tilt position.
|
description: Name(s) of cover(s) to set cover tilt position.
|
||||||
example: 'cover.living_room'
|
example: 'cover.living_room'
|
||||||
position:
|
tilt_position:
|
||||||
description: Position of the cover (0 to 100).
|
description: Tilt position of the cover (0 to 100).
|
||||||
example: 30
|
example: 30
|
||||||
|
|
||||||
stop_cover_tilt:
|
stop_cover_tilt:
|
||||||
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/cover.tahoma/
|
https://home-assistant.io/components/cover.tahoma/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverDevice
|
from homeassistant.components.cover import CoverDevice
|
||||||
from homeassistant.components.tahoma import (
|
from homeassistant.components.tahoma import (
|
||||||
@ -15,8 +14,6 @@ DEPENDENCIES = ['tahoma']
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up Tahoma covers."""
|
"""Set up Tahoma covers."""
|
||||||
|
@ -67,11 +67,6 @@ COVER_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||||
})
|
})
|
||||||
|
|
||||||
COVER_SCHEMA = vol.All(
|
|
||||||
cv.deprecated(CONF_ENTITY_ID),
|
|
||||||
COVER_SCHEMA,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||||
})
|
})
|
||||||
|
@ -158,7 +158,7 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closing(self):
|
def is_closing(self):
|
||||||
"""Return true if cover is in an closing state."""
|
"""Return true if cover is in a closing state."""
|
||||||
return self._state == "Closing"
|
return self._state == "Closing"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.util.json import load_json, save_json
|
from homeassistant.util.json import load_json, save_json
|
||||||
|
|
||||||
REQUIREMENTS = ['pydeconz==25']
|
REQUIREMENTS = ['pydeconz==27']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ SERVICE_DATA = 'data'
|
|||||||
|
|
||||||
SERVICE_SCHEMA = vol.Schema({
|
SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(SERVICE_FIELD): cv.string,
|
vol.Required(SERVICE_FIELD): cv.string,
|
||||||
vol.Required(SERVICE_DATA): cv.string,
|
vol.Required(SERVICE_DATA): dict,
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_INSTRUCTIONS = """
|
CONFIG_INSTRUCTIONS = """
|
||||||
|
@ -42,7 +42,7 @@ Device = namedtuple('Device', ['mac', 'ip', 'last_update'])
|
|||||||
|
|
||||||
|
|
||||||
class ActiontecDeviceScanner(DeviceScanner):
|
class ActiontecDeviceScanner(DeviceScanner):
|
||||||
"""This class queries a an actiontec router for connected devices."""
|
"""This class queries an actiontec router for connected devices."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
|
@ -242,7 +242,7 @@ class _Connection:
|
|||||||
return self._connected
|
return self._connected
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Mark currenct connection state as connected."""
|
"""Mark current connection state as connected."""
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Support for HUAWEI routers.
|
Support for HUAWEI routers.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.huawei/
|
https://home-assistant.io/components/device_tracker.huawei_router/
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
@ -119,7 +119,7 @@ class HuaweiDeviceScanner(DeviceScanner):
|
|||||||
cnt = requests.post('http://{}/asp/GetRandCount.asp'.format(self.host))
|
cnt = requests.post('http://{}/asp/GetRandCount.asp'.format(self.host))
|
||||||
cnt_str = str(cnt.content, cnt.apparent_encoding, errors='replace')
|
cnt_str = str(cnt.content, cnt.apparent_encoding, errors='replace')
|
||||||
|
|
||||||
_LOGGER.debug("Loggin in")
|
_LOGGER.debug("Logging in")
|
||||||
cookie = requests.post('http://{}/login.cgi'.format(self.host),
|
cookie = requests.post('http://{}/login.cgi'.format(self.host),
|
||||||
data=[('UserName', self.username),
|
data=[('UserName', self.username),
|
||||||
('PassWord', self.password),
|
('PassWord', self.password),
|
||||||
|
71
homeassistant/components/device_tracker/mercedesme.py
Normal file
71
homeassistant/components/device_tracker/mercedesme.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Support for Mercedes cars with Mercedes ME.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.mercedesme/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.components.mercedesme import DATA_MME
|
||||||
|
from homeassistant.helpers.event import track_time_interval
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mercedesme']
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config, see, discovery_info=None):
|
||||||
|
"""Set up the Mercedes ME tracker."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
data = hass.data[DATA_MME].data
|
||||||
|
|
||||||
|
if not data.cars:
|
||||||
|
return False
|
||||||
|
|
||||||
|
MercedesMEDeviceTracker(hass, config, see, data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class MercedesMEDeviceTracker(object):
|
||||||
|
"""A class representing a Mercedes ME device tracker."""
|
||||||
|
|
||||||
|
def __init__(self, hass, config, see, data):
|
||||||
|
"""Initialize the Mercedes ME device tracker."""
|
||||||
|
self.see = see
|
||||||
|
self.data = data
|
||||||
|
self.update_info()
|
||||||
|
|
||||||
|
track_time_interval(
|
||||||
|
hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
|
def update_info(self, now=None):
|
||||||
|
"""Update the device info."""
|
||||||
|
for device in self.data.cars:
|
||||||
|
_LOGGER.debug("Updating %s", device["vin"])
|
||||||
|
location = self.data.get_location(device["vin"])
|
||||||
|
if location is None:
|
||||||
|
return False
|
||||||
|
dev_id = device["vin"]
|
||||||
|
name = device["license"]
|
||||||
|
|
||||||
|
lat = location['positionLat']['value']
|
||||||
|
lon = location['positionLong']['value']
|
||||||
|
attrs = {
|
||||||
|
'trackr_id': dev_id,
|
||||||
|
'id': dev_id,
|
||||||
|
'name': name
|
||||||
|
}
|
||||||
|
self.see(
|
||||||
|
dev_id=dev_id, host_name=name,
|
||||||
|
gps=(lat, lon), attributes=attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||||
|
|
||||||
REQUIREMENTS = ['librouteros==1.0.4']
|
REQUIREMENTS = ['librouteros==1.0.5']
|
||||||
|
|
||||||
MTK_DEFAULT_API_PORT = '8728'
|
MTK_DEFAULT_API_PORT = '8728'
|
||||||
|
|
||||||
|
@ -31,17 +31,14 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|||||||
devices = config[CONF_DEVICES]
|
devices = config[CONF_DEVICES]
|
||||||
qos = config[CONF_QOS]
|
qos = config[CONF_QOS]
|
||||||
|
|
||||||
dev_id_lookup = {}
|
for dev_id, topic in devices.items():
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_tracker_message_received(topic, payload, qos):
|
def async_message_received(topic, payload, qos, dev_id=dev_id):
|
||||||
"""Handle received MQTT message."""
|
"""Handle received MQTT message."""
|
||||||
hass.async_add_job(
|
hass.async_add_job(
|
||||||
async_see(dev_id=dev_id_lookup[topic], location_name=payload))
|
async_see(dev_id=dev_id, location_name=payload))
|
||||||
|
|
||||||
for dev_id, topic in devices.items():
|
|
||||||
dev_id_lookup[topic] = dev_id
|
|
||||||
yield from mqtt.async_subscribe(
|
yield from mqtt.async_subscribe(
|
||||||
hass, topic, async_tracker_message_received, qos)
|
hass, topic, async_message_received, qos)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -41,13 +41,10 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|||||||
devices = config[CONF_DEVICES]
|
devices = config[CONF_DEVICES]
|
||||||
qos = config[CONF_QOS]
|
qos = config[CONF_QOS]
|
||||||
|
|
||||||
dev_id_lookup = {}
|
for dev_id, topic in devices.items():
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_tracker_message_received(topic, payload, qos):
|
def async_message_received(topic, payload, qos, dev_id=dev_id):
|
||||||
"""Handle received MQTT message."""
|
"""Handle received MQTT message."""
|
||||||
dev_id = dev_id_lookup[topic]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
|
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
|
||||||
except vol.MultipleInvalid:
|
except vol.MultipleInvalid:
|
||||||
@ -60,13 +57,10 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|||||||
return
|
return
|
||||||
|
|
||||||
kwargs = _parse_see_args(dev_id, data)
|
kwargs = _parse_see_args(dev_id, data)
|
||||||
hass.async_add_job(
|
hass.async_add_job(async_see(**kwargs))
|
||||||
async_see(**kwargs))
|
|
||||||
|
|
||||||
for dev_id, topic in devices.items():
|
|
||||||
dev_id_lookup[topic] = dev_id
|
|
||||||
yield from mqtt.async_subscribe(
|
yield from mqtt.async_subscribe(
|
||||||
hass, topic, async_tracker_message_received, qos)
|
hass, topic, async_message_received, qos)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -143,6 +143,8 @@ def _parse_see_args(message, subscribe_topic):
|
|||||||
kwargs['attributes']['tid'] = message['tid']
|
kwargs['attributes']['tid'] = message['tid']
|
||||||
if 'addr' in message:
|
if 'addr' in message:
|
||||||
kwargs['attributes']['address'] = message['addr']
|
kwargs['attributes']['address'] = message['addr']
|
||||||
|
if 'cog' in message:
|
||||||
|
kwargs['attributes']['course'] = message['cog']
|
||||||
if 't' in message:
|
if 't' in message:
|
||||||
if message['t'] == 'c':
|
if message['t'] == 'c':
|
||||||
kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS
|
kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS
|
||||||
|
@ -46,8 +46,8 @@ def get_scanner(hass, config):
|
|||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
def _refresh_on_acccess_denied(func):
|
def _refresh_on_access_denied(func):
|
||||||
"""If remove rebooted, it lost our session so rebuld one and try again."""
|
"""If remove rebooted, it lost our session so rebuild one and try again."""
|
||||||
def decorator(self, *args, **kwargs):
|
def decorator(self, *args, **kwargs):
|
||||||
"""Wrap the function to refresh session_id on PermissionError."""
|
"""Wrap the function to refresh session_id on PermissionError."""
|
||||||
try:
|
try:
|
||||||
@ -95,16 +95,15 @@ class UbusDeviceScanner(DeviceScanner):
|
|||||||
"""Must be implemented depending on the software."""
|
"""Must be implemented depending on the software."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@_refresh_on_acccess_denied
|
@_refresh_on_access_denied
|
||||||
def get_device_name(self, mac):
|
def get_device_name(self, mac):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""Return the name of the given device or None if we don't know."""
|
||||||
if self.mac2name is None:
|
if self.mac2name is None:
|
||||||
self._generate_mac2name()
|
self._generate_mac2name()
|
||||||
name = self.mac2name.get(mac.upper(), None)
|
name = self.mac2name.get(mac.upper(), None)
|
||||||
self.mac2name = None
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@_refresh_on_acccess_denied
|
@_refresh_on_access_denied
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the router is up to date.
|
"""Ensure the information from the router is up to date.
|
||||||
|
|
||||||
@ -122,13 +121,18 @@ class UbusDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
results = 0
|
results = 0
|
||||||
|
# for each access point
|
||||||
for hostapd in self.hostapd:
|
for hostapd in self.hostapd:
|
||||||
result = _req_json_rpc(
|
result = _req_json_rpc(
|
||||||
self.url, self.session_id, 'call', hostapd, 'get_clients')
|
self.url, self.session_id, 'call', hostapd, 'get_clients')
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
results = results + 1
|
results = results + 1
|
||||||
self.last_results.extend(result['clients'].keys())
|
# Check for each device is authorized (valid wpa key)
|
||||||
|
for key in result['clients'].keys():
|
||||||
|
device = result['clients'][key]
|
||||||
|
if device['authorized']:
|
||||||
|
self.last_results.append(key)
|
||||||
|
|
||||||
return bool(results)
|
return bool(results)
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ def get_scanner(hass, config):
|
|||||||
class UnifiScanner(DeviceScanner):
|
class UnifiScanner(DeviceScanner):
|
||||||
"""Provide device_tracker support from Unifi WAP client data."""
|
"""Provide device_tracker support from Unifi WAP client data."""
|
||||||
|
|
||||||
def __init__(self, controller, detection_time: timedelta):
|
def __init__(self, controller, detection_time: timedelta) -> None:
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
self._detection_time = detection_time
|
self._detection_time = detection_time
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Support for Dominos Pizza ordering.
|
Support for Dominos Pizza ordering.
|
||||||
|
|
||||||
The Dominos Pizza component ceates a service which can be invoked to order
|
The Dominos Pizza component creates a service which can be invoked to order
|
||||||
from their menu
|
from their menu
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
|
@ -73,6 +73,7 @@ def setup(hass, config):
|
|||||||
class DoorbirdRequestView(HomeAssistantView):
|
class DoorbirdRequestView(HomeAssistantView):
|
||||||
"""Provide a page for the device to call."""
|
"""Provide a page for the device to call."""
|
||||||
|
|
||||||
|
requires_auth = False
|
||||||
url = API_URL
|
url = API_URL
|
||||||
name = API_URL[1:].replace('/', ':')
|
name = API_URL[1:].replace('/', ':')
|
||||||
extra_urls = [API_URL + '/{sensor}']
|
extra_urls = [API_URL + '/{sensor}']
|
||||||
|
@ -79,7 +79,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
if req.status_code != 200:
|
if req.status_code != 200:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"downloading '%s' failed, stauts_code=%d",
|
"downloading '%s' failed, status_code=%d",
|
||||||
url,
|
url,
|
||||||
req.status_code)
|
req.status_code)
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ class EightSleepUserEntity(Entity):
|
|||||||
"""The Eight Sleep device entity."""
|
"""The Eight Sleep device entity."""
|
||||||
|
|
||||||
def __init__(self, eight):
|
def __init__(self, eight):
|
||||||
"""Initialize the data oject."""
|
"""Initialize the data object."""
|
||||||
self._eight = eight
|
self._eight = eight
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -217,7 +217,7 @@ class EightSleepHeatEntity(Entity):
|
|||||||
"""The Eight Sleep device entity."""
|
"""The Eight Sleep device entity."""
|
||||||
|
|
||||||
def __init__(self, eight):
|
def __init__(self, eight):
|
||||||
"""Initialize the data oject."""
|
"""Initialize the data object."""
|
||||||
self._eight = eight
|
self._eight = eight
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -59,7 +59,7 @@ def setup(hass, config):
|
|||||||
payload, fullurl, req.status_code)
|
payload, fullurl, req.status_code)
|
||||||
|
|
||||||
def update_emoncms(time):
|
def update_emoncms(time):
|
||||||
"""Send whitelisted entities states reguarly to Emoncms."""
|
"""Send whitelisted entities states regularly to Emoncms."""
|
||||||
payload_dict = {}
|
payload_dict = {}
|
||||||
|
|
||||||
for entity_id in whitelist:
|
for entity_id in whitelist:
|
||||||
|
@ -39,6 +39,9 @@ CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
|
|||||||
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
|
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
|
||||||
CONF_EXPOSED_DOMAINS = 'exposed_domains'
|
CONF_EXPOSED_DOMAINS = 'exposed_domains'
|
||||||
CONF_TYPE = 'type'
|
CONF_TYPE = 'type'
|
||||||
|
CONF_ENTITIES = 'entities'
|
||||||
|
CONF_ENTITY_NAME = 'name'
|
||||||
|
CONF_ENTITY_HIDDEN = 'hidden'
|
||||||
|
|
||||||
TYPE_ALEXA = 'alexa'
|
TYPE_ALEXA = 'alexa'
|
||||||
TYPE_GOOGLE = 'google_home'
|
TYPE_GOOGLE = 'google_home'
|
||||||
@ -52,6 +55,11 @@ DEFAULT_EXPOSED_DOMAINS = [
|
|||||||
]
|
]
|
||||||
DEFAULT_TYPE = TYPE_GOOGLE
|
DEFAULT_TYPE = TYPE_GOOGLE
|
||||||
|
|
||||||
|
CONFIG_ENTITY_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_ENTITY_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean
|
||||||
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Optional(CONF_HOST_IP): cv.string,
|
vol.Optional(CONF_HOST_IP): cv.string,
|
||||||
@ -63,11 +71,14 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
|
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||||
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
|
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
|
||||||
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE):
|
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE):
|
||||||
vol.Any(TYPE_ALEXA, TYPE_GOOGLE)
|
vol.Any(TYPE_ALEXA, TYPE_GOOGLE),
|
||||||
|
vol.Optional(CONF_ENTITIES):
|
||||||
|
vol.Schema({cv.entity_id: CONFIG_ENTITY_SCHEMA})
|
||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
ATTR_EMULATED_HUE = 'emulated_hue'
|
||||||
|
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
|
||||||
ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
|
ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
|
||||||
|
|
||||||
|
|
||||||
@ -183,6 +194,8 @@ class Config(object):
|
|||||||
self.advertise_port = conf.get(
|
self.advertise_port = conf.get(
|
||||||
CONF_ADVERTISE_PORT) or self.listen_port
|
CONF_ADVERTISE_PORT) or self.listen_port
|
||||||
|
|
||||||
|
self.entities = conf.get(CONF_ENTITIES, {})
|
||||||
|
|
||||||
def entity_id_to_number(self, entity_id):
|
def entity_id_to_number(self, entity_id):
|
||||||
"""Get a unique number for the entity id."""
|
"""Get a unique number for the entity id."""
|
||||||
if self.type == TYPE_ALEXA:
|
if self.type == TYPE_ALEXA:
|
||||||
@ -215,6 +228,14 @@ class Config(object):
|
|||||||
assert isinstance(number, str)
|
assert isinstance(number, str)
|
||||||
return self.numbers.get(number)
|
return self.numbers.get(number)
|
||||||
|
|
||||||
|
def get_entity_name(self, entity):
|
||||||
|
"""Get the name of an entity."""
|
||||||
|
if entity.entity_id in self.entities and \
|
||||||
|
CONF_ENTITY_NAME in self.entities[entity.entity_id]:
|
||||||
|
return self.entities[entity.entity_id][CONF_ENTITY_NAME]
|
||||||
|
|
||||||
|
return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
||||||
|
|
||||||
def is_entity_exposed(self, entity):
|
def is_entity_exposed(self, entity):
|
||||||
"""Determine if an entity should be exposed on the emulated bridge.
|
"""Determine if an entity should be exposed on the emulated bridge.
|
||||||
|
|
||||||
@ -227,6 +248,12 @@ class Config(object):
|
|||||||
domain = entity.domain.lower()
|
domain = entity.domain.lower()
|
||||||
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
|
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
|
||||||
explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None)
|
explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None)
|
||||||
|
|
||||||
|
if entity.entity_id in self.entities and \
|
||||||
|
CONF_ENTITY_HIDDEN in self.entities[entity.entity_id]:
|
||||||
|
explicit_hidden = \
|
||||||
|
self.entities[entity.entity_id][CONF_ENTITY_HIDDEN]
|
||||||
|
|
||||||
if explicit_expose is True or explicit_hidden is False:
|
if explicit_expose is True or explicit_hidden is False:
|
||||||
expose = True
|
expose = True
|
||||||
elif explicit_expose is False or explicit_hidden is True:
|
elif explicit_expose is False or explicit_hidden is True:
|
||||||
|
@ -24,9 +24,6 @@ from homeassistant.components.http import HomeAssistantView
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
|
||||||
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
|
|
||||||
|
|
||||||
HUE_API_STATE_ON = 'on'
|
HUE_API_STATE_ON = 'on'
|
||||||
HUE_API_STATE_BRI = 'bri'
|
HUE_API_STATE_BRI = 'bri'
|
||||||
|
|
||||||
@ -77,7 +74,7 @@ class HueAllLightsStateView(HomeAssistantView):
|
|||||||
|
|
||||||
number = self.config.entity_id_to_number(entity.entity_id)
|
number = self.config.entity_id_to_number(entity.entity_id)
|
||||||
json_response[number] = entity_to_json(
|
json_response[number] = entity_to_json(
|
||||||
entity, state, brightness)
|
self.config, entity, state, brightness)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@ -110,7 +107,7 @@ class HueOneLightStateView(HomeAssistantView):
|
|||||||
|
|
||||||
state, brightness = get_entity_state(self.config, entity)
|
state, brightness = get_entity_state(self.config, entity)
|
||||||
|
|
||||||
json_response = entity_to_json(entity, state, brightness)
|
json_response = entity_to_json(self.config, entity, state, brightness)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@ -344,10 +341,8 @@ def get_entity_state(config, entity):
|
|||||||
return (final_state, final_brightness)
|
return (final_state, final_brightness)
|
||||||
|
|
||||||
|
|
||||||
def entity_to_json(entity, is_on=None, brightness=None):
|
def entity_to_json(config, entity, is_on=None, brightness=None):
|
||||||
"""Convert an entity to its Hue bridge JSON representation."""
|
"""Convert an entity to its Hue bridge JSON representation."""
|
||||||
name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'state':
|
'state':
|
||||||
{
|
{
|
||||||
@ -356,7 +351,7 @@ def entity_to_json(entity, is_on=None, brightness=None):
|
|||||||
'reachable': True
|
'reachable': True
|
||||||
},
|
},
|
||||||
'type': 'Dimmable light',
|
'type': 'Dimmable light',
|
||||||
'name': name,
|
'name': config.get_entity_name(entity),
|
||||||
'modelid': 'HASS123',
|
'modelid': 'HASS123',
|
||||||
'uniqueid': entity.entity_id,
|
'uniqueid': entity.entity_id,
|
||||||
'swversion': '123'
|
'swversion': '123'
|
||||||
|
@ -205,7 +205,7 @@ def async_setup(hass, config: dict):
|
|||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_fan_service(service):
|
def async_handle_fan_service(service):
|
||||||
"""Hande service call for fans."""
|
"""Handle service call for fans."""
|
||||||
method = SERVICE_TO_METHOD.get(service.service)
|
method = SERVICE_TO_METHOD.get(service.service)
|
||||||
params = service.data.copy()
|
params = service.data.copy()
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class ComfoConnectFan(FanEntity):
|
class ComfoConnectFan(FanEntity):
|
||||||
"""Representation of the ComfoConnect fan platform."""
|
"""Representation of the ComfoConnect fan platform."""
|
||||||
|
|
||||||
def __init__(self, hass, name, ccb: ComfoConnectBridge):
|
def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None:
|
||||||
"""Initialize the ComfoConnect fan."""
|
"""Initialize the ComfoConnect fan."""
|
||||||
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
|
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ class ComfoConnectFan(FanEntity):
|
|||||||
speed = SPEED_LOW
|
speed = SPEED_LOW
|
||||||
self.set_speed(speed)
|
self.set_speed(speed)
|
||||||
|
|
||||||
def turn_off(self) -> None:
|
def turn_off(self, **kwargs) -> None:
|
||||||
"""Turn off the fan (to away)."""
|
"""Turn off the fan (to away)."""
|
||||||
self.set_speed(SPEED_OFF)
|
self.set_speed(SPEED_OFF)
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class InsteonLocalFanDevice(FanEntity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the ID of this Insteon node."""
|
"""Return the ID of this Insteon node."""
|
||||||
return 'insteon_local_{}_fan'.format(self.node.device_id)
|
return self.node.device_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speed(self) -> str:
|
def speed(self) -> str:
|
||||||
|
@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
REQUIREMENTS = ['python-miio==0.3.4']
|
REQUIREMENTS = ['python-miio==0.3.5']
|
||||||
|
|
||||||
ATTR_TEMPERATURE = 'temperature'
|
ATTR_TEMPERATURE = 'temperature'
|
||||||
ATTR_HUMIDITY = 'humidity'
|
ATTR_HUMIDITY = 'humidity'
|
||||||
@ -200,7 +200,7 @@ class XiaomiAirPurifier(FanEntity):
|
|||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def _try_command(self, mask_error, func, *args, **kwargs):
|
def _try_command(self, mask_error, func, *args, **kwargs):
|
||||||
"""Call a air purifier command handling error messages."""
|
"""Call an air purifier command handling error messages."""
|
||||||
from miio import DeviceException
|
from miio import DeviceException
|
||||||
try:
|
try:
|
||||||
result = yield from self.hass.async_add_job(
|
result = yield from self.hass.async_add_job(
|
||||||
|
@ -153,8 +153,7 @@ class StoredData(object):
|
|||||||
with self._lock, open(self._data_file, 'rb') as myfile:
|
with self._lock, open(self._data_file, 'rb') as myfile:
|
||||||
self._data = pickle.load(myfile) or {}
|
self._data = pickle.load(myfile) or {}
|
||||||
self._cache_outdated = False
|
self._cache_outdated = False
|
||||||
# pylint: disable=bare-except
|
except: # noqa: E722 # pylint: disable=bare-except
|
||||||
except:
|
|
||||||
_LOGGER.error("Error loading data from pickled file %s",
|
_LOGGER.error("Error loading data from pickled file %s",
|
||||||
self._data_file)
|
self._data_file)
|
||||||
|
|
||||||
@ -172,8 +171,7 @@ class StoredData(object):
|
|||||||
url, self._data_file)
|
url, self._data_file)
|
||||||
try:
|
try:
|
||||||
pickle.dump(self._data, myfile)
|
pickle.dump(self._data, myfile)
|
||||||
# pylint: disable=bare-except
|
except: # noqa: E722 # pylint: disable=bare-except
|
||||||
except:
|
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Error saving pickled data to %s", self._data_file)
|
"Error saving pickled data to %s", self._data_file)
|
||||||
self._cache_outdated = True
|
self._cache_outdated = True
|
||||||
|
@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
REQUIREMENTS = ['home-assistant-frontend==20180130.0', 'user-agents==1.1.0']
|
REQUIREMENTS = ['home-assistant-frontend==20180209.0', 'user-agents==1.1.0']
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||||
@ -408,7 +408,7 @@ def async_setup_themes(hass, themes):
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def set_theme(call):
|
def set_theme(call):
|
||||||
"""Set backend-prefered theme."""
|
"""Set backend-preferred theme."""
|
||||||
data = call.data
|
data = call.data
|
||||||
name = data[CONF_NAME]
|
name = data[CONF_NAME]
|
||||||
if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]:
|
if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]:
|
||||||
@ -585,11 +585,13 @@ def _is_latest(js_option, request):
|
|||||||
return useragent.os.version[0] >= 12
|
return useragent.os.version[0] >= 12
|
||||||
|
|
||||||
family_min_version = {
|
family_min_version = {
|
||||||
'Chrome': 50, # Probably can reduce this
|
'Chrome': 54, # Object.values
|
||||||
'Firefox': 43, # Array.protopype.includes added in 43
|
'Chrome Mobile': 54,
|
||||||
'Opera': 40, # Probably can reduce this
|
'Firefox': 47, # Object.values
|
||||||
'Edge': 14, # Array.protopype.includes added in 14
|
'Firefox Mobile': 47,
|
||||||
'Safari': 10, # many features not supported by 9
|
'Opera': 41, # Object.values
|
||||||
|
'Edge': 14, # Array.prototype.includes added in 14
|
||||||
|
'Safari': 10, # Many features not supported by 9
|
||||||
}
|
}
|
||||||
version = family_min_version.get(useragent.browser.family)
|
version = family_min_version.get(useragent.browser.family)
|
||||||
return version and useragent.browser.version[0] >= version
|
return version and useragent.browser.version[0] >= version
|
||||||
|
62
homeassistant/components/goalfeed.py
Normal file
62
homeassistant/components/goalfeed.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Component for the Goalfeed service.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/goalfeed/
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pysher==0.2.0']
|
||||||
|
|
||||||
|
DOMAIN = 'goalfeed'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
GOALFEED_HOST = 'feed.goalfeed.ca'
|
||||||
|
GOALFEED_AUTH_ENDPOINT = 'https://goalfeed.ca/feed/auth'
|
||||||
|
GOALFEED_APP_ID = 'bfd4ed98c1ff22c04074'
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Set up the Goalfeed component."""
|
||||||
|
import pysher
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
username = conf.get(CONF_USERNAME)
|
||||||
|
password = conf.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
def goal_handler(data):
|
||||||
|
"""Handle goal events."""
|
||||||
|
goal = json.loads(json.loads(data))
|
||||||
|
|
||||||
|
hass.bus.fire('goal', event_data=goal)
|
||||||
|
|
||||||
|
def connect_handler(data):
|
||||||
|
"""Handle connection."""
|
||||||
|
post_data = {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'connection_info': data}
|
||||||
|
resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data,
|
||||||
|
timeout=30).json()
|
||||||
|
|
||||||
|
channel = pusher.subscribe('private-goals', resp['auth'])
|
||||||
|
channel.bind('goal', goal_handler)
|
||||||
|
|
||||||
|
pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080,
|
||||||
|
custom_host=GOALFEED_HOST)
|
||||||
|
|
||||||
|
pusher.connection.bind('pusher:connection_established', connect_handler)
|
||||||
|
pusher.connect()
|
||||||
|
|
||||||
|
return True
|
@ -128,7 +128,7 @@ def do_authentication(hass, config):
|
|||||||
"""Keep trying to validate the user_code until it expires."""
|
"""Keep trying to validate the user_code until it expires."""
|
||||||
if now >= dt.as_local(dev_flow.user_code_expiry):
|
if now >= dt.as_local(dev_flow.user_code_expiry):
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
'Authenication code expired, please restart '
|
'Authentication code expired, please restart '
|
||||||
'Home-Assistant and try again',
|
'Home-Assistant and try again',
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID)
|
||||||
|
@ -37,6 +37,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
HANDLERS = Registry()
|
HANDLERS = Registry()
|
||||||
|
QUERY_HANDLERS = Registry()
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Mapping is [actions schema, primary trait, optional features]
|
# Mapping is [actions schema, primary trait, optional features]
|
||||||
@ -79,7 +80,7 @@ class SmartHomeError(Exception):
|
|||||||
"""Log error code."""
|
"""Log error code."""
|
||||||
super(SmartHomeError, self).__init__(msg)
|
super(SmartHomeError, self).__init__(msg)
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"An error has ocurred in Google SmartHome: %s."
|
"An error has occurred in Google SmartHome: %s."
|
||||||
"Error code: %s", msg, code
|
"Error code: %s", msg, code
|
||||||
)
|
)
|
||||||
self.code = code
|
self.code = code
|
||||||
@ -96,7 +97,7 @@ class Config:
|
|||||||
|
|
||||||
|
|
||||||
def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
|
def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
|
||||||
"""Convert a hass entity into an google actions device."""
|
"""Convert a hass entity into a google actions device."""
|
||||||
entity_config = config.entity_config.get(entity.entity_id, {})
|
entity_config = config.entity_config.get(entity.entity_id, {})
|
||||||
google_domain = entity_config.get(CONF_TYPE)
|
google_domain = entity_config.get(CONF_TYPE)
|
||||||
class_data = MAPPING_COMPONENT.get(
|
class_data = MAPPING_COMPONENT.get(
|
||||||
@ -177,19 +178,26 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
|
def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]:
|
||||||
"""Take an entity and return a properly formatted device object."""
|
|
||||||
def celsius(deg: Optional[float]) -> Optional[float]:
|
|
||||||
"""Convert a float to Celsius and rounds to one decimal place."""
|
"""Convert a float to Celsius and rounds to one decimal place."""
|
||||||
if deg is None:
|
if deg is None:
|
||||||
return None
|
return None
|
||||||
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
||||||
|
|
||||||
if entity.domain == sensor.DOMAIN:
|
|
||||||
|
@QUERY_HANDLERS.register(sensor.DOMAIN)
|
||||||
|
def query_response_sensor(
|
||||||
|
entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||||
|
"""Convert a sensor entity to a QUERY response."""
|
||||||
entity_config = config.entity_config.get(entity.entity_id, {})
|
entity_config = config.entity_config.get(entity.entity_id, {})
|
||||||
google_domain = entity_config.get(CONF_TYPE)
|
google_domain = entity_config.get(CONF_TYPE)
|
||||||
|
|
||||||
if google_domain == climate.DOMAIN:
|
if google_domain != climate.DOMAIN:
|
||||||
|
raise SmartHomeError(
|
||||||
|
ERROR_NOT_SUPPORTED,
|
||||||
|
"Sensor type {} is not supported".format(google_domain)
|
||||||
|
)
|
||||||
|
|
||||||
# check if we have a string value to convert it to number
|
# check if we have a string value to convert it to number
|
||||||
value = entity.state
|
value = entity.state
|
||||||
if isinstance(entity.state, str):
|
if isinstance(entity.state, str):
|
||||||
@ -211,7 +219,7 @@ def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
|
|||||||
units.temperature_unit
|
units.temperature_unit
|
||||||
)
|
)
|
||||||
if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
|
if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
|
||||||
value = celsius(value)
|
value = celsius(value, units)
|
||||||
attr = 'thermostatTemperatureAmbient'
|
attr = 'thermostatTemperatureAmbient'
|
||||||
elif unit_of_measurement == '%':
|
elif unit_of_measurement == '%':
|
||||||
attr = 'thermostatHumidityAmbient'
|
attr = 'thermostatHumidityAmbient'
|
||||||
@ -224,73 +232,91 @@ def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
|
|||||||
|
|
||||||
return {attr: value}
|
return {attr: value}
|
||||||
|
|
||||||
raise SmartHomeError(
|
|
||||||
ERROR_NOT_SUPPORTED,
|
|
||||||
"Sensor type {} is not supported".format(google_domain)
|
|
||||||
)
|
|
||||||
|
|
||||||
if entity.domain == climate.DOMAIN:
|
@QUERY_HANDLERS.register(climate.DOMAIN)
|
||||||
|
def query_response_climate(
|
||||||
|
entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||||
|
"""Convert a climate entity to a QUERY response."""
|
||||||
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
|
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
|
||||||
if mode not in CLIMATE_SUPPORTED_MODES:
|
if mode not in CLIMATE_SUPPORTED_MODES:
|
||||||
mode = 'heat'
|
mode = 'heat'
|
||||||
|
attrs = entity.attributes
|
||||||
response = {
|
response = {
|
||||||
'thermostatMode': mode,
|
'thermostatMode': mode,
|
||||||
'thermostatTemperatureSetpoint':
|
'thermostatTemperatureSetpoint':
|
||||||
celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)),
|
celsius(attrs.get(climate.ATTR_TEMPERATURE), units),
|
||||||
'thermostatTemperatureAmbient':
|
'thermostatTemperatureAmbient':
|
||||||
celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)),
|
celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units),
|
||||||
'thermostatTemperatureSetpointHigh':
|
'thermostatTemperatureSetpointHigh':
|
||||||
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)),
|
celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units),
|
||||||
'thermostatTemperatureSetpointLow':
|
'thermostatTemperatureSetpointLow':
|
||||||
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)),
|
celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units),
|
||||||
'thermostatHumidityAmbient':
|
'thermostatHumidityAmbient':
|
||||||
entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY),
|
attrs.get(climate.ATTR_CURRENT_HUMIDITY),
|
||||||
}
|
}
|
||||||
return {k: v for k, v in response.items() if v is not None}
|
return {k: v for k, v in response.items() if v is not None}
|
||||||
|
|
||||||
final_state = entity.state != STATE_OFF
|
|
||||||
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
|
|
||||||
if final_state else 0)
|
|
||||||
|
|
||||||
if entity.domain == media_player.DOMAIN:
|
@QUERY_HANDLERS.register(media_player.DOMAIN)
|
||||||
level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL, 1.0
|
def query_response_media_player(
|
||||||
if final_state else 0.0)
|
entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||||
|
"""Convert a media_player entity to a QUERY response."""
|
||||||
|
level = entity.attributes.get(
|
||||||
|
media_player.ATTR_MEDIA_VOLUME_LEVEL,
|
||||||
|
1.0 if entity.state != STATE_OFF else 0.0)
|
||||||
# Convert 0.0-1.0 to 0-255
|
# Convert 0.0-1.0 to 0-255
|
||||||
final_brightness = round(min(1.0, level) * 255)
|
brightness = int(level * 100)
|
||||||
|
|
||||||
if final_brightness is None:
|
return {'brightness': brightness}
|
||||||
final_brightness = 255 if final_state else 0
|
|
||||||
|
|
||||||
final_brightness = 100 * (final_brightness / 255)
|
|
||||||
|
|
||||||
query_response = {
|
@QUERY_HANDLERS.register(light.DOMAIN)
|
||||||
"on": final_state,
|
def query_response_light(
|
||||||
"online": True,
|
entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||||
"brightness": int(final_brightness)
|
"""Convert a light entity to a QUERY response."""
|
||||||
}
|
response = {} # type: Dict[str, Any]
|
||||||
|
|
||||||
|
brightness = entity.attributes.get(light.ATTR_BRIGHTNESS)
|
||||||
|
if brightness is not None:
|
||||||
|
response['brightness'] = int(100 * (brightness / 255))
|
||||||
|
|
||||||
supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported_features & \
|
if supported_features & \
|
||||||
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
|
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
|
||||||
query_response["color"] = {}
|
response['color'] = {}
|
||||||
|
|
||||||
if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None:
|
if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None:
|
||||||
query_response["color"]["temperature"] = \
|
response['color']['temperature'] = \
|
||||||
int(round(color.color_temperature_mired_to_kelvin(
|
int(round(color.color_temperature_mired_to_kelvin(
|
||||||
entity.attributes.get(light.ATTR_COLOR_TEMP))))
|
entity.attributes.get(light.ATTR_COLOR_TEMP))))
|
||||||
|
|
||||||
if entity.attributes.get(light.ATTR_COLOR_NAME) is not None:
|
if entity.attributes.get(light.ATTR_COLOR_NAME) is not None:
|
||||||
query_response["color"]["name"] = \
|
response['color']['name'] = \
|
||||||
entity.attributes.get(light.ATTR_COLOR_NAME)
|
entity.attributes.get(light.ATTR_COLOR_NAME)
|
||||||
|
|
||||||
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
|
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
|
||||||
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
|
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
|
||||||
if color_rgb is not None:
|
if color_rgb is not None:
|
||||||
query_response["color"]["spectrumRGB"] = \
|
response['color']['spectrumRGB'] = \
|
||||||
int(color.color_rgb_to_hex(
|
int(color.color_rgb_to_hex(
|
||||||
color_rgb[0], color_rgb[1], color_rgb[2]), 16)
|
color_rgb[0], color_rgb[1], color_rgb[2]), 16)
|
||||||
|
|
||||||
return query_response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||||
|
"""Take an entity and return a properly formatted device object."""
|
||||||
|
state = entity.state != STATE_OFF
|
||||||
|
defaults = {
|
||||||
|
'on': state,
|
||||||
|
'online': True
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = QUERY_HANDLERS.get(entity.domain)
|
||||||
|
if callable(handler):
|
||||||
|
defaults.update(handler(entity, config, units))
|
||||||
|
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
|
||||||
# erroneous bug on old pythons and pylint
|
# erroneous bug on old pythons and pylint
|
||||||
@ -429,7 +455,7 @@ def async_devices_query(hass, config, payload):
|
|||||||
devices = {}
|
devices = {}
|
||||||
for device in payload.get('devices', []):
|
for device in payload.get('devices', []):
|
||||||
devid = device.get('id')
|
devid = device.get('id')
|
||||||
# In theory this should never happpen
|
# In theory this should never happen
|
||||||
if not devid:
|
if not devid:
|
||||||
_LOGGER.error('Device missing ID: %s', device)
|
_LOGGER.error('Device missing ID: %s', device)
|
||||||
continue
|
continue
|
||||||
@ -438,7 +464,7 @@ def async_devices_query(hass, config, payload):
|
|||||||
if not state:
|
if not state:
|
||||||
# If we can't find a state, the device is offline
|
# If we can't find a state, the device is offline
|
||||||
devices[devid] = {'online': False}
|
devices[devid] = {'online': False}
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
devices[devid] = query_device(state, config, hass.config.units)
|
devices[devid] = query_device(state, config, hass.config.units)
|
||||||
except SmartHomeError as error:
|
except SmartHomeError as error:
|
||||||
|
@ -247,7 +247,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
|||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up all groups found definded in the configuration."""
|
"""Set up all groups found defined in the configuration."""
|
||||||
component = hass.data.get(DOMAIN)
|
component = hass.data.get(DOMAIN)
|
||||||
|
|
||||||
if component is None:
|
if component is None:
|
||||||
@ -371,7 +371,6 @@ def async_setup(hass, config):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def _async_process_config(hass, config, component):
|
def _async_process_config(hass, config, component):
|
||||||
"""Process group configuration."""
|
"""Process group configuration."""
|
||||||
groups = []
|
|
||||||
for object_id, conf in config.get(DOMAIN, {}).items():
|
for object_id, conf in config.get(DOMAIN, {}).items():
|
||||||
name = conf.get(CONF_NAME, object_id)
|
name = conf.get(CONF_NAME, object_id)
|
||||||
entity_ids = conf.get(CONF_ENTITIES) or []
|
entity_ids = conf.get(CONF_ENTITIES) or []
|
||||||
@ -381,13 +380,9 @@ def _async_process_config(hass, config, component):
|
|||||||
|
|
||||||
# Don't create tasks and await them all. The order is important as
|
# Don't create tasks and await them all. The order is important as
|
||||||
# groups get a number based on creation order.
|
# groups get a number based on creation order.
|
||||||
group = yield from Group.async_create_group(
|
yield from Group.async_create_group(
|
||||||
hass, name, entity_ids, icon=icon, view=view,
|
hass, name, entity_ids, icon=icon, view=view,
|
||||||
control=control, object_id=object_id)
|
control=control, object_id=object_id)
|
||||||
groups.append(group)
|
|
||||||
|
|
||||||
if groups:
|
|
||||||
yield from component.async_add_entities(groups)
|
|
||||||
|
|
||||||
|
|
||||||
class Group(Entity):
|
class Group(Entity):
|
||||||
|
@ -320,7 +320,7 @@ def setup(hass: HomeAssistant, base_config):
|
|||||||
class CecDevice(Entity):
|
class CecDevice(Entity):
|
||||||
"""Representation of a HDMI CEC device entity."""
|
"""Representation of a HDMI CEC device entity."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, device, logical):
|
def __init__(self, hass: HomeAssistant, device, logical) -> None:
|
||||||
"""Initialize the device."""
|
"""Initialize the device."""
|
||||||
self._device = device
|
self._device = device
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
REQUIREMENTS = ['pyhomematic==0.1.38']
|
REQUIREMENTS = ['pyhomematic==0.1.39']
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -218,7 +218,7 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
|
|||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def virtualkey(hass, address, channel, param, interface=None):
|
def virtualkey(hass, address, channel, param, interface=None):
|
||||||
"""Send virtual keypress to homematic controlller."""
|
"""Send virtual keypress to homematic controller."""
|
||||||
data = {
|
data = {
|
||||||
ATTR_ADDRESS: address,
|
ATTR_ADDRESS: address,
|
||||||
ATTR_CHANNEL: channel,
|
ATTR_CHANNEL: channel,
|
||||||
@ -256,7 +256,7 @@ def set_device_value(hass, address, channel, param, value, interface=None):
|
|||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def set_install_mode(hass, interface, mode=None, time=None, address=None):
|
def set_install_mode(hass, interface, mode=None, time=None, address=None):
|
||||||
"""Call setInstallMode XML-RPC method of supplied inteface."""
|
"""Call setInstallMode XML-RPC method of supplied interface."""
|
||||||
data = {
|
data = {
|
||||||
key: value for key, value in (
|
key: value for key, value in (
|
||||||
(ATTR_INTERFACE, interface),
|
(ATTR_INTERFACE, interface),
|
||||||
@ -466,7 +466,7 @@ def _system_callback_handler(hass, config, src, *args):
|
|||||||
hass, discovery_type, addresses, interface)
|
hass, discovery_type, addresses, interface)
|
||||||
|
|
||||||
# When devices of this type are found
|
# When devices of this type are found
|
||||||
# they are setup in HASS and an discovery event is fired
|
# they are setup in HASS and a discovery event is fired
|
||||||
if found_devices:
|
if found_devices:
|
||||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||||
ATTR_DISCOVER_DEVICES: found_devices
|
ATTR_DISCOVER_DEVICES: found_devices
|
||||||
@ -665,7 +665,7 @@ class HMHub(Entity):
|
|||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _update_variables(self, now):
|
def _update_variables(self, now):
|
||||||
"""Retrive all variable data and update hmvariable states."""
|
"""Retrieve all variable data and update hmvariable states."""
|
||||||
variables = self._homematic.getAllSystemVariables(self._name)
|
variables = self._homematic.getAllSystemVariables(self._name)
|
||||||
if variables is None:
|
if variables is None:
|
||||||
return
|
return
|
||||||
|
@ -13,7 +13,7 @@ virtualkey:
|
|||||||
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
|
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
|
||||||
example: PRESS_LONG
|
example: PRESS_LONG
|
||||||
interface:
|
interface:
|
||||||
description: (Optional) for set a interface value.
|
description: (Optional) for set an interface value.
|
||||||
example: Interfaces name from config
|
example: Interfaces name from config
|
||||||
|
|
||||||
set_variable_value:
|
set_variable_value:
|
||||||
@ -42,7 +42,7 @@ set_device_value:
|
|||||||
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
|
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
|
||||||
example: PRESS_LONG
|
example: PRESS_LONG
|
||||||
interface:
|
interface:
|
||||||
description: (Optional) for set a interface value
|
description: (Optional) for set an interface value
|
||||||
example: Interfaces name from config
|
example: Interfaces name from config
|
||||||
value:
|
value:
|
||||||
description: New value
|
description: New value
|
||||||
|
@ -23,7 +23,7 @@ def auth_middleware(request, handler):
|
|||||||
# If no password set, just always set authenticated=True
|
# If no password set, just always set authenticated=True
|
||||||
if request.app['hass'].http.api_password is None:
|
if request.app['hass'].http.api_password is None:
|
||||||
request[KEY_AUTHENTICATED] = True
|
request[KEY_AUTHENTICATED] = True
|
||||||
return handler(request)
|
return (yield from handler(request))
|
||||||
|
|
||||||
# Check authentication
|
# Check authentication
|
||||||
authenticated = False
|
authenticated = False
|
||||||
@ -46,7 +46,7 @@ def auth_middleware(request, handler):
|
|||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
request[KEY_AUTHENTICATED] = authenticated
|
request[KEY_AUTHENTICATED] = authenticated
|
||||||
return handler(request)
|
return (yield from handler(request))
|
||||||
|
|
||||||
|
|
||||||
def is_trusted_ip(request):
|
def is_trusted_ip(request):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user