commit
0e16f7f307
|
@ -145,6 +145,9 @@ omit =
|
|||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/mercedesme.py
|
||||
homeassistant/components/*/mercedesme.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
|
@ -377,6 +380,7 @@ omit =
|
|||
homeassistant/components/fan/xiaomi_miio.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
|
@ -435,6 +439,7 @@ omit =
|
|||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/liveboxplaytv.py
|
||||
homeassistant/components/media_player/mediaroom.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
|
@ -449,7 +454,6 @@ omit =
|
|||
homeassistant/components/media_player/roku.py
|
||||
homeassistant/components/media_player/russound_rio.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
|
@ -506,6 +510,7 @@ omit =
|
|||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/remote/xiaomi_miio.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
|
@ -592,6 +597,7 @@ omit =
|
|||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pollen.py
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
|
|
|
@ -16,7 +16,9 @@ Icon
|
|||
# Thumbnails
|
||||
._*
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# pytest
|
||||
.cache
|
||||
|
@ -98,3 +100,6 @@ desktop.ini
|
|||
/home-assistant.pyproj
|
||||
/home-assistant.sln
|
||||
/.vs/*
|
||||
|
||||
# mypy
|
||||
/.mypy_cache/*
|
||||
|
|
|
@ -41,7 +41,7 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave
|
|||
|
||||
homeassistant/components/hassio.py @home-assistant/hassio
|
||||
|
||||
# Indiviudal components
|
||||
# Individual components
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
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/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
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://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://gitter.im/home-assistant/home-assistant">Gitter</a></li>
|
||||
<li><a href="https://discord.gg/c5DvZ4e">Discord</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -182,7 +182,8 @@ def check_pid(pid_file: str) -> None:
|
|||
"""Check that Home Assistant is not already running."""
|
||||
# Check pid file
|
||||
try:
|
||||
pid = int(open(pid_file, 'r').readline())
|
||||
with open(pid_file, 'r') as file:
|
||||
pid = int(file.readline())
|
||||
except IOError:
|
||||
# PID File does not exist
|
||||
return
|
||||
|
@ -204,7 +205,8 @@ def write_pid(pid_file: str) -> None:
|
|||
"""Create a PID File."""
|
||||
pid = os.getpid()
|
||||
try:
|
||||
open(pid_file, 'w').write(str(pid))
|
||||
with open(pid_file, 'w') as file:
|
||||
file.write(str(pid))
|
||||
except IOError:
|
||||
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
|
||||
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
|
||||
# second delay while we're blocking waiting for a response.
|
||||
# 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.
|
||||
blocking = hass.services.has_service(domain, service.service)
|
||||
|
||||
|
|
|
@ -277,7 +277,7 @@ class Alert(ToggleEntity):
|
|||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_toggle(self):
|
||||
def async_toggle(self, **kwargs):
|
||||
"""Async toggle alert."""
|
||||
if self._ack:
|
||||
return self.async_turn_on()
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.const import (
|
|||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -40,6 +40,7 @@ CONF_DESCRIPTION = 'description'
|
|||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||
|
||||
HANDLERS = Registry()
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
|
||||
|
||||
class _DisplayCategory(object):
|
||||
|
@ -50,8 +51,8 @@ class _DisplayCategory(object):
|
|||
|
||||
# Describes a combination of devices set to a specific state, when the
|
||||
# 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
|
||||
# HDMI1. Applies to Scenes
|
||||
# Netflix" scene might require the: 1. TV to be powered on & 2. Input set
|
||||
# to HDMI1. Applies to Scenes
|
||||
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
||||
|
||||
# Indicates media devices with video or photo capabilities.
|
||||
|
@ -133,10 +134,36 @@ def _capability(interface,
|
|||
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):
|
||||
self.config = config
|
||||
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):
|
||||
"""Return a list of display categories."""
|
||||
|
@ -154,17 +181,217 @@ class _EntityCapabilities(object):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def capabilities(self):
|
||||
"""Return a list of supported capabilities.
|
||||
def get_interface(self, capability):
|
||||
"""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
|
||||
|
||||
|
||||
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.
|
||||
|
||||
The choice of last resort.
|
||||
|
@ -173,78 +400,87 @@ class _GenericCapabilities(_EntityCapabilities):
|
|||
def default_display_categories(self):
|
||||
return [_DisplayCategory.OTHER]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.PowerController')]
|
||||
def interfaces(self):
|
||||
return [_AlexaPowerController(self.entity)]
|
||||
|
||||
|
||||
class _SwitchCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(switch.DOMAIN)
|
||||
class _SwitchCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SWITCH]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.PowerController')]
|
||||
def interfaces(self):
|
||||
return [_AlexaPowerController(self.entity)]
|
||||
|
||||
|
||||
class _CoverCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
class _CoverCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.DOOR]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
def interfaces(self):
|
||||
yield _AlexaPowerController(self.entity)
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & cover.SUPPORT_SET_POSITION:
|
||||
capabilities.append(_capability('Alexa.PercentageController'))
|
||||
return capabilities
|
||||
yield _AlexaPercentageController(self.entity)
|
||||
|
||||
|
||||
class _LightCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
||||
class _LightCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.LIGHT]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
def interfaces(self):
|
||||
yield _AlexaPowerController(self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & light.SUPPORT_BRIGHTNESS:
|
||||
capabilities.append(_capability('Alexa.BrightnessController'))
|
||||
yield _AlexaBrightnessController(self.entity)
|
||||
if supported & light.SUPPORT_RGB_COLOR:
|
||||
capabilities.append(_capability('Alexa.ColorController'))
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_XY_COLOR:
|
||||
capabilities.append(_capability('Alexa.ColorController'))
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
capabilities.append(
|
||||
_capability('Alexa.ColorTemperatureController'))
|
||||
return capabilities
|
||||
yield _AlexaColorTemperatureController(self.entity)
|
||||
|
||||
|
||||
class _FanCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
||||
class _FanCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.OTHER]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
def interfaces(self):
|
||||
yield _AlexaPowerController(self.entity)
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
capabilities.append(_capability('Alexa.PercentageController'))
|
||||
return capabilities
|
||||
yield _AlexaPercentageController(self.entity)
|
||||
|
||||
|
||||
class _LockCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||
class _LockCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SMARTLOCK]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.LockController')]
|
||||
def interfaces(self):
|
||||
return [_AlexaLockController(self.entity)]
|
||||
|
||||
|
||||
class _MediaPlayerCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||
class _MediaPlayerCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.TV]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
def interfaces(self):
|
||||
yield _AlexaPowerController(self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
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 |
|
||||
media_player.SUPPORT_PAUSE |
|
||||
|
@ -252,89 +488,62 @@ class _MediaPlayerCapabilities(_EntityCapabilities):
|
|||
media_player.SUPPORT_NEXT_TRACK |
|
||||
media_player.SUPPORT_PREVIOUS_TRACK)
|
||||
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):
|
||||
return [_DisplayCategory.SCENE_TRIGGER]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.SceneController')]
|
||||
def interfaces(self):
|
||||
return [_AlexaSceneController(self.entity,
|
||||
supports_deactivation=False)]
|
||||
|
||||
|
||||
class _ScriptCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
||||
class _ScriptCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
||||
|
||||
def capabilities(self):
|
||||
def interfaces(self):
|
||||
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
||||
return [_capability('Alexa.SceneController',
|
||||
supports_deactivation=can_cancel)]
|
||||
return [_AlexaSceneController(self.entity,
|
||||
supports_deactivation=can_cancel)]
|
||||
|
||||
|
||||
class _GroupCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
||||
class _GroupCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SCENE_TRIGGER]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.SceneController',
|
||||
supports_deactivation=True)]
|
||||
def interfaces(self):
|
||||
return [_AlexaSceneController(self.entity,
|
||||
supports_deactivation=True)]
|
||||
|
||||
|
||||
class _SensorCapabilities(_EntityCapabilities):
|
||||
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||
class _SensorCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
# although there are other kinds of sensors, all but temperature
|
||||
# sensors are currently ignored.
|
||||
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = []
|
||||
|
||||
def interfaces(self):
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
):
|
||||
capabilities.append(_capability(
|
||||
'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,
|
||||
}
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
|
||||
|
||||
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')
|
||||
if token:
|
||||
response[API_EVENT][API_HEADER]['correlationToken'] = token
|
||||
|
@ -511,36 +720,26 @@ def async_api_discovery(hass, config, request):
|
|||
entity.entity_id)
|
||||
continue
|
||||
|
||||
try:
|
||||
entity_capabilities = _capabilities_for_entity(config, entity)
|
||||
except _UnknownEntityDomainError:
|
||||
if entity.domain not in ENTITY_ADAPTERS:
|
||||
continue
|
||||
|
||||
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)
|
||||
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': entity_capabilities.display_categories(),
|
||||
'displayCategories': alexa_entity.display_categories(),
|
||||
'additionalApplianceDetails': {},
|
||||
'endpointId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyName': friendly_name,
|
||||
'description': description,
|
||||
'endpointId': alexa_entity.entity_id(),
|
||||
'friendlyName': alexa_entity.friendly_name(),
|
||||
'description': alexa_entity.description(),
|
||||
'manufacturerName': 'Home Assistant',
|
||||
}
|
||||
|
||||
alexa_capabilities = entity_capabilities.capabilities()
|
||||
if not alexa_capabilities:
|
||||
endpoint['capabilities'] = [
|
||||
i.serialize_discovery() for i in alexa_entity.interfaces()]
|
||||
|
||||
if not endpoint['capabilities']:
|
||||
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
||||
entity.entity_id)
|
||||
continue
|
||||
endpoint['capabilities'] = alexa_capabilities
|
||||
discovery_endpoints.append(endpoint)
|
||||
|
||||
return api_message(
|
||||
|
@ -624,7 +823,7 @@ def async_api_set_brightness(hass, config, request, entity):
|
|||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
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'])
|
||||
|
||||
# read current state
|
||||
|
@ -812,7 +1011,7 @@ def async_api_set_percentage(hass, config, request, entity):
|
|||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
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'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
@ -873,7 +1072,7 @@ def async_api_lock(hass, config, request, entity):
|
|||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
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, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
|
@ -900,11 +1099,46 @@ def async_api_set_volume(hass, config, request, entity):
|
|||
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'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
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'])
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
|
@ -1033,18 +1291,13 @@ def async_api_previous(hass, config, request, entity):
|
|||
@asyncio.coroutine
|
||||
def async_api_reportstate(hass, config, request, entity):
|
||||
"""Process a ReportState request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp_property = {
|
||||
'namespace': 'Alexa.TemperatureSensor',
|
||||
'name': 'temperature',
|
||||
'value': {
|
||||
'value': float(entity.state),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
}
|
||||
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||
properties = []
|
||||
for interface in alexa_entity.interfaces():
|
||||
properties.extend(interface.serialize_properties())
|
||||
|
||||
return api_message(
|
||||
request,
|
||||
name='StateReport',
|
||||
context={'properties': [temp_property]}
|
||||
context={'properties': properties}
|
||||
)
|
||||
|
|
|
@ -251,7 +251,7 @@ class AndroidIPCamEntity(Entity):
|
|||
"""The Android device running IP Webcam."""
|
||||
|
||||
def __init__(self, host, ipcam):
|
||||
"""Initialize the data oject."""
|
||||
"""Initialize the data object."""
|
||||
self._host = host
|
||||
self._ipcam = ipcam
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ class APCUPSdData(object):
|
|||
"""
|
||||
|
||||
def __init__(self, host, port):
|
||||
"""Initialize the data oject."""
|
||||
"""Initialize the data object."""
|
||||
from apcaccess import status
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
|
|
@ -47,7 +47,7 @@ def setup(hass, config):
|
|||
return False
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
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(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
|
|
|
@ -50,7 +50,6 @@ class BloomSkySensor(BinarySensorDevice):
|
|||
self._device_id = device['DeviceID']
|
||||
self._sensor_name = sensor_name
|
||||
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
|
||||
self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name)
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
|
@ -58,11 +57,6 @@ class BloomSkySensor(BinarySensorDevice):
|
|||
"""Return the name of the BloomSky device and this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
|
|
|
@ -65,6 +65,11 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
"""Return the name of the sensor."""
|
||||
return self._sensor.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this sensor."""
|
||||
return self._sensor.uniqueid
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the sensor."""
|
||||
|
|
|
@ -50,11 +50,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||
"""Return the status of the sensor."""
|
||||
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
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
|
|
|
@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
@asyncio.coroutine
|
||||
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]
|
||||
|
||||
if not manager.async_run_test(config.get(CONF_INPUT)):
|
||||
|
|
|
@ -118,7 +118,7 @@ class HikvisionData(object):
|
|||
"""Hikvision device event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
"""Initialize the data oject."""
|
||||
"""Initialize the data object."""
|
||||
from pyhik.hikvision import HikCamera
|
||||
self._url = url
|
||||
self._port = port
|
||||
|
@ -212,7 +212,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return '{}.{}'.format(self.__class__, self._id)
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -59,5 +59,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
|||
self.node_device_type)
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data frome Hive."""
|
||||
"""Update all Node data from Hive."""
|
||||
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,
|
||||
sensor_type: str, inverting: bool, product: Element=None):
|
||||
sensor_type: str, inverting: bool,
|
||||
product: Element=None) -> None:
|
||||
"""Initialize the IHC binary sensor."""
|
||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||
self._state = None
|
||||
|
|
|
@ -83,5 +83,5 @@ class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
|||
@callback
|
||||
def async_binarysensor_update(self, message):
|
||||
"""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())
|
||||
|
|
|
@ -67,8 +67,8 @@ def setup_platform(hass, config: ConfigType,
|
|||
elif subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif device_type == 'moisture':
|
||||
# Moisure nodes have a subnode 2, but we ignore it because it's
|
||||
# just the inverse of the primary node.
|
||||
# Moisture nodes have a subnode 2, but we ignore it because
|
||||
# it's just the inverse of the primary node.
|
||||
if subnode_id == 4:
|
||||
# Heartbeat node
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
|
|
|
@ -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 (
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
||||
CONF_DEVICE_CLASS)
|
||||
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
|
||||
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
|
||||
|
@ -24,8 +24,10 @@ import homeassistant.helpers.config_validation as cv
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
|
||||
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_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
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)
|
||||
|
||||
|
||||
|
@ -53,6 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_DEVICE_CLASS),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_FORCE_UPDATE),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
|
@ -65,7 +69,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
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):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
|
@ -77,6 +81,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
self._force_update = force_update
|
||||
self._template = value_template
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -94,6 +99,11 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||
self._state = True
|
||||
elif payload == self._payload_off:
|
||||
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()
|
||||
|
||||
|
@ -119,3 +129,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
|||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
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._sensor_name = sensor
|
||||
self._name += ' ' + sensor
|
||||
camera_id = data.camera_data.cameraByName(
|
||||
self._unique_id = data.camera_data.cameraByName(
|
||||
camera=camera_name, home=home)['id']
|
||||
self._unique_id = "Netatmo_binary_sensor {0} - {1}".format(
|
||||
self._name, camera_id)
|
||||
self._cameratype = camera_type
|
||||
self._state = None
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ class PilightBinarySensor(BinarySensorDevice):
|
|||
def _handle_code(self, call):
|
||||
"""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.
|
||||
"""
|
||||
# Check if received code matches defined playoad
|
||||
|
@ -162,10 +162,10 @@ class PilightTriggerSensor(BinarySensorDevice):
|
|||
def _handle_code(self, call):
|
||||
"""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.
|
||||
"""
|
||||
# Check if received code matches defined playoad
|
||||
# Check if received code matches defined payload
|
||||
# True if payload is contained in received code dict
|
||||
payload_ok = True
|
||||
for key in self._payload:
|
||||
|
|
|
@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
sensor_type))
|
||||
|
||||
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:
|
||||
sensors.append(RainCloudBinarySensor(zone, sensor_type))
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
|
|||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -29,6 +30,8 @@ CONF_DELAY_OFF = 'delay_off'
|
|||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
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_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
|
@ -38,11 +41,6 @@ SENSOR_SCHEMA = vol.Schema({
|
|||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
SENSOR_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
SENSOR_SCHEMA,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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():
|
||||
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
|
||||
value_template.extract_entities())
|
||||
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:
|
||||
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(
|
||||
BinarySensorTemplate(
|
||||
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:
|
||||
_LOGGER.error("No sensors added")
|
||||
|
@ -82,7 +90,8 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
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."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
@ -91,6 +100,10 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
self._device_class = device_class
|
||||
self._template = value_template
|
||||
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._delay_on = delay_on
|
||||
self._delay_off = delay_off
|
||||
|
@ -119,6 +132,16 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
"""Return the name of the sensor."""
|
||||
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
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
|
@ -137,8 +160,9 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
@callback
|
||||
def _async_render(self):
|
||||
"""Get the state of template."""
|
||||
state = None
|
||||
try:
|
||||
return self._template.async_render().lower() == 'true'
|
||||
state = (self._template.async_render().lower() == 'true')
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
|
@ -148,6 +172,29 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
return
|
||||
_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
|
||||
def async_check_state(self):
|
||||
"""Update the state from the template."""
|
||||
|
|
|
@ -126,11 +126,12 @@ class ThresholdSensor(BinarySensorDevice):
|
|||
@property
|
||||
def threshold_type(self):
|
||||
"""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
|
||||
elif self._threshold_lower:
|
||||
elif self._threshold_lower is not None:
|
||||
return TYPE_LOWER
|
||||
elif self._threshold_upper:
|
||||
elif self._threshold_upper is not None:
|
||||
return TYPE_UPPER
|
||||
|
||||
@property
|
||||
|
|
|
@ -58,11 +58,11 @@ class WemoBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this WeMo device."""
|
||||
return '{}.{}'.format(self.__class__, self.wemo.serialnumber)
|
||||
return self.wemo.serialnumber
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sevice if any."""
|
||||
"""Return the name of the service if any."""
|
||||
return self.wemo.name
|
||||
|
||||
@property
|
||||
|
|
|
@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
|
||||
|
@ -63,7 +63,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||
"""Initialize the ZHA binary sensor."""
|
||||
super().__init__(**kwargs)
|
||||
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]
|
||||
|
||||
@property
|
||||
|
@ -78,7 +78,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
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."""
|
||||
if command_id == 0:
|
||||
self._state = args[0] & 3
|
||||
|
|
|
@ -174,7 +174,7 @@ class WebDavCalendarData(object):
|
|||
|
||||
@staticmethod
|
||||
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:
|
||||
return True
|
||||
|
||||
|
|
|
@ -411,7 +411,7 @@ class TodoistProjectData(object):
|
|||
|
||||
The "best" event is determined by the following criteria:
|
||||
* 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)
|
||||
* A proposed event must be on the same day or earlier as our
|
||||
current event
|
||||
|
|
|
@ -91,13 +91,13 @@ def async_snapshot(hass, filename, entity_id=None):
|
|||
@bind_hass
|
||||
@asyncio.coroutine
|
||||
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)
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None:
|
||||
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(
|
||||
hass.config.api.base_url,
|
||||
|
|
|
@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
|||
_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."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
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
|
||||
https://home-assistant.io/components/camera.canary/
|
||||
"""
|
||||
import asyncio
|
||||
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.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__)
|
||||
|
||||
ATTR_MOTION_START_TIME = "motion_start_time"
|
||||
ATTR_MOTION_END_TIME = "motion_end_time"
|
||||
MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
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 = []
|
||||
|
||||
for location in data.locations:
|
||||
entries = data.get_motion_entries(location.location_id)
|
||||
if entries:
|
||||
devices.append(CanaryCamera(data, location.location_id,
|
||||
DEFAULT_TIMEOUT))
|
||||
for device in location.devices:
|
||||
if device.is_online:
|
||||
devices.append(
|
||||
CanaryCamera(hass, data, location, device, DEFAULT_TIMEOUT,
|
||||
config.get(CONF_FFMPEG_ARGUMENTS)))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
@ -36,60 +48,65 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class CanaryCamera(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."""
|
||||
super().__init__()
|
||||
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = ffmpeg_args
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
self._location = location
|
||||
self._device = device
|
||||
self._timeout = timeout
|
||||
|
||||
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
|
||||
self._live_stream_session = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._location.name
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device 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
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
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
|
||||
else:
|
||||
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(
|
||||
camera=camera_name
|
||||
)
|
||||
self._unique_id = data.camera_data.cameraByName(
|
||||
camera=camera_name, home=home)['id']
|
||||
self._cameratype = camera_type
|
||||
|
||||
def camera_image(self):
|
||||
|
@ -117,5 +115,5 @@ class NetatmoCamera(Camera):
|
|||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
"""Return the unique ID for this camera."""
|
||||
return self._unique_id
|
||||
|
|
|
@ -28,7 +28,7 @@ CONF_VERTICAL_FLIP = 'vertical_flip'
|
|||
|
||||
DEFAULT_HORIZONTAL_FLIP = 0
|
||||
DEFAULT_IMAGE_HEIGHT = 480
|
||||
DEFAULT_IMAGE_QUALITIY = 7
|
||||
DEFAULT_IMAGE_QUALITY = 7
|
||||
DEFAULT_IMAGE_ROTATION = 0
|
||||
DEFAULT_IMAGE_WIDTH = 640
|
||||
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.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
||||
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.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=359)),
|
||||
|
@ -131,7 +131,7 @@ class RaspberryCamera(Camera):
|
|||
stderr=subprocess.STDOUT)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return raspstill image response."""
|
||||
"""Return raspistill image response."""
|
||||
with open(self._config[CONF_FILE_PATH], 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
|
|
|
@ -127,6 +127,9 @@ class UnifiVideoCamera(Camera):
|
|||
else:
|
||||
client_cls = uvc_camera.UVCCameraClient
|
||||
|
||||
if caminfo['username'] is None:
|
||||
caminfo['username'] = 'ubnt'
|
||||
|
||||
camera = None
|
||||
for addr in addrs:
|
||||
try:
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
|||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyxeoma==1.2']
|
||||
REQUIREMENTS = ['pyxeoma==1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,6 +22,8 @@ CONF_CAMERAS = 'cameras'
|
|||
CONF_HIDE = 'hide'
|
||||
CONF_IMAGE_NAME = 'image_name'
|
||||
CONF_NEW_VERSION = 'new_version'
|
||||
CONF_VIEWER_PASSWORD = 'viewer_password'
|
||||
CONF_VIEWER_USERNAME = 'viewer_username'
|
||||
|
||||
CAMERAS_SCHEMA = vol.Schema({
|
||||
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]
|
||||
login = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
new_version = config[CONF_NEW_VERSION]
|
||||
|
||||
xeoma = Xeoma(host, new_version, login, password)
|
||||
xeoma = Xeoma(host, login, password)
|
||||
|
||||
try:
|
||||
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_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]:
|
||||
|
@ -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))
|
||||
async_add_devices(
|
||||
[XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME])
|
||||
for camera in cameras])
|
||||
[XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME],
|
||||
camera[CONF_VIEWER_USERNAME],
|
||||
camera[CONF_VIEWER_PASSWORD]) for camera in cameras])
|
||||
except XeomaError as err:
|
||||
_LOGGER.error("Error: %s", err.message)
|
||||
return
|
||||
|
@ -87,12 +92,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
class XeomaCamera(Camera):
|
||||
"""Implementation of a Xeoma camera."""
|
||||
|
||||
def __init__(self, xeoma, image, name):
|
||||
def __init__(self, xeoma, image, name, username, password):
|
||||
"""Initialize a Xeoma camera."""
|
||||
super().__init__()
|
||||
self._xeoma = xeoma
|
||||
self._name = name
|
||||
self._image = image
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._last_image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -100,7 +107,8 @@ class XeomaCamera(Camera):
|
|||
"""Return a still image response from the camera."""
|
||||
from pyxeoma.xeoma import XeomaError
|
||||
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
|
||||
except XeomaError as err:
|
||||
_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.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['py-canary==0.2.3']
|
||||
REQUIREMENTS = ['py-canary==0.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -111,7 +111,18 @@ class CanaryData(object):
|
|||
"""Return a list of readings based on 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):
|
||||
"""Set location mode."""
|
||||
self._api.set_location_mode(location_id, mode_name, is_private)
|
||||
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._api.device.set(values)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this AC."""
|
||||
return "{}.{}".format(self.__class__, self._api.ip_address)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
|
|
|
@ -14,23 +14,19 @@ from homeassistant.components.climate import (
|
|||
SUPPORT_ON_OFF)
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
|
||||
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)
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo climate devices."""
|
||||
add_devices([
|
||||
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',
|
||||
67, 54, 'Off', 'cool', False, None, None),
|
||||
DemoClimate('Ecobee', None, TEMP_CELSIUS, None, None, 23, 'Auto Low',
|
||||
None, None, 'Auto', 'auto', None, 24, 21)
|
||||
67, 54, 'Off', 'cool', False, None, None, None),
|
||||
DemoClimate('Ecobee', None, TEMP_CELSIUS, None, 'home', 23, 'Auto Low',
|
||||
None, None, 'Auto', 'auto', None, 24, 21, None)
|
||||
])
|
||||
|
||||
|
||||
|
@ -40,9 +36,37 @@ class DemoClimate(ClimateDevice):
|
|||
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||
away, hold, current_temperature, current_fan_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."""
|
||||
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_humidity = target_humidity
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
@ -59,12 +83,12 @@ class DemoClimate(ClimateDevice):
|
|||
self._swing_list = ['Auto', '1', '2', '3', 'Off']
|
||||
self._target_temperature_high = target_temp_high
|
||||
self._target_temperature_low = target_temp_low
|
||||
self._on = True
|
||||
self._on = is_on
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -207,7 +231,7 @@ class DemoClimate(ClimateDevice):
|
|||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
"""Turn auxiliary heater on."""
|
||||
self._aux = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
|
|
@ -13,7 +13,9 @@ from homeassistant.components.climate import (
|
|||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
||||
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 (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
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_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):
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||
TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyeconet==0.0.4']
|
||||
REQUIREMENTS = ['pyeconet==0.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
# pylint: disable=import-error
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
||||
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
|
|
|
@ -156,7 +156,7 @@ class GenericThermostat(ClimateDevice):
|
|||
# If we have no initial temperature, restore
|
||||
if self._target_temp is None:
|
||||
# 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:
|
||||
self._target_temp = self.max_temp
|
||||
else:
|
||||
|
@ -166,15 +166,15 @@ class GenericThermostat(ClimateDevice):
|
|||
else:
|
||||
self._target_temp = float(
|
||||
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(
|
||||
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON
|
||||
if (self._initial_operation_mode is None and
|
||||
old_state.attributes[ATTR_OPERATION_MODE] is not None):
|
||||
self._current_operation = \
|
||||
old_state.attributes[ATTR_OPERATION_MODE]
|
||||
if self._current_operation != STATE_OFF:
|
||||
self._enabled = True
|
||||
self._enabled = self._current_operation != STATE_OFF
|
||||
|
||||
else:
|
||||
# No previous state, try and restore defaults
|
||||
if self._target_temp is None:
|
||||
|
@ -249,7 +249,7 @@ class GenericThermostat(ClimateDevice):
|
|||
else:
|
||||
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
|
||||
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()
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
|
@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
serport = connection.connection(ipaddress, port)
|
||||
serport.open()
|
||||
|
||||
for thermostat, tstat in tstats.items():
|
||||
for tstat in tstats.values():
|
||||
add_devices([
|
||||
HeatmiserV3Thermostat(
|
||||
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
||||
|
|
|
@ -174,5 +174,5 @@ class HiveClimateEntity(ClimateDevice):
|
|||
entity.handle_update(self.data_updatesource)
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data frome Hive."""
|
||||
"""Update all Node data from Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
|
|
|
@ -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
|
||||
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:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
|
@ -576,7 +576,7 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
|||
|
||||
@asyncio.coroutine
|
||||
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:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
|
|
@ -139,8 +139,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
|||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, value_type, value)
|
||||
if self.gateway.optimistic:
|
||||
# O
|
||||
# ptimistically assume that device has changed state
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[value_type] = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
|
|
@ -97,6 +97,11 @@ class NestThermostat(ClimateDevice):
|
|||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Unique ID for this device."""
|
||||
return self.device.serial
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
|
|
|
@ -24,7 +24,7 @@ CONF_RELAY = 'relay'
|
|||
CONF_THERMOSTAT = 'thermostat'
|
||||
|
||||
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
|
||||
# # Return cached results if last scan was less then this time ago
|
||||
# # 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):
|
||||
"""Interface class for the oemthermostat modul."""
|
||||
"""Interface class for the oemthermostat module."""
|
||||
|
||||
def __init__(self, hass, thermostat, name, away_temp):
|
||||
"""Initialize the device."""
|
||||
|
|
|
@ -249,7 +249,7 @@ class TadoClimate(ClimateDevice):
|
|||
data = self._store.get_data(self._data_id)
|
||||
|
||||
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
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
|
@ -294,7 +294,7 @@ class TadoClimate(ClimateDevice):
|
|||
|
||||
overlay = False
|
||||
overlay_data = None
|
||||
termination = self._current_operation
|
||||
termination = CONST_MODE_SMART_SCHEDULE
|
||||
cooling = False
|
||||
fan_speed = CONST_MODE_OFF
|
||||
|
||||
|
@ -317,7 +317,7 @@ class TadoClimate(ClimateDevice):
|
|||
fan_speed = setting_data['fanSpeed']
|
||||
|
||||
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"
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.components.climate import (
|
|||
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pytouchline==0.6']
|
||||
REQUIREMENTS = ['pytouchline==0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
|||
TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['venstarcolortouch==0.5']
|
||||
REQUIREMENTS = ['venstarcolortouch==0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -16,8 +16,7 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
|
||||
from homeassistant.helpers import entityfilter
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import entityfilter, config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
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)
|
||||
|
||||
success = yield from cloud.initialize()
|
||||
|
||||
if not success:
|
||||
return False
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start)
|
||||
yield from http_api.async_setup(hass)
|
||||
return True
|
||||
|
||||
|
@ -192,19 +186,6 @@ class Cloud:
|
|||
|
||||
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):
|
||||
"""Get config path inside cloud dir.
|
||||
|
||||
|
@ -234,19 +215,34 @@ class Cloud:
|
|||
'refresh_token': self.refresh_token,
|
||||
}, indent=4))
|
||||
|
||||
def _start_cloud(self, event):
|
||||
@asyncio.coroutine
|
||||
def async_start(self, _):
|
||||
"""Start the cloud component."""
|
||||
# Ensure config dir exists
|
||||
path = self.hass.config.path(CONFIG_DIR)
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
success = yield from self._fetch_jwt_keyset()
|
||||
|
||||
user_info = self.user_info_path
|
||||
if not os.path.isfile(user_info):
|
||||
# Fetching keyset can fail if internet is not up yet.
|
||||
if not success:
|
||||
self.hass.helpers.async_call_later(5, self.async_start)
|
||||
return
|
||||
|
||||
with open(user_info, 'rt') as file:
|
||||
info = json.loads(file.read())
|
||||
def load_config():
|
||||
"""Load config."""
|
||||
# Ensure config dir exists
|
||||
path = self.hass.config.path(CONFIG_DIR)
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
|
||||
user_info = self.user_info_path
|
||||
if not os.path.isfile(user_info):
|
||||
return None
|
||||
|
||||
with open(user_info, 'rt') as file:
|
||||
return json.loads(file.read())
|
||||
|
||||
info = yield from self.hass.async_add_job(load_config)
|
||||
|
||||
if info is None:
|
||||
return
|
||||
|
||||
# Validate tokens
|
||||
try:
|
||||
|
|
|
@ -5,16 +5,17 @@ For more details about this component, please refer to the documentation at
|
|||
https://home-assistant.io/components/coinbase/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['coinbase==2.0.7']
|
||||
|
||||
REQUIREMENTS = ['coinbase==2.0.6']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'coinbase'
|
||||
|
@ -46,14 +47,13 @@ def setup(hass, config):
|
|||
api_secret = config[DOMAIN].get(CONF_API_SECRET)
|
||||
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
|
||||
|
||||
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key,
|
||||
api_secret)
|
||||
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(
|
||||
api_key, api_secret)
|
||||
|
||||
if not hasattr(coinbase_data, 'accounts'):
|
||||
return False
|
||||
for account in coinbase_data.accounts.data:
|
||||
load_platform(hass, 'sensor', DOMAIN,
|
||||
{'account': account}, config)
|
||||
load_platform(hass, 'sensor', DOMAIN, {'account': account}, config)
|
||||
for currency in exchange_currencies:
|
||||
if currency not in coinbase_data.exchange_rates.rates:
|
||||
_LOGGER.warning("Currency %s not found", currency)
|
||||
|
|
|
@ -68,7 +68,7 @@ class HMCover(HMDevice, CoverDevice):
|
|||
self._hmdevice.stop(self._channel)
|
||||
|
||||
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._data.update({self._state: STATE_UNKNOWN})
|
||||
if "LEVEL_2" in self._hmdevice.WRITENODE:
|
||||
|
|
|
@ -42,7 +42,7 @@ def setup_platform(hass, config: ConfigType,
|
|||
class ISYCoverDevice(ISYDevice, CoverDevice):
|
||||
"""Representation of an ISY994 cover device."""
|
||||
|
||||
def __init__(self, node: object):
|
||||
def __init__(self, node: object) -> None:
|
||||
"""Initialize the ISY994 cover device."""
|
||||
super().__init__(node)
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
|||
|
||||
@callback
|
||||
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
|
||||
cover = xknx.devices.Cover(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
|
|
|
@ -63,7 +63,7 @@ class LutronCover(LutronDevice, CoverDevice):
|
|||
|
||||
def update(self):
|
||||
"""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
|
||||
_LOGGER.debug("Lutron ID: %d updated to %f",
|
||||
self._lutron_device.id, level)
|
||||
|
|
|
@ -214,16 +214,6 @@ class MqttCover(MqttAvailability, CoverDevice):
|
|||
|
||||
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:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
|
@ -232,11 +222,6 @@ class MqttCover(MqttAvailability, CoverDevice):
|
|||
self.hass, self._state_topic,
|
||||
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:
|
||||
self._tilt_optimistic = True
|
||||
else:
|
||||
|
|
|
@ -89,11 +89,6 @@ class RPiGPIOCover(CoverDevice):
|
|||
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
||||
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
|
||||
def name(self):
|
||||
"""Return the name of the cover if any."""
|
||||
|
|
|
@ -51,8 +51,8 @@ set_cover_tilt_position:
|
|||
entity_id:
|
||||
description: Name(s) of cover(s) to set cover tilt position.
|
||||
example: 'cover.living_room'
|
||||
position:
|
||||
description: Position of the cover (0 to 100).
|
||||
tilt_position:
|
||||
description: Tilt position of the cover (0 to 100).
|
||||
example: 30
|
||||
|
||||
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/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.tahoma import (
|
||||
|
@ -15,8 +14,6 @@ DEPENDENCIES = ['tahoma']
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tahoma covers."""
|
||||
|
|
|
@ -67,11 +67,6 @@ COVER_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
COVER_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
COVER_SCHEMA,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
|
|
@ -158,7 +158,7 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase):
|
|||
|
||||
@property
|
||||
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"
|
||||
|
||||
@property
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==25']
|
||||
REQUIREMENTS = ['pydeconz==27']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -38,7 +38,7 @@ SERVICE_DATA = 'data'
|
|||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(SERVICE_FIELD): cv.string,
|
||||
vol.Required(SERVICE_DATA): cv.string,
|
||||
vol.Required(SERVICE_DATA): dict,
|
||||
})
|
||||
|
||||
CONFIG_INSTRUCTIONS = """
|
||||
|
|
|
@ -42,7 +42,7 @@ Device = namedtuple('Device', ['mac', 'ip', 'last_update'])
|
|||
|
||||
|
||||
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):
|
||||
"""Initialize the scanner."""
|
||||
|
|
|
@ -242,7 +242,7 @@ class _Connection:
|
|||
return self._connected
|
||||
|
||||
def connect(self):
|
||||
"""Mark currenct connection state as connected."""
|
||||
"""Mark current connection state as connected."""
|
||||
self._connected = True
|
||||
|
||||
def disconnect(self):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Support for HUAWEI routers.
|
||||
|
||||
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 logging
|
||||
|
@ -119,7 +119,7 @@ class HuaweiDeviceScanner(DeviceScanner):
|
|||
cnt = requests.post('http://{}/asp/GetRandCount.asp'.format(self.host))
|
||||
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),
|
||||
data=[('UserName', self.username),
|
||||
('PassWord', self.password),
|
||||
|
|
|
@ -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 (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['librouteros==1.0.4']
|
||||
REQUIREMENTS = ['librouteros==1.0.5']
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
|
||||
|
|
|
@ -31,17 +31,14 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
@callback
|
||||
def async_tracker_message_received(topic, payload, qos):
|
||||
"""Handle received MQTT message."""
|
||||
hass.async_add_job(
|
||||
async_see(dev_id=dev_id_lookup[topic], location_name=payload))
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
@callback
|
||||
def async_message_received(topic, payload, qos, dev_id=dev_id):
|
||||
"""Handle received MQTT message."""
|
||||
hass.async_add_job(
|
||||
async_see(dev_id=dev_id, location_name=payload))
|
||||
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, topic, async_tracker_message_received, qos)
|
||||
hass, topic, async_message_received, qos)
|
||||
|
||||
return True
|
||||
|
|
|
@ -41,32 +41,26 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
@callback
|
||||
def async_tracker_message_received(topic, payload, qos):
|
||||
"""Handle received MQTT message."""
|
||||
dev_id = dev_id_lookup[topic]
|
||||
|
||||
try:
|
||||
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
|
||||
except vol.MultipleInvalid:
|
||||
_LOGGER.error("Skipping update for following data "
|
||||
"because of missing or malformatted data: %s",
|
||||
payload)
|
||||
return
|
||||
except ValueError:
|
||||
_LOGGER.error("Error parsing JSON payload: %s", payload)
|
||||
return
|
||||
|
||||
kwargs = _parse_see_args(dev_id, data)
|
||||
hass.async_add_job(
|
||||
async_see(**kwargs))
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
@callback
|
||||
def async_message_received(topic, payload, qos, dev_id=dev_id):
|
||||
"""Handle received MQTT message."""
|
||||
try:
|
||||
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
|
||||
except vol.MultipleInvalid:
|
||||
_LOGGER.error("Skipping update for following data "
|
||||
"because of missing or malformatted data: %s",
|
||||
payload)
|
||||
return
|
||||
except ValueError:
|
||||
_LOGGER.error("Error parsing JSON payload: %s", payload)
|
||||
return
|
||||
|
||||
kwargs = _parse_see_args(dev_id, data)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, topic, async_tracker_message_received, qos)
|
||||
hass, topic, async_message_received, qos)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -143,6 +143,8 @@ def _parse_see_args(message, subscribe_topic):
|
|||
kwargs['attributes']['tid'] = message['tid']
|
||||
if 'addr' in message:
|
||||
kwargs['attributes']['address'] = message['addr']
|
||||
if 'cog' in message:
|
||||
kwargs['attributes']['course'] = message['cog']
|
||||
if 't' in message:
|
||||
if message['t'] == 'c':
|
||||
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
|
||||
|
||||
|
||||
def _refresh_on_acccess_denied(func):
|
||||
"""If remove rebooted, it lost our session so rebuld one and try again."""
|
||||
def _refresh_on_access_denied(func):
|
||||
"""If remove rebooted, it lost our session so rebuild one and try again."""
|
||||
def decorator(self, *args, **kwargs):
|
||||
"""Wrap the function to refresh session_id on PermissionError."""
|
||||
try:
|
||||
|
@ -95,16 +95,15 @@ class UbusDeviceScanner(DeviceScanner):
|
|||
"""Must be implemented depending on the software."""
|
||||
raise NotImplementedError
|
||||
|
||||
@_refresh_on_acccess_denied
|
||||
@_refresh_on_access_denied
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if self.mac2name is None:
|
||||
self._generate_mac2name()
|
||||
name = self.mac2name.get(mac.upper(), None)
|
||||
self.mac2name = None
|
||||
return name
|
||||
|
||||
@_refresh_on_acccess_denied
|
||||
@_refresh_on_access_denied
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
|
@ -122,13 +121,18 @@ class UbusDeviceScanner(DeviceScanner):
|
|||
|
||||
self.last_results = []
|
||||
results = 0
|
||||
# for each access point
|
||||
for hostapd in self.hostapd:
|
||||
result = _req_json_rpc(
|
||||
self.url, self.session_id, 'call', hostapd, 'get_clients')
|
||||
|
||||
if result:
|
||||
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)
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ def get_scanner(hass, config):
|
|||
class UnifiScanner(DeviceScanner):
|
||||
"""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."""
|
||||
self._detection_time = detection_time
|
||||
self._controller = controller
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
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
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
|
|
|
@ -73,6 +73,7 @@ def setup(hass, config):
|
|||
class DoorbirdRequestView(HomeAssistantView):
|
||||
"""Provide a page for the device to call."""
|
||||
|
||||
requires_auth = False
|
||||
url = API_URL
|
||||
name = API_URL[1:].replace('/', ':')
|
||||
extra_urls = [API_URL + '/{sensor}']
|
||||
|
|
|
@ -79,7 +79,7 @@ def setup(hass, config):
|
|||
|
||||
if req.status_code != 200:
|
||||
_LOGGER.warning(
|
||||
"downloading '%s' failed, stauts_code=%d",
|
||||
"downloading '%s' failed, status_code=%d",
|
||||
url,
|
||||
req.status_code)
|
||||
|
||||
|
|
|
@ -193,7 +193,7 @@ class EightSleepUserEntity(Entity):
|
|||
"""The Eight Sleep device entity."""
|
||||
|
||||
def __init__(self, eight):
|
||||
"""Initialize the data oject."""
|
||||
"""Initialize the data object."""
|
||||
self._eight = eight
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -217,7 +217,7 @@ class EightSleepHeatEntity(Entity):
|
|||
"""The Eight Sleep device entity."""
|
||||
|
||||
def __init__(self, eight):
|
||||
"""Initialize the data oject."""
|
||||
"""Initialize the data object."""
|
||||
self._eight = eight
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
|
@ -59,7 +59,7 @@ def setup(hass, config):
|
|||
payload, fullurl, req.status_code)
|
||||
|
||||
def update_emoncms(time):
|
||||
"""Send whitelisted entities states reguarly to Emoncms."""
|
||||
"""Send whitelisted entities states regularly to Emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
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_EXPOSED_DOMAINS = 'exposed_domains'
|
||||
CONF_TYPE = 'type'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_ENTITY_NAME = 'name'
|
||||
CONF_ENTITY_HIDDEN = 'hidden'
|
||||
|
||||
TYPE_ALEXA = 'alexa'
|
||||
TYPE_GOOGLE = 'google_home'
|
||||
|
@ -52,6 +55,11 @@ DEFAULT_EXPOSED_DOMAINS = [
|
|||
]
|
||||
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({
|
||||
DOMAIN: vol.Schema({
|
||||
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_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
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)
|
||||
|
||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
||||
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
|
||||
ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
|
||||
|
||||
|
||||
|
@ -183,6 +194,8 @@ class Config(object):
|
|||
self.advertise_port = conf.get(
|
||||
CONF_ADVERTISE_PORT) or self.listen_port
|
||||
|
||||
self.entities = conf.get(CONF_ENTITIES, {})
|
||||
|
||||
def entity_id_to_number(self, entity_id):
|
||||
"""Get a unique number for the entity id."""
|
||||
if self.type == TYPE_ALEXA:
|
||||
|
@ -215,6 +228,14 @@ class Config(object):
|
|||
assert isinstance(number, str)
|
||||
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):
|
||||
"""Determine if an entity should be exposed on the emulated bridge.
|
||||
|
||||
|
@ -227,6 +248,12 @@ class Config(object):
|
|||
domain = entity.domain.lower()
|
||||
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, 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:
|
||||
expose = 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__)
|
||||
|
||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
||||
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
|
||||
|
||||
HUE_API_STATE_ON = 'on'
|
||||
HUE_API_STATE_BRI = 'bri'
|
||||
|
||||
|
@ -77,7 +74,7 @@ class HueAllLightsStateView(HomeAssistantView):
|
|||
|
||||
number = self.config.entity_id_to_number(entity.entity_id)
|
||||
json_response[number] = entity_to_json(
|
||||
entity, state, brightness)
|
||||
self.config, entity, state, brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
|
@ -110,7 +107,7 @@ class HueOneLightStateView(HomeAssistantView):
|
|||
|
||||
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)
|
||||
|
||||
|
@ -344,10 +341,8 @@ def get_entity_state(config, entity):
|
|||
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."""
|
||||
name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
||||
|
||||
return {
|
||||
'state':
|
||||
{
|
||||
|
@ -356,7 +351,7 @@ def entity_to_json(entity, is_on=None, brightness=None):
|
|||
'reachable': True
|
||||
},
|
||||
'type': 'Dimmable light',
|
||||
'name': name,
|
||||
'name': config.get_entity_name(entity),
|
||||
'modelid': 'HASS123',
|
||||
'uniqueid': entity.entity_id,
|
||||
'swversion': '123'
|
||||
|
|
|
@ -205,7 +205,7 @@ def async_setup(hass, config: dict):
|
|||
|
||||
@asyncio.coroutine
|
||||
def async_handle_fan_service(service):
|
||||
"""Hande service call for fans."""
|
||||
"""Handle service call for fans."""
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
params = service.data.copy()
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class ComfoConnectFan(FanEntity):
|
||||
"""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."""
|
||||
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
|
||||
|
||||
|
@ -93,7 +93,7 @@ class ComfoConnectFan(FanEntity):
|
|||
speed = SPEED_LOW
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self) -> None:
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn off the fan (to away)."""
|
||||
self.set_speed(SPEED_OFF)
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ class InsteonLocalFanDevice(FanEntity):
|
|||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this Insteon node."""
|
||||
return 'insteon_local_{}_fan'.format(self.node.device_id)
|
||||
return self.node.device_id
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
|
|
|
@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
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_HUMIDITY = 'humidity'
|
||||
|
@ -200,7 +200,7 @@ class XiaomiAirPurifier(FanEntity):
|
|||
|
||||
@asyncio.coroutine
|
||||
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
|
||||
try:
|
||||
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:
|
||||
self._data = pickle.load(myfile) or {}
|
||||
self._cache_outdated = False
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
except: # noqa: E722 # pylint: disable=bare-except
|
||||
_LOGGER.error("Error loading data from pickled file %s",
|
||||
self._data_file)
|
||||
|
||||
|
@ -172,8 +171,7 @@ class StoredData(object):
|
|||
url, self._data_file)
|
||||
try:
|
||||
pickle.dump(self._data, myfile)
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
except: # noqa: E722 # pylint: disable=bare-except
|
||||
_LOGGER.error(
|
||||
"Error saving pickled data to %s", self._data_file)
|
||||
self._cache_outdated = True
|
||||
|
|
|
@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
|||
from homeassistant.core import callback
|
||||
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'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
|
@ -408,7 +408,7 @@ def async_setup_themes(hass, themes):
|
|||
|
||||
@callback
|
||||
def set_theme(call):
|
||||
"""Set backend-prefered theme."""
|
||||
"""Set backend-preferred theme."""
|
||||
data = call.data
|
||||
name = data[CONF_NAME]
|
||||
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
|
||||
|
||||
family_min_version = {
|
||||
'Chrome': 50, # Probably can reduce this
|
||||
'Firefox': 43, # Array.protopype.includes added in 43
|
||||
'Opera': 40, # Probably can reduce this
|
||||
'Edge': 14, # Array.protopype.includes added in 14
|
||||
'Safari': 10, # many features not supported by 9
|
||||
'Chrome': 54, # Object.values
|
||||
'Chrome Mobile': 54,
|
||||
'Firefox': 47, # Object.values
|
||||
'Firefox Mobile': 47,
|
||||
'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)
|
||||
return version and useragent.browser.version[0] >= version
|
||||
|
|
|
@ -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."""
|
||||
if now >= dt.as_local(dev_flow.user_code_expiry):
|
||||
hass.components.persistent_notification.create(
|
||||
'Authenication code expired, please restart '
|
||||
'Authentication code expired, please restart '
|
||||
'Home-Assistant and try again',
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
|
|
|
@ -37,6 +37,7 @@ from .const import (
|
|||
)
|
||||
|
||||
HANDLERS = Registry()
|
||||
QUERY_HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Mapping is [actions schema, primary trait, optional features]
|
||||
|
@ -79,7 +80,7 @@ class SmartHomeError(Exception):
|
|||
"""Log error code."""
|
||||
super(SmartHomeError, self).__init__(msg)
|
||||
_LOGGER.error(
|
||||
"An error has ocurred in Google SmartHome: %s."
|
||||
"An error has occurred in Google SmartHome: %s."
|
||||
"Error code: %s", msg, code
|
||||
)
|
||||
self.code = code
|
||||
|
@ -96,7 +97,7 @@ class Config:
|
|||
|
||||
|
||||
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, {})
|
||||
google_domain = entity_config.get(CONF_TYPE)
|
||||
class_data = MAPPING_COMPONENT.get(
|
||||
|
@ -177,120 +178,145 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
|
|||
return device
|
||||
|
||||
|
||||
def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||
"""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."""
|
||||
if deg is None:
|
||||
return None
|
||||
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
||||
def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]:
|
||||
"""Convert a float to Celsius and rounds to one decimal place."""
|
||||
if deg is None:
|
||||
return None
|
||||
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
||||
|
||||
if entity.domain == sensor.DOMAIN:
|
||||
entity_config = config.entity_config.get(entity.entity_id, {})
|
||||
google_domain = entity_config.get(CONF_TYPE)
|
||||
|
||||
if google_domain == climate.DOMAIN:
|
||||
# check if we have a string value to convert it to number
|
||||
value = entity.state
|
||||
if isinstance(entity.state, str):
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = None
|
||||
|
||||
if value is None:
|
||||
raise SmartHomeError(
|
||||
ERROR_NOT_SUPPORTED,
|
||||
"Invalid value {} for the climate sensor"
|
||||
.format(entity.state)
|
||||
)
|
||||
|
||||
# detect if we report temperature or humidity
|
||||
unit_of_measurement = entity.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
units.temperature_unit
|
||||
)
|
||||
if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
|
||||
value = celsius(value)
|
||||
attr = 'thermostatTemperatureAmbient'
|
||||
elif unit_of_measurement == '%':
|
||||
attr = 'thermostatHumidityAmbient'
|
||||
else:
|
||||
raise SmartHomeError(
|
||||
ERROR_NOT_SUPPORTED,
|
||||
"Unit {} is not supported by the climate sensor"
|
||||
.format(unit_of_measurement)
|
||||
)
|
||||
|
||||
return {attr: value}
|
||||
@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, {})
|
||||
google_domain = entity_config.get(CONF_TYPE)
|
||||
|
||||
if google_domain != climate.DOMAIN:
|
||||
raise SmartHomeError(
|
||||
ERROR_NOT_SUPPORTED,
|
||||
"Sensor type {} is not supported".format(google_domain)
|
||||
)
|
||||
|
||||
if entity.domain == climate.DOMAIN:
|
||||
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
|
||||
if mode not in CLIMATE_SUPPORTED_MODES:
|
||||
mode = 'heat'
|
||||
response = {
|
||||
'thermostatMode': mode,
|
||||
'thermostatTemperatureSetpoint':
|
||||
celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)),
|
||||
'thermostatTemperatureAmbient':
|
||||
celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)),
|
||||
'thermostatTemperatureSetpointHigh':
|
||||
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)),
|
||||
'thermostatTemperatureSetpointLow':
|
||||
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)),
|
||||
'thermostatHumidityAmbient':
|
||||
entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY),
|
||||
}
|
||||
return {k: v for k, v in response.items() if v is not None}
|
||||
# check if we have a string value to convert it to number
|
||||
value = entity.state
|
||||
if isinstance(entity.state, str):
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = None
|
||||
|
||||
final_state = entity.state != STATE_OFF
|
||||
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
|
||||
if final_state else 0)
|
||||
if value is None:
|
||||
raise SmartHomeError(
|
||||
ERROR_NOT_SUPPORTED,
|
||||
"Invalid value {} for the climate sensor"
|
||||
.format(entity.state)
|
||||
)
|
||||
|
||||
if entity.domain == media_player.DOMAIN:
|
||||
level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL, 1.0
|
||||
if final_state else 0.0)
|
||||
# Convert 0.0-1.0 to 0-255
|
||||
final_brightness = round(min(1.0, level) * 255)
|
||||
# detect if we report temperature or humidity
|
||||
unit_of_measurement = entity.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
units.temperature_unit
|
||||
)
|
||||
if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
|
||||
value = celsius(value, units)
|
||||
attr = 'thermostatTemperatureAmbient'
|
||||
elif unit_of_measurement == '%':
|
||||
attr = 'thermostatHumidityAmbient'
|
||||
else:
|
||||
raise SmartHomeError(
|
||||
ERROR_NOT_SUPPORTED,
|
||||
"Unit {} is not supported by the climate sensor"
|
||||
.format(unit_of_measurement)
|
||||
)
|
||||
|
||||
if final_brightness is None:
|
||||
final_brightness = 255 if final_state else 0
|
||||
return {attr: value}
|
||||
|
||||
final_brightness = 100 * (final_brightness / 255)
|
||||
|
||||
query_response = {
|
||||
"on": final_state,
|
||||
"online": True,
|
||||
"brightness": int(final_brightness)
|
||||
@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()
|
||||
if mode not in CLIMATE_SUPPORTED_MODES:
|
||||
mode = 'heat'
|
||||
attrs = entity.attributes
|
||||
response = {
|
||||
'thermostatMode': mode,
|
||||
'thermostatTemperatureSetpoint':
|
||||
celsius(attrs.get(climate.ATTR_TEMPERATURE), units),
|
||||
'thermostatTemperatureAmbient':
|
||||
celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units),
|
||||
'thermostatTemperatureSetpointHigh':
|
||||
celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units),
|
||||
'thermostatTemperatureSetpointLow':
|
||||
celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units),
|
||||
'thermostatHumidityAmbient':
|
||||
attrs.get(climate.ATTR_CURRENT_HUMIDITY),
|
||||
}
|
||||
return {k: v for k, v in response.items() if v is not None}
|
||||
|
||||
|
||||
@QUERY_HANDLERS.register(media_player.DOMAIN)
|
||||
def query_response_media_player(
|
||||
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
|
||||
brightness = int(level * 100)
|
||||
|
||||
return {'brightness': brightness}
|
||||
|
||||
|
||||
@QUERY_HANDLERS.register(light.DOMAIN)
|
||||
def query_response_light(
|
||||
entity: Entity, config: Config, units: UnitSystem) -> dict:
|
||||
"""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)
|
||||
if supported_features & \
|
||||
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
|
||||
query_response["color"] = {}
|
||||
response['color'] = {}
|
||||
|
||||
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(
|
||||
entity.attributes.get(light.ATTR_COLOR_TEMP))))
|
||||
|
||||
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)
|
||||
|
||||
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
|
||||
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
|
||||
if color_rgb is not None:
|
||||
query_response["color"]["spectrumRGB"] = \
|
||||
response['color']['spectrumRGB'] = \
|
||||
int(color.color_rgb_to_hex(
|
||||
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
|
||||
|
@ -429,7 +455,7 @@ def async_devices_query(hass, config, payload):
|
|||
devices = {}
|
||||
for device in payload.get('devices', []):
|
||||
devid = device.get('id')
|
||||
# In theory this should never happpen
|
||||
# In theory this should never happen
|
||||
if not devid:
|
||||
_LOGGER.error('Device missing ID: %s', device)
|
||||
continue
|
||||
|
@ -438,11 +464,11 @@ def async_devices_query(hass, config, payload):
|
|||
if not state:
|
||||
# If we can't find a state, the device is offline
|
||||
devices[devid] = {'online': False}
|
||||
|
||||
try:
|
||||
devices[devid] = query_device(state, config, hass.config.units)
|
||||
except SmartHomeError as error:
|
||||
devices[devid] = {'errorCode': error.code}
|
||||
else:
|
||||
try:
|
||||
devices[devid] = query_device(state, config, hass.config.units)
|
||||
except SmartHomeError as error:
|
||||
devices[devid] = {'errorCode': error.code}
|
||||
|
||||
return {'devices': devices}
|
||||
|
||||
|
|
|
@ -247,7 +247,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
|||
|
||||
@asyncio.coroutine
|
||||
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)
|
||||
|
||||
if component is None:
|
||||
|
@ -371,7 +371,6 @@ def async_setup(hass, config):
|
|||
@asyncio.coroutine
|
||||
def _async_process_config(hass, config, component):
|
||||
"""Process group configuration."""
|
||||
groups = []
|
||||
for object_id, conf in config.get(DOMAIN, {}).items():
|
||||
name = conf.get(CONF_NAME, object_id)
|
||||
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
|
||||
# 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,
|
||||
control=control, object_id=object_id)
|
||||
groups.append(group)
|
||||
|
||||
if groups:
|
||||
yield from component.async_add_entities(groups)
|
||||
|
||||
|
||||
class Group(Entity):
|
||||
|
|
|
@ -320,7 +320,7 @@ def setup(hass: HomeAssistant, base_config):
|
|||
class CecDevice(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."""
|
||||
self._device = device
|
||||
self.hass = hass
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.38']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.39']
|
||||
DOMAIN = 'homematic'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -218,7 +218,7 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
|
|||
|
||||
@bind_hass
|
||||
def virtualkey(hass, address, channel, param, interface=None):
|
||||
"""Send virtual keypress to homematic controlller."""
|
||||
"""Send virtual keypress to homematic controller."""
|
||||
data = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_CHANNEL: channel,
|
||||
|
@ -256,7 +256,7 @@ def set_device_value(hass, address, channel, param, value, interface=None):
|
|||
|
||||
@bind_hass
|
||||
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 = {
|
||||
key: value for key, value in (
|
||||
(ATTR_INTERFACE, interface),
|
||||
|
@ -466,7 +466,7 @@ def _system_callback_handler(hass, config, src, *args):
|
|||
hass, discovery_type, addresses, interface)
|
||||
|
||||
# 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:
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices
|
||||
|
@ -665,7 +665,7 @@ class HMHub(Entity):
|
|||
self.schedule_update_ha_state()
|
||||
|
||||
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)
|
||||
if variables is None:
|
||||
return
|
||||
|
|
|
@ -13,7 +13,7 @@ virtualkey:
|
|||
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
|
||||
example: PRESS_LONG
|
||||
interface:
|
||||
description: (Optional) for set a interface value.
|
||||
description: (Optional) for set an interface value.
|
||||
example: Interfaces name from config
|
||||
|
||||
set_variable_value:
|
||||
|
@ -42,7 +42,7 @@ set_device_value:
|
|||
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
|
||||
example: PRESS_LONG
|
||||
interface:
|
||||
description: (Optional) for set a interface value
|
||||
description: (Optional) for set an interface value
|
||||
example: Interfaces name from config
|
||||
value:
|
||||
description: New value
|
||||
|
|
|
@ -23,7 +23,7 @@ def auth_middleware(request, handler):
|
|||
# If no password set, just always set authenticated=True
|
||||
if request.app['hass'].http.api_password is None:
|
||||
request[KEY_AUTHENTICATED] = True
|
||||
return handler(request)
|
||||
return (yield from handler(request))
|
||||
|
||||
# Check authentication
|
||||
authenticated = False
|
||||
|
@ -46,7 +46,7 @@ def auth_middleware(request, handler):
|
|||
authenticated = True
|
||||
|
||||
request[KEY_AUTHENTICATED] = authenticated
|
||||
return handler(request)
|
||||
return (yield from handler(request))
|
||||
|
||||
|
||||
def is_trusted_ip(request):
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue