Merge pull request #11589 from home-assistant/release-0-61

0.61
This commit is contained in:
Fabian Affolter 2018-01-15 10:35:00 +01:00 committed by GitHub
commit c3ff5de016
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
364 changed files with 10937 additions and 3937 deletions

View file

@ -50,9 +50,15 @@ omit =
homeassistant/components/bloomsky.py homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py homeassistant/components/*/bloomsky.py
homeassistant/components/coinbase.py
homeassistant/components/sensor/coinbase.py
homeassistant/components/comfoconnect.py homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py homeassistant/components/*/comfoconnect.py
homeassistant/components/deconz/*
homeassistant/components/*/deconz.py
homeassistant/components/digital_ocean.py homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py homeassistant/components/*/digital_ocean.py
@ -263,6 +269,9 @@ omit =
homeassistant/components/zoneminder.py homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py homeassistant/components/*/zoneminder.py
homeassistant/components/daikin.py
homeassistant/components/*/daikin.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
@ -296,6 +305,7 @@ omit =
homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py homeassistant/components/camera/synology.py
homeassistant/components/camera/yi.py homeassistant/components/camera/yi.py
homeassistant/components/climate/econet.py
homeassistant/components/climate/ephember.py homeassistant/components/climate/ephember.py
homeassistant/components/climate/eq3btsmart.py homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/flexit.py homeassistant/components/climate/flexit.py
@ -307,6 +317,7 @@ omit =
homeassistant/components/climate/proliphix.py homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py homeassistant/components/climate/radiotherm.py
homeassistant/components/climate/sensibo.py homeassistant/components/climate/sensibo.py
homeassistant/components/climate/touchline.py
homeassistant/components/cover/garadget.py homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py homeassistant/components/cover/knx.py
@ -365,8 +376,10 @@ omit =
homeassistant/components/light/decora.py homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/iglo.py
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
@ -476,6 +489,7 @@ omit =
homeassistant/components/notify/yessssms.py homeassistant/components/notify/yessssms.py
homeassistant/components/nuimo_controller.py homeassistant/components/nuimo_controller.py
homeassistant/components/prometheus.py homeassistant/components/prometheus.py
homeassistant/components/rainbird.py
homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py homeassistant/components/remote/itach.py
@ -504,6 +518,7 @@ omit =
homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
@ -517,7 +532,6 @@ omit =
homeassistant/components/sensor/etherscan.py homeassistant/components/sensor/etherscan.py
homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fedex.py homeassistant/components/sensor/fedex.py
homeassistant/components/sensor/fido.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
@ -532,7 +546,6 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
@ -570,6 +583,7 @@ omit =
homeassistant/components/sensor/pyload.py homeassistant/components/sensor/pyload.py
homeassistant/components/sensor/qnap.py homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
@ -580,6 +594,7 @@ omit =
homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/steam_online.py
@ -648,9 +663,9 @@ omit =
homeassistant/components/vacuum/xiaomi_miio.py homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
homeassistant/components/weather/metoffice.py homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/yweather.py
homeassistant/components/weather/zamg.py homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py homeassistant/components/zeroconf.py
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py

View file

@ -11,6 +11,7 @@
``` ```
## Checklist: ## Checklist:
- [ ] The code change is tested and works locally.
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)

1
.gitignore vendored
View file

@ -74,6 +74,7 @@ pip-selfcheck.json
venv venv
.venv .venv
Pipfile* Pipfile*
share/*
# vimmy stuff # vimmy stuff
*.swp *.swp

View file

@ -53,10 +53,11 @@ homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
@ -64,9 +65,11 @@ homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342 homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342

View file

@ -10,7 +10,6 @@ Component design guidelines:
import asyncio import asyncio
import itertools as it import itertools as it
import logging import logging
import os
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.config as conf_util import homeassistant.config as conf_util
@ -111,11 +110,6 @@ def async_reload_core_config(hass):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up general services related to Home Assistant.""" """Set up general services related to Home Assistant."""
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@asyncio.coroutine @asyncio.coroutine
def async_handle_turn_service(service): def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off.""" """Handle calls to homeassistant.turn_on/off."""
@ -155,14 +149,11 @@ def async_setup(hass, config):
yield from asyncio.wait(tasks, loop=hass.loop) yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
descriptions[ha.DOMAIN][SERVICE_TURN_OFF])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
descriptions[ha.DOMAIN][SERVICE_TURN_ON])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
@asyncio.coroutine @asyncio.coroutine
def async_handle_core_service(call): def async_handle_core_service(call):
@ -187,14 +178,11 @@ def async_setup(hass, config):
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service, ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service, ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service, ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG])
@asyncio.coroutine @asyncio.coroutine
def async_handle_reload_config(call): def async_handle_reload_config(call):
@ -209,7 +197,6 @@ def async_setup(hass, config):
hass, conf.get(ha.DOMAIN) or {}) hass, conf.get(ha.DOMAIN) or {})
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config, ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG])
return True return True

View file

@ -7,11 +7,9 @@ https://home-assistant.io/components/abode/
import asyncio import asyncio
import logging import logging
from functools import partial from functools import partial
from os import path
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME, ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS, CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
@ -188,22 +186,16 @@ def setup_hass_services(hass):
for device in target_devices: for device in target_devices:
device.trigger() device.trigger()
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))[DOMAIN]
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SETTINGS, change_setting, DOMAIN, SERVICE_SETTINGS, change_setting,
descriptions.get(SERVICE_SETTINGS),
schema=CHANGE_SETTING_SCHEMA) schema=CHANGE_SETTING_SCHEMA)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
descriptions.get(SERVICE_CAPTURE_IMAGE),
schema=CAPTURE_IMAGE_SCHEMA) schema=CAPTURE_IMAGE_SCHEMA)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_TRIGGER, trigger_quick_action, DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
descriptions.get(SERVICE_TRIGGER),
schema=TRIGGER_SCHEMA) schema=TRIGGER_SCHEMA)

View file

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation.
https://home-assistant.io/components/ads/ https://home-assistant.io/components/ads/
""" """
import os
import threading import threading
import struct import struct
import logging import logging
@ -14,7 +13,6 @@ from collections import namedtuple
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
EVENT_HOMEASSISTANT_STOP EVENT_HOMEASSISTANT_STOP
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyads==2.2.6'] REQUIREMENTS = ['pyads==2.2.6']
@ -107,13 +105,8 @@ def setup(hass, config):
except pyads.ADSError as err: except pyads.ADSError as err:
_LOGGER.error(err) _LOGGER.error(err)
# load descriptions from services.yaml
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register( hass.services.register(
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name, DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
descriptions[SERVICE_WRITE_DATA_BY_NAME],
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME
) )

View file

@ -7,7 +7,6 @@ https://home-assistant.io/components/alarm_control_panel/
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
@ -15,7 +14,6 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -148,14 +146,10 @@ def async_setup(hass, config):
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
hass.services.async_register( hass.services.async_register(
DOMAIN, service, async_alarm_service_handler, DOMAIN, service, async_alarm_service_handler,
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA) schema=ALARM_SERVICE_SCHEMA)
return True return True

View file

@ -7,23 +7,39 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
import asyncio import asyncio
import logging import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv
from homeassistant.components.alarmdecoder import ( from homeassistant.components.alarmdecoder import (
DATA_AD, SIGNAL_PANEL_MESSAGE) DATA_AD, SIGNAL_PANEL_MESSAGE)
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_TRIGGERED) STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder'] DEPENDENCIES = ['alarmdecoder']
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
vol.Required(ATTR_CODE): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up for AlarmDecoder alarm panels.""" """Set up for AlarmDecoder alarm panels."""
add_devices([AlarmDecoderAlarmPanel()]) device = AlarmDecoderAlarmPanel()
add_devices([device])
return True def alarm_toggle_chime_handler(service):
"""Register toggle chime handler."""
code = service.data.get(ATTR_CODE)
device.alarm_toggle_chime(code)
hass.services.register(
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA)
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@ -34,6 +50,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
self._display = "" self._display = ""
self._name = "Alarm Panel" self._name = "Alarm Panel"
self._state = None self._state = None
self._ac_power = None
self._backlight_on = None
self._battery_low = None
self._check_zone = None
self._chime = None
self._entry_delay_off = None
self._programming_mode = None
self._ready = None
self._zone_bypassed = None
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
@ -43,20 +68,24 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
def _message_callback(self, message): def _message_callback(self, message):
if message.alarm_sounding or message.fire_alarm: if message.alarm_sounding or message.fire_alarm:
if self._state != STATE_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED self._state = STATE_ALARM_TRIGGERED
self.schedule_update_ha_state()
elif message.armed_away: elif message.armed_away:
if self._state != STATE_ALARM_ARMED_AWAY:
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
self.schedule_update_ha_state()
elif message.armed_home: elif message.armed_home:
if self._state != STATE_ALARM_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
self.schedule_update_ha_state()
else: else:
if self._state != STATE_ALARM_DISARMED:
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._ac_power = message.ac_power
self._backlight_on = message.backlight_on
self._battery_low = message.battery_low
self._check_zone = message.check_zone
self._chime = message.chime_on
self._entry_delay_off = message.entry_delay_off
self._programming_mode = message.programming_mode
self._ready = message.ready
self._zone_bypassed = message.zone_bypassed
self.schedule_update_ha_state() self.schedule_update_ha_state()
@property @property
@ -79,20 +108,37 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'ac_power': self._ac_power,
'backlight_on': self._backlight_on,
'battery_low': self._battery_low,
'check_zone': self._check_zone,
'chime': self._chime,
'entry_delay_off': self._entry_delay_off,
'programming_mode': self._programming_mode,
'ready': self._ready,
'zone_bypassed': self._zone_bypassed
}
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if code: if code:
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
self.hass.data[DATA_AD].send("{!s}1".format(code)) self.hass.data[DATA_AD].send("{!s}1".format(code))
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
if code: if code:
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
self.hass.data[DATA_AD].send("{!s}2".format(code)) self.hass.data[DATA_AD].send("{!s}2".format(code))
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
if code: if code:
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
self.hass.data[DATA_AD].send("{!s}3".format(code)) self.hass.data[DATA_AD].send("{!s}3".format(code))
def alarm_toggle_chime(self, code=None):
"""Send toggle chime command."""
if code:
self.hass.data[DATA_AD].send("{!s}9".format(code))

View file

@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.14'] REQUIREMENTS = ['concord232==0.15']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -121,4 +121,4 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
self._alarm.arm('auto') self._alarm.arm('away')

View file

@ -16,9 +16,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pythonegardia==1.0.22'] REQUIREMENTS = ['pythonegardia==1.0.26']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,13 +26,15 @@ CONF_REPORT_SERVER_CODES = 'report_server_codes'
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled' CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
CONF_REPORT_SERVER_PORT = 'report_server_port' CONF_REPORT_SERVER_PORT = 'report_server_port'
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore' CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
CONF_VERSION = 'version'
DEFAULT_NAME = 'Egardia' DEFAULT_NAME = 'Egardia'
DEFAULT_PORT = 80 DEFAULT_PORT = 80
DEFAULT_REPORT_SERVER_ENABLED = False DEFAULT_REPORT_SERVER_ENABLED = False
DEFAULT_REPORT_SERVER_PORT = 52010 DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = 'GATE-01'
DOMAIN = 'egardia' DOMAIN = 'egardia'
D_EGARDIASRV = 'egardiaserver'
NOTIFICATION_ID = 'egardia_notification' NOTIFICATION_ID = 'egardia_notification'
NOTIFICATION_TITLE = 'Egardia' NOTIFICATION_TITLE = 'Egardia'
@ -49,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list), vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
@ -62,6 +65,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Egardia platform.""" """Set up the Egardia platform."""
from pythonegardia import egardiadevice from pythonegardia import egardiadevice
from pythonegardia import egardiaserver
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
@ -71,41 +75,62 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED) rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
rs_port = config.get(CONF_REPORT_SERVER_PORT) rs_port = config.get(CONF_REPORT_SERVER_PORT)
rs_codes = config.get(CONF_REPORT_SERVER_CODES) rs_codes = config.get(CONF_REPORT_SERVER_CODES)
version = config.get(CONF_VERSION)
try: try:
egardiasystem = egardiadevice.EgardiaDevice( egardiasystem = egardiadevice.EgardiaDevice(
host, port, username, password, '') host, port, username, password, '', version)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
raise exc.PlatformNotReady() raise exc.PlatformNotReady()
except egardiadevice.UnauthorizedError: except egardiadevice.UnauthorizedError:
_LOGGER.error("Unable to authorize. Wrong password or username") _LOGGER.error("Unable to authorize. Wrong password or username")
return False return
add_devices([EgardiaAlarm( eg_dev = EgardiaAlarm(
name, egardiasystem, hass, rs_enabled, rs_port, rs_codes)], True) name, egardiasystem, rs_enabled, rs_codes)
if rs_enabled:
# Set up the egardia server
_LOGGER.info("Setting up EgardiaServer")
try:
if D_EGARDIASRV not in hass.data:
server = egardiaserver.EgardiaServer('', rs_port)
bound = server.bind()
if not bound:
raise IOError("Binding error occurred while " +
"starting EgardiaServer")
hass.data[D_EGARDIASRV] = server
server.start()
except IOError:
return
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
def handle_stop_event(event):
"""Callback function for HA stop event."""
hass.data[D_EGARDIASRV].stop()
# listen to home assistant stop event
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
# add egardia alarm device
add_devices([eg_dev], True)
class EgardiaAlarm(alarm.AlarmControlPanel): class EgardiaAlarm(alarm.AlarmControlPanel):
"""Representation of a Egardia alarm.""" """Representation of a Egardia alarm."""
def __init__(self, name, egardiasystem, hass, rs_enabled=False, def __init__(self, name, egardiasystem,
rs_port=None, rs_codes=None): rs_enabled=False, rs_codes=None):
"""Initialize object.""" """Initialize object."""
self._name = name self._name = name
self._egardiasystem = egardiasystem self._egardiasystem = egardiasystem
self._status = STATE_UNKNOWN self._status = None
self._rs_enabled = rs_enabled self._rs_enabled = rs_enabled
self._rs_port = rs_port
self._hass = hass
if rs_codes is not None: if rs_codes is not None:
self._rs_codes = rs_codes[0] self._rs_codes = rs_codes[0]
else: else:
self._rs_codes = rs_codes self._rs_codes = rs_codes
if self._rs_enabled:
self.listen_to_system_status()
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -123,19 +148,14 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
return True return True
return False return False
def handle_system_status_event(self, event): def handle_status_event(self, event):
"""Handle egardia_system_status_event.""" """Handle egardia_system_status_event."""
if event.data.get('status') is not None: statuscode = event.get('status')
statuscode = event.data.get('status') if statuscode is not None:
status = self.lookupstatusfromcode(statuscode) status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status) self.parsestatus(status)
self.schedule_update_ha_state() self.schedule_update_ha_state()
def listen_to_system_status(self):
"""Subscribe to egardia_system_status event."""
self._hass.bus.listen(
'egardia_system_status', self.handle_system_status_event)
def lookupstatusfromcode(self, statuscode): def lookupstatusfromcode(self, statuscode):
"""Look at the rs_codes and returns the status from the code.""" """Look at the rs_codes and returns the status from the code."""
status = 'UNKNOWN' status = 'UNKNOWN'

View file

@ -6,7 +6,6 @@ https://home-assistant.io/components/alarm_control_panel.envisalink/
""" """
import asyncio import asyncio
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
@ -14,7 +13,6 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.components.envisalink import ( from homeassistant.components.envisalink import (
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE) CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
@ -69,14 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for device in target_devices: for device in target_devices:
device.async_alarm_keypress(keypress) device.async_alarm_keypress(keypress)
# Register Envisalink specific services
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register( hass.services.async_register(
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA) schema=ALARM_KEYPRESS_SCHEMA)
return True return True

View file

@ -17,7 +17,9 @@ from homeassistant.const import (
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
CONF_NAME, CONF_CODE) CONF_NAME, CONF_CODE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -54,15 +56,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_DISARM), config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY), config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_CODE))]) config.get(CONF_CODE),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
class MqttAlarm(alarm.AlarmControlPanel): class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, payload_disarm, def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
payload_arm_home, payload_arm_away, code): payload_arm_home, payload_arm_away, code, availability_topic,
payload_available, payload_not_available):
"""Init the MQTT Alarm Control Panel.""" """Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
@ -73,11 +81,11 @@ class MqttAlarm(alarm.AlarmControlPanel):
self._payload_arm_away = payload_arm_away self._payload_arm_away = payload_arm_away
self._code = code self._code = code
@asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe mqtt events. """Subscribe mqtt events."""
yield from super().async_added_to_hass()
This method must be run in the event loop and returns a coroutine.
"""
@callback @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""Run when new MQTT message has been received.""" """Run when new MQTT message has been received."""
@ -89,7 +97,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
self._state = payload self._state = payload
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
return mqtt.async_subscribe( yield from mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos) self.hass, self._state_topic, message_received, self._qos)
@property @property

View file

@ -59,3 +59,13 @@ envisalink_alarm_keypress:
keypress: keypress:
description: 'String to send to the alarm panel (1-6 characters).' description: 'String to send to the alarm panel (1-6 characters).'
example: '*71' example: '*71'
alarmdecoder_alarm_toggle_chime:
description: Send the alarm the toggle chime command.
fields:
entity_id:
description: Name of the alarm control panel to trigger.
example: 'alarm_control_panel.downstairs'
code:
description: A required code to toggle the alarm control panel chime with.
example: 1234

View file

@ -6,13 +6,16 @@ https://home-assistant.io/components/alarmdecoder/
""" """
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.util import dt as dt_util
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
REQUIREMENTS = ['alarmdecoder==0.12.3'] REQUIREMENTS = ['alarmdecoder==1.13.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,6 +32,7 @@ CONF_DEVICE_TYPE = 'type'
CONF_PANEL_DISPLAY = 'panel_display' CONF_PANEL_DISPLAY = 'panel_display'
CONF_ZONE_NAME = 'name' CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type' CONF_ZONE_TYPE = 'type'
CONF_ZONE_RFID = 'rfid'
CONF_ZONES = 'zones' CONF_ZONES = 'zones'
DEFAULT_DEVICE_TYPE = 'socket' DEFAULT_DEVICE_TYPE = 'socket'
@ -48,6 +52,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
DEVICE_SOCKET_SCHEMA = vol.Schema({ DEVICE_SOCKET_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'socket', vol.Required(CONF_DEVICE_TYPE): 'socket',
@ -64,7 +69,9 @@ DEVICE_USB_SCHEMA = vol.Schema({
ZONE_SCHEMA = vol.Schema({ ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string, vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string}) vol.Optional(CONF_ZONE_TYPE,
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
vol.Optional(CONF_ZONE_RFID): cv.string})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -85,6 +92,7 @@ def setup(hass, config):
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
restart = False
device = conf.get(CONF_DEVICE) device = conf.get(CONF_DEVICE)
display = conf.get(CONF_PANEL_DISPLAY) display = conf.get(CONF_PANEL_DISPLAY)
zones = conf.get(CONF_ZONES) zones = conf.get(CONF_ZONES)
@ -98,13 +106,43 @@ def setup(hass, config):
def stop_alarmdecoder(event): def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder.""" """Handle the shutdown of AlarmDecoder."""
_LOGGER.debug("Shutting down alarmdecoder") _LOGGER.debug("Shutting down alarmdecoder")
nonlocal restart
restart = False
controller.close() controller.close()
def open_connection(now=None):
"""Open a connection to AlarmDecoder."""
from alarmdecoder.util import NoDeviceError
nonlocal restart
try:
controller.open(baud)
except NoDeviceError:
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
hass.helpers.event.track_point_in_time(
open_connection, dt_util.utcnow() + timedelta(seconds=5))
return
_LOGGER.debug("Established a connection with the alarmdecoder")
restart = True
def handle_closed_connection(event):
"""Restart after unexpected loss of connection."""
nonlocal restart
if not restart:
return
restart = False
_LOGGER.warning("AlarmDecoder unexpectedly lost connection.")
hass.add_job(open_connection)
def handle_message(sender, message): def handle_message(sender, message):
"""Handle message from AlarmDecoder.""" """Handle message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send( hass.helpers.dispatcher.dispatcher_send(
SIGNAL_PANEL_MESSAGE, message) SIGNAL_PANEL_MESSAGE, message)
def handle_rfx_message(sender, message):
"""Handle RFX message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_RFX_MESSAGE, message)
def zone_fault_callback(sender, zone): def zone_fault_callback(sender, zone):
"""Handle zone fault from AlarmDecoder.""" """Handle zone fault from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send( hass.helpers.dispatcher.dispatcher_send(
@ -129,14 +167,15 @@ def setup(hass, config):
return False return False
controller.on_message += handle_message controller.on_message += handle_message
controller.on_rfx_message += handle_rfx_message
controller.on_zone_fault += zone_fault_callback controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback controller.on_zone_restore += zone_restore_callback
controller.on_close += handle_closed_connection
hass.data[DATA_AD] = controller hass.data[DATA_AD] = controller
controller.open(baud) open_connection()
_LOGGER.debug("Established a connection with the alarmdecoder")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config) load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)

View file

@ -7,12 +7,10 @@ https://home-assistant.io/components/alert/
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
@ -129,22 +127,16 @@ def async_setup(hass, config):
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK]) alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
all_alerts[entity.entity_id] = entity all_alerts[entity.entity_id] = entity
# Read descriptions
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
descriptions = descriptions.get(DOMAIN, {})
# Setup service calls # Setup service calls
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA) schema=ALERT_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA) schema=ALERT_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA) schema=ALERT_SERVICE_SCHEMA)
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()] tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
if tasks: if tasks:

View file

@ -9,15 +9,16 @@ import asyncio
import enum import enum
import logging import logging
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.components import http from homeassistant.components import http
from homeassistant.util.decorator import Registry
from .const import DOMAIN, SYN_RESOLUTION_MATCH from .const import DOMAIN, SYN_RESOLUTION_MATCH
INTENTS_API_ENDPOINT = '/api/alexa' INTENTS_API_ENDPOINT = '/api/alexa'
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,6 +48,10 @@ def async_setup(hass):
hass.http.register_view(AlexaIntentsView) hass.http.register_view(AlexaIntentsView)
class UnknownRequest(HomeAssistantError):
"""When an unknown Alexa request is passed in."""
class AlexaIntentsView(http.HomeAssistantView): class AlexaIntentsView(http.HomeAssistantView):
"""Handle Alexa requests.""" """Handle Alexa requests."""
@ -57,57 +62,98 @@ class AlexaIntentsView(http.HomeAssistantView):
def post(self, request): def post(self, request):
"""Handle Alexa.""" """Handle Alexa."""
hass = request.app['hass'] hass = request.app['hass']
data = yield from request.json() message = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data) _LOGGER.debug('Received Alexa request: %s', message)
req = data.get('request') try:
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)
except UnknownRequest as err:
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message, str(err)))
if req is None: except intent.UnknownIntent as err:
_LOGGER.error('Received invalid data from Alexa: %s', data) _LOGGER.warning(str(err))
return self.json_message('Expected request value not received', return self.json(intent_error_response(
HTTP_BAD_REQUEST) hass, message,
"This intent is not yet configured within Home Assistant."))
except intent.InvalidSlotInfo as err:
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
return self.json(intent_error_response(
hass, message,
"Invalid slot information received for this intent."))
except intent.IntentError as err:
_LOGGER.exception(str(err))
return self.json(intent_error_response(
hass, message, "Error handling intent."))
def intent_error_response(hass, message, error):
"""Return an Alexa response that will speak the error message."""
alexa_intent_info = message.get('request').get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict()
@asyncio.coroutine
def async_handle_message(hass, message):
"""Handle an Alexa intent.
Raises:
- UnknownRequest
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
req = message.get('request')
req_type = req['type'] req_type = req['type']
if req_type == 'SessionEndedRequest': handler = HANDLERS.get(req_type)
if not handler:
raise UnknownRequest('Received unknown request {}'.format(req_type))
return (yield from handler(hass, message))
@HANDLERS.register('SessionEndedRequest')
@asyncio.coroutine
def async_handle_session_end(hass, message):
"""Handle a session end request."""
return None return None
@HANDLERS.register('IntentRequest')
@HANDLERS.register('LaunchRequest')
@asyncio.coroutine
def async_handle_intent(hass, message):
"""Handle an intent request.
Raises:
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
req = message.get('request')
alexa_intent_info = req.get('intent') alexa_intent_info = req.get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response = AlexaResponse(hass, alexa_intent_info)
if req_type != 'IntentRequest' and req_type != 'LaunchRequest': if req['type'] == 'LaunchRequest':
_LOGGER.warning('Received unsupported request: %s', req_type) intent_name = message.get('session', {}) \
return self.json_message(
'Received unsupported request: {}'.format(req_type),
HTTP_BAD_REQUEST)
if req_type == 'LaunchRequest':
intent_name = data.get('session', {}) \
.get('application', {}) \ .get('application', {}) \
.get('applicationId') .get('applicationId')
else: else:
intent_name = alexa_intent_info['name'] intent_name = alexa_intent_info['name']
try:
intent_response = yield from intent.async_handle( intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name, hass, DOMAIN, intent_name,
{key: {'value': value} for key, value {key: {'value': value} for key, value
in alexa_response.variables.items()}) in alexa_response.variables.items()})
except intent.UnknownIntent as err:
_LOGGER.warning('Received unknown intent %s', intent_name)
alexa_response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
return self.json(alexa_response)
except intent.InvalidSlotInfo as err:
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception('Error handling request for %s', intent_name)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items(): for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech: if intent_speech in intent_response.speech:
@ -121,7 +167,7 @@ class AlexaIntentsView(http.HomeAssistantView):
CardType.simple, intent_response.card['simple']['title'], CardType.simple, intent_response.card['simple']['title'],
intent_response.card['simple']['content']) intent_response.card['simple']['content'])
return self.json(alexa_response) return alexa_response.as_dict()
def resolve_slot_synonyms(key, request): def resolve_slot_synonyms(key, request):

View file

@ -1,6 +1,5 @@
"""Support for alexa Smart Home Skill API.""" """Support for alexa Smart Home Skill API."""
import asyncio import asyncio
from collections import namedtuple
import logging import logging
import math import math
from uuid import uuid4 from uuid import uuid4
@ -27,10 +26,9 @@ API_EVENT = 'event'
API_HEADER = 'header' API_HEADER = 'header'
API_PAYLOAD = 'payload' API_PAYLOAD = 'payload'
ATTR_ALEXA_DESCRIPTION = 'alexa_description' CONF_DESCRIPTION = 'description'
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories' CONF_DISPLAY_CATEGORIES = 'display_categories'
ATTR_ALEXA_HIDDEN = 'alexa_hidden' CONF_NAME = 'name'
ATTR_ALEXA_NAME = 'alexa_name'
MAPPING_COMPONENT = { MAPPING_COMPONENT = {
@ -73,7 +71,13 @@ MAPPING_COMPONENT = {
} }
Config = namedtuple('AlexaConfig', 'filter') class Config:
"""Hold the configuration for Alexa."""
def __init__(self, should_expose, entity_config=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.entity_config = entity_config or {}
@asyncio.coroutine @asyncio.coroutine
@ -150,32 +154,28 @@ def async_api_discovery(hass, config, request):
discovery_endpoints = [] discovery_endpoints = []
for entity in hass.states.async_all(): for entity in hass.states.async_all():
if not config.filter(entity.entity_id): if not config.should_expose(entity.entity_id):
_LOGGER.debug("Not exposing %s because filtered by config", _LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id) entity.entity_id)
continue continue
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
entity.entity_id)
continue
class_data = MAPPING_COMPONENT.get(entity.domain) class_data = MAPPING_COMPONENT.get(entity.domain)
if not class_data: if not class_data:
continue continue
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name) entity_conf = config.entity_config.get(entity.entity_id, {})
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
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 # Required description as per Amazon Scene docs
if entity.domain == scene.DOMAIN: if entity.domain == scene.DOMAIN:
scene_fmt = '{} (Scene connected via Home Assistant)' scene_fmt = '{} (Scene connected via Home Assistant)'
description = scene_fmt.format(description) description = scene_fmt.format(description)
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES,
display_categories = entity.attributes.get(cat_key, class_data[0]) class_data[0])
endpoint = { endpoint = {
'displayCategories': [display_categories], 'displayCategories': [display_categories],
@ -243,7 +243,11 @@ def async_api_turn_on(hass, config, request, entity):
if entity.domain == group.DOMAIN: if entity.domain == group.DOMAIN:
domain = ha.DOMAIN domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_ON, { service = SERVICE_TURN_ON
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER
yield from hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=False) }, blocking=False)
@ -259,7 +263,11 @@ def async_api_turn_off(hass, config, request, entity):
if entity.domain == group.DOMAIN: if entity.domain == group.DOMAIN:
domain = ha.DOMAIN domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, { service = SERVICE_TURN_OFF
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER
yield from hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=False) }, blocking=False)

View file

@ -24,6 +24,7 @@ from homeassistant.const import (
__version__) __version__)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import AsyncTrackStates from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -293,10 +294,11 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = "api:services"
@ha.callback @asyncio.coroutine
def get(self, request): def get(self, request):
"""Get registered services.""" """Get registered services."""
return self.json(async_services_json(request.app['hass'])) services = yield from async_services_json(request.app['hass'])
return self.json(services)
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
@ -355,10 +357,12 @@ class APITemplateView(HomeAssistantView):
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
@asyncio.coroutine
def async_services_json(hass): def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""
descriptions = yield from async_get_all_descriptions(hass)
return [{"domain": key, "services": value} return [{"domain": key, "services": value}
for key, value in hass.services.async_services().items()] for key, value in descriptions.items()]
def async_events_json(hass): def async_events_json(hass):

View file

@ -4,7 +4,6 @@ Support for Apple TV.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/apple_tv/ https://home-assistant.io/components/apple_tv/
""" """
import os
import asyncio import asyncio
import logging import logging
@ -12,7 +11,6 @@ import voluptuous as vol
from typing import Union, TypeVar, Sequence from typing import Union, TypeVar, Sequence
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID) from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.components.discovery import SERVICE_APPLE_TV
@ -183,18 +181,12 @@ def async_setup(hass, config):
if tasks: if tasks:
yield from asyncio.wait(tasks, loop=hass.loop) yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler, DOMAIN, SERVICE_SCAN, async_service_handler,
descriptions.get(SERVICE_SCAN),
schema=APPLE_TV_SCAN_SCHEMA) schema=APPLE_TV_SCAN_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
descriptions.get(SERVICE_AUTHENTICATE),
schema=APPLE_TV_AUTHENTICATE_SCHEMA) schema=APPLE_TV_AUTHENTICATE_SCHEMA)
return True return True

View file

@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.1.0'] REQUIREMENTS = ['pyarlo==0.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -7,14 +7,12 @@ https://home-assistant.io/components/automation/
import asyncio import asyncio
from functools import partial from functools import partial
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import CoreState from homeassistant.core import CoreState
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant import config as conf_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
@ -166,11 +164,6 @@ def async_setup(hass, config):
yield from _async_process_config(hass, config, component) yield from _async_process_config(hass, config, component)
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@asyncio.coroutine @asyncio.coroutine
def trigger_service_handler(service_call): def trigger_service_handler(service_call):
"""Handle automation triggers.""" """Handle automation triggers."""
@ -216,20 +209,20 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_service_handler, DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA) schema=TRIGGER_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler, DOMAIN, SERVICE_RELOAD, reload_service_handler,
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA) schema=RELOAD_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, toggle_service_handler, DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA) schema=SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
hass.services.async_register( hass.services.async_register(
DOMAIN, service, turn_onoff_service_handler, DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service), schema=SERVICE_SCHEMA) schema=SERVICE_SCHEMA)
return True return True

View file

@ -55,7 +55,7 @@ def async_trigger(hass, config, action):
# Ignore changes to state attributes if from/to is in use # Ignore changes to state attributes if from/to is in use
if (not match_all and from_s is not None and to_s is not None and if (not match_all and from_s is not None and to_s is not None and
from_s.last_changed == to_s.last_changed): from_s.state == to_s.state):
return return
if not time_delta: if not time_delta:

View file

@ -6,12 +6,10 @@ https://home-assistant.io/components/axis/
""" """
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.components.discovery import SERVICE_AXIS from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED, from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_EVENT, CONF_HOST, CONF_INCLUDE, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_NAME, CONF_PASSWORD, CONF_PORT,
@ -195,10 +193,6 @@ def setup(hass, config):
if not setup_device(hass, config, device_config): if not setup_device(hass, config, device_config):
_LOGGER.error("Couldn\'t set up %s", device_config[CONF_NAME]) _LOGGER.error("Couldn\'t set up %s", device_config[CONF_NAME])
# Services to communicate with device.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def vapix_service(call): def vapix_service(call):
"""Service to send a message.""" """Service to send a message."""
for _, device in AXIS_DEVICES.items(): for _, device in AXIS_DEVICES.items():
@ -216,7 +210,6 @@ def setup(hass, config):
hass.services.register(DOMAIN, hass.services.register(DOMAIN,
SERVICE_VAPIX_CALL, SERVICE_VAPIX_CALL,
vapix_service, vapix_service,
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
schema=SERVICE_SCHEMA) schema=SERVICE_SCHEMA)
return True return True

View file

@ -21,24 +21,27 @@ SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [ DEVICE_CLASSES = [
'battery', # On means low, Off means normal 'battery', # On means low, Off means normal
'cold', # On means cold (or too cold) 'cold', # On means cold, Off means normal
'connectivity', # On means connection present, Off = no connection 'connectivity', # On means connected, Off means disconnected
'gas', # CO, CO2, etc. 'door', # On means open, Off means closed
'heat', # On means hot (or too hot) 'garage_door', # On means open, Off means closed
'light', # Lightness threshold 'gas', # On means gas detected, Off means no gas (clear)
'moisture', # Specifically a wetness sensor 'heat', # On means hot, Off means normal
'motion', # Motion sensor 'light', # On means light detected, Off means no light
'moving', # On means moving, Off means stopped 'moisture', # On means wet, Off means dry
'occupancy', # On means occupied, Off means not occupied 'motion', # On means motion detected, Off means no motion (clear)
'opening', # Door, window, etc. 'moving', # On means moving, Off means not moving (stopped)
'occupancy', # On means occupied, Off means not occupied (clear)
'opening', # On means open, Off means closed
'plug', # On means plugged in, Off means unplugged 'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc 'power', # On means power detected, Off means no power
'presence', # On means home, Off means away 'presence', # On means home, Off means away
'problem', # On means there is a problem, Off means the status is OK 'problem', # On means problem detected, Off means no problem (OK)
'safety', # Generic on=unsafe, off=safe 'safety', # On means unsafe, Off means safe
'smoke', # Smoke detector 'smoke', # On means smoke detected, Off means no smoke (clear)
'sound', # On means sound detected, Off means no sound 'sound', # On means sound detected, Off means no sound (clear)
'vibration', # On means vibration detected, Off means no vibration 'vibration', # On means vibration detected, Off means no vibration
'window', # On means open, Off means closed
] ]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))

View file

@ -10,12 +10,22 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import ( from homeassistant.components.alarmdecoder import (
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE) CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
SIGNAL_RFX_MESSAGE)
DEPENDENCIES = ['alarmdecoder'] DEPENDENCIES = ['alarmdecoder']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_RF_BIT0 = 'rf_bit0'
ATTR_RF_LOW_BAT = 'rf_low_battery'
ATTR_RF_SUPERVISED = 'rf_supervised'
ATTR_RF_BIT3 = 'rf_bit3'
ATTR_RF_LOOP3 = 'rf_loop3'
ATTR_RF_LOOP2 = 'rf_loop2'
ATTR_RF_LOOP4 = 'rf_loop4'
ATTR_RF_LOOP1 = 'rf_loop1'
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices.""" """Set up the AlarmDecoder binary sensor devices."""
@ -26,7 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type) zone_rfid = device_config_data.get(CONF_ZONE_RFID)
device = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid)
devices.append(device) devices.append(device)
add_devices(devices) add_devices(devices)
@ -37,13 +49,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice): class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor.""" """Representation of an AlarmDecoder binary sensor."""
def __init__(self, zone_number, zone_name, zone_type): def __init__(self, zone_number, zone_name, zone_type, zone_rfid):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._zone_number = zone_number self._zone_number = zone_number
self._zone_type = zone_type self._zone_type = zone_type
self._state = 0 self._state = None
self._name = zone_name self._name = zone_name
self._type = zone_type self._rfid = zone_rfid
self._rfstate = None
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
@ -54,27 +67,34 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self.hass.helpers.dispatcher.async_dispatcher_connect( self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_ZONE_RESTORE, self._restore_callback) SIGNAL_ZONE_RESTORE, self._restore_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
@property @property
def name(self): def name(self):
"""Return the name of the entity.""" """Return the name of the entity."""
return self._name return self._name
@property
def icon(self):
"""Icon for device by its type."""
if "window" in self._name.lower():
return "mdi:window-open" if self.is_on else "mdi:window-closed"
if self._type == 'smoke':
return "mdi:fire"
return None
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed.""" """No polling needed."""
return False return False
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self._rfid and self._rfstate is not None:
attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False
attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False
attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False
attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False
attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False
attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False
attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False
attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False
return attr
@property @property
def is_on(self): def is_on(self):
"""Return true if sensor is on.""" """Return true if sensor is on."""
@ -96,3 +116,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
if zone is None or int(zone) == self._zone_number: if zone is None or int(zone) == self._zone_number:
self._state = 0 self._state = 0
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _rfx_message_callback(self, message):
"""Update RF state."""
if self._rfid and message and message.serial_number == self._rfid:
self._rfstate = message.value
self.schedule_update_ha_state()

2
homeassistant/components/binary_sensor/concord232.py Executable file → Normal file
View file

@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import (CONF_HOST, CONF_PORT) from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.14'] REQUIREMENTS = ['concord232==0.15']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,97 @@
"""
Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
DEPENDENCIES = ['deconz']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup binary sensor for deCONZ component."""
if discovery_info is None:
return
from pydeconz.sensor import DECONZ_BINARY_SENSOR
sensors = hass.data[DECONZ_DATA].sensors
entities = []
for sensor in sensors.values():
if sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
class DeconzBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor."""
def __init__(self, sensor):
"""Setup sensor and add update callback to get data from websocket."""
self._sensor = sensor
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return true if sensor is on."""
return self._sensor.is_tripped
@property
def name(self):
"""Return the name of the sensor."""
return self._sensor.name
@property
def device_class(self):
"""Class of the sensor."""
return self._sensor.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._sensor.sensor_icon
@property
def available(self):
"""Return True if sensor is available."""
return self._sensor.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {
ATTR_BATTERY_LEVEL: self._sensor.battery,
}
if self._sensor.type == PRESENCE:
attr['dark'] = self._sensor.dark
return attr

View file

@ -1,60 +0,0 @@
"""Support for reading binary states from a DoorBird video doorbell."""
from datetime import timedelta
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
from homeassistant.util import Throttle
DEPENDENCIES = ['doorbird']
_LOGGER = logging.getLogger(__name__)
_MIN_UPDATE_INTERVAL = timedelta(milliseconds=250)
SENSOR_TYPES = {
"doorbell": {
"name": "Doorbell Ringing",
"icon": {
True: "bell-ring",
False: "bell",
None: "bell-outline"
}
}
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the DoorBird binary sensor component."""
device = hass.data.get(DOORBIRD_DOMAIN)
add_devices([DoorBirdBinarySensor(device, "doorbell")], True)
class DoorBirdBinarySensor(BinarySensorDevice):
"""A binary sensor of a DoorBird device."""
def __init__(self, device, sensor_type):
"""Initialize a binary sensor on a DoorBird device."""
self._device = device
self._sensor_type = sensor_type
self._state = None
@property
def name(self):
"""Get the name of the sensor."""
return SENSOR_TYPES[self._sensor_type]["name"]
@property
def icon(self):
"""Get an icon to display."""
state_icon = SENSOR_TYPES[self._sensor_type]["icon"][self._state]
return "mdi:{}".format(state_icon)
@property
def is_on(self):
"""Get the state of the binary sensor."""
return self._state
@Throttle(_MIN_UPDATE_INTERVAL)
def update(self):
"""Pull the latest value from the device."""
self._state = self._device.doorbell_state()

View file

@ -12,7 +12,8 @@ from typing import Callable # noqa
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
import homeassistant.components.isy994 as isy from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
ISYDevice)
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
@ -20,9 +21,6 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
ISY_DEVICE_TYPES = { ISY_DEVICE_TYPES = {
'moisture': ['16.8', '16.13', '16.14'], 'moisture': ['16.8', '16.13', '16.14'],
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'], 'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
@ -34,16 +32,11 @@ ISY_DEVICE_TYPES = {
def setup_platform(hass, config: ConfigType, def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None): add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 binary sensor platform.""" """Set up the ISY994 binary sensor platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error("A connection has not been made to the ISY controller")
return False
devices = [] devices = []
devices_by_nid = {} devices_by_nid = {}
child_nodes = [] child_nodes = []
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM, for node in hass.data[ISY994_NODES][DOMAIN]:
states=STATES):
if node.parent_node is None: if node.parent_node is None:
device = ISYBinarySensorDevice(node) device = ISYBinarySensorDevice(node)
devices.append(device) devices.append(device)
@ -87,13 +80,8 @@ def setup_platform(hass, config: ConfigType,
device = ISYBinarySensorDevice(node) device = ISYBinarySensorDevice(node)
devices.append(device) devices.append(device)
for program in isy.PROGRAMS.get(DOMAIN, []): for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
try: devices.append(ISYBinarySensorProgram(name, status))
status = program[isy.KEY_STATUS]
except (KeyError, AssertionError):
pass
else:
devices.append(ISYBinarySensorProgram(program.name, status))
add_devices(devices) add_devices(devices)
@ -118,7 +106,7 @@ def _is_val_unknown(val):
return val == -1*float('inf') return val == -1*float('inf')
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice): class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor device. """Representation of an ISY994 binary sensor device.
Often times, a single device is represented by multiple nodes in the ISY, Often times, a single device is represented by multiple nodes in the ISY,
@ -258,7 +246,7 @@ class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
return self._device_class_from_type return self._device_class_from_type
class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice): class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
"""Representation of the battery state of an ISY994 sensor.""" """Representation of the battery state of an ISY994 sensor."""
def __init__(self, node, parent_device) -> None: def __init__(self, node, parent_device) -> None:
@ -361,7 +349,7 @@ class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
return attr return attr
class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice): class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor program. """Representation of an ISY994 binary sensor program.
This does not need all of the subnode logic in the device version of binary This does not need all of the subnode logic in the device version of binary

View file

@ -129,6 +129,11 @@ class KNXBinarySensor(BinarySensorDevice):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""
return self.device.name return self.device.name
@property
def available(self):
"""Return True if entity is available."""
return self.hass.data[DATA_KNX].connected
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed within KNX.""" """No polling needed within KNX."""

View file

@ -17,19 +17,15 @@ from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
CONF_DEVICE_CLASS) CONF_DEVICE_CLASS)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, valid_subscribe_topic) CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PAYLOAD_AVAILABLE = 'payload_available'
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_NAME = 'MQTT Binary sensor'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_AVAILABLE = 'online'
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -38,12 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
vol.Optional(CONF_PAYLOAD_AVAILABLE,
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
})
@asyncio.coroutine @asyncio.coroutine
@ -70,31 +61,29 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
)]) )])
class MqttBinarySensor(BinarySensorDevice): class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class, def __init__(self, name, state_topic, availability_topic, device_class,
qos, payload_on, payload_off, payload_available, qos, payload_on, payload_off, payload_available,
payload_not_available, value_template): payload_not_available, value_template):
"""Initialize the MQTT binary sensor.""" """Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._name = name self._name = name
self._state = None self._state = None
self._state_topic = state_topic self._state_topic = state_topic
self._availability_topic = availability_topic
self._available = True if availability_topic is None else False
self._device_class = device_class self._device_class = device_class
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
self._payload_available = payload_available
self._payload_not_available = payload_not_available
self._qos = qos self._qos = qos
self._template = value_template self._template = value_template
@asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe mqtt events. """Subscribe mqtt events."""
yield from super().async_added_to_hass()
This method must be run in the event loop and returns a coroutine.
"""
@callback @callback
def state_message_received(topic, payload, qos): def state_message_received(topic, payload, qos):
"""Handle a new received MQTT state message.""" """Handle a new received MQTT state message."""
@ -111,21 +100,6 @@ class MqttBinarySensor(BinarySensorDevice):
yield from mqtt.async_subscribe( yield from mqtt.async_subscribe(
self.hass, self._state_topic, state_message_received, self._qos) self.hass, self._state_topic, state_message_received, self._qos)
@callback
def availability_message_received(topic, payload, qos):
"""Handle a new received MQTT availability message."""
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._availability_topic is not None:
yield from mqtt.async_subscribe(
self.hass, self._availability_topic,
availability_message_received, self._qos)
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""
@ -136,11 +110,6 @@ class MqttBinarySensor(BinarySensorDevice):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return self._name return self._name
@property
def available(self) -> bool:
"""Return if the binary sensor is available."""
return self._available
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""

View file

@ -98,6 +98,11 @@ class RestBinarySensor(BinarySensorDevice):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._device_class return self._device_class
@property
def available(self):
"""Return the availability of this sensor."""
return self.rest.data is not None
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""

View file

@ -7,30 +7,40 @@ tested. Other types may need some work.
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF, CONF_NAME)
from homeassistant.components import rfxtrx from homeassistant.components import rfxtrx
from homeassistant.helpers import event as evt
from homeassistant.helpers import config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.rfxtrx import (
ATTR_NAME, ATTR_DATA_BITS, ATTR_OFF_DELAY, ATTR_FIRE_EVENT,
CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT,
CONF_DATA_BITS, CONF_DEVICES)
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import event as evt
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rfxtrx import (
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
ATTR_DATA_BITS, CONF_DEVICES
)
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
DEPENDENCIES = ["rfxtrx"] DEPENDENCIES = ["rfxtrx"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required("platform"): rfxtrx.DOMAIN, vol.Optional(CONF_DEVICES, default={}): {
vol.Optional(CONF_DEVICES, default={}): vol.All( cv.string: vol.Schema({
dict, rfxtrx.valid_binary_sensor), vol.Optional(CONF_NAME): cv.string,
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
vol.Optional(CONF_DATA_BITS): cv.positive_int,
vol.Optional(CONF_COMMAND_ON): cv.byte,
vol.Optional(CONF_COMMAND_OFF): cv.byte
})
},
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -46,17 +56,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if device_id in rfxtrx.RFX_DEVICES: if device_id in rfxtrx.RFX_DEVICES:
continue continue
if entity[ATTR_DATA_BITS] is not None: if entity[CONF_DATA_BITS] is not None:
_LOGGER.info("Masked device id: %s", _LOGGER.debug("Masked device id: %s",
rfxtrx.get_pt2262_deviceid(device_id, rfxtrx.get_pt2262_deviceid(device_id,
entity[ATTR_DATA_BITS])) entity[ATTR_DATA_BITS]))
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)", _LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)",
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS]) entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
device = RfxtrxBinarySensor(event, entity[ATTR_NAME], device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
entity[CONF_DEVICE_CLASS], entity[CONF_DEVICE_CLASS],
entity[ATTR_FIREEVENT], entity[ATTR_FIRE_EVENT],
entity[ATTR_OFF_DELAY], entity[ATTR_OFF_DELAY],
entity[ATTR_DATA_BITS], entity[ATTR_DATA_BITS],
entity[CONF_COMMAND_ON], entity[CONF_COMMAND_ON],
@ -82,14 +92,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if sensor is None: if sensor is None:
# Add the entity if not exists and automatic_add is True # Add the entity if not exists and automatic_add is True
if not config[ATTR_AUTOMATIC_ADD]: if not config[CONF_AUTOMATIC_ADD]:
return return
if event.device.packettype == 0x13: if event.device.packettype == 0x13:
poss_dev = rfxtrx.find_possible_pt2262_device(device_id) poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
if poss_dev is not None: if poss_dev is not None:
poss_id = slugify(poss_dev.event.device.id_string.lower()) poss_id = slugify(poss_dev.event.device.id_string.lower())
_LOGGER.info("Found possible matching deviceid %s.", _LOGGER.debug("Found possible matching deviceid %s.",
poss_id) poss_id)
pkt_id = "".join("{0:02x}".format(x) for x in event.data) pkt_id = "".join("{0:02x}".format(x) for x in event.data)
@ -107,7 +117,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
elif not isinstance(sensor, RfxtrxBinarySensor): elif not isinstance(sensor, RfxtrxBinarySensor):
return return
else: else:
_LOGGER.info("Binary sensor update " _LOGGER.debug("Binary sensor update "
"(Device_id: %s Class: %s Sub: %s)", "(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()), slugify(event.device.id_string.lower()),
event.device.__class__.__name__, event.device.__class__.__name__,
@ -163,10 +173,8 @@ class RfxtrxBinarySensor(BinarySensorDevice):
self._masked_id = rfxtrx.get_pt2262_deviceid( self._masked_id = rfxtrx.get_pt2262_deviceid(
event.device.id_string.lower(), event.device.id_string.lower(),
data_bits) data_bits)
else:
def __str__(self): self._masked_id = None
"""Return the name of the sensor."""
return self._name
@property @property
def name(self): def name(self):

View file

@ -38,6 +38,11 @@ SENSOR_SCHEMA = vol.Schema({
vol.All(cv.time_period, cv.positive_timedelta), vol.All(cv.time_period, cv.positive_timedelta),
}) })
SENSOR_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
SENSOR_SCHEMA,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
}) })

View file

@ -9,40 +9,48 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
ATTR_ENTITY_ID, CONF_DEVICE_CLASS) STATE_UNKNOWN)
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_HYSTERESIS = 'hysteresis' ATTR_HYSTERESIS = 'hysteresis'
ATTR_LOWER = 'lower'
ATTR_POSITION = 'position'
ATTR_SENSOR_VALUE = 'sensor_value' ATTR_SENSOR_VALUE = 'sensor_value'
ATTR_THRESHOLD = 'threshold'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
ATTR_UPPER = 'upper'
CONF_HYSTERESIS = 'hysteresis' CONF_HYSTERESIS = 'hysteresis'
CONF_LOWER = 'lower' CONF_LOWER = 'lower'
CONF_THRESHOLD = 'threshold'
CONF_UPPER = 'upper' CONF_UPPER = 'upper'
DEFAULT_NAME = 'Threshold' DEFAULT_NAME = 'Threshold'
DEFAULT_HYSTERESIS = 0.0 DEFAULT_HYSTERESIS = 0.0
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER] POSITION_ABOVE = 'above'
POSITION_BELOW = 'below'
POSITION_IN_RANGE = 'in_range'
POSITION_UNKNOWN = 'unknown'
TYPE_LOWER = 'lower'
TYPE_RANGE = 'range'
TYPE_UPPER = 'upper'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS):
vol.Coerce(float),
vol.Optional(CONF_LOWER): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UPPER): vol.Coerce(float),
}) })
@ -51,47 +59,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Threshold sensor.""" """Set up the Threshold sensor."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD) lower = config.get(CONF_LOWER)
upper = config.get(CONF_UPPER)
hysteresis = config.get(CONF_HYSTERESIS) hysteresis = config.get(CONF_HYSTERESIS)
limit_type = config.get(CONF_TYPE)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices([ThresholdSensor( async_add_devices([ThresholdSensor(
hass, entity_id, name, threshold, hass, entity_id, name, lower, upper, hysteresis, device_class)], True)
hysteresis, limit_type, device_class)
], True)
return True
class ThresholdSensor(BinarySensorDevice): class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor.""" """Representation of a Threshold sensor."""
def __init__(self, hass, entity_id, name, threshold, def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
hysteresis, limit_type, device_class): device_class):
"""Initialize the Threshold sensor.""" """Initialize the Threshold sensor."""
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self.is_upper = limit_type == 'upper'
self._name = name self._name = name
self._threshold = threshold self._threshold_lower = lower
self._threshold_upper = upper
self._hysteresis = hysteresis self._hysteresis = hysteresis
self._device_class = device_class self._device_class = device_class
self._state = False
self.sensor_value = 0
@callback self._state_position = None
self._state = False
self.sensor_value = None
# pylint: disable=invalid-name # pylint: disable=invalid-name
@callback
def async_threshold_sensor_state_listener( def async_threshold_sensor_state_listener(
entity, old_state, new_state): entity, old_state, new_state):
"""Handle sensor state changes.""" """Handle sensor state changes."""
if new_state.state == STATE_UNKNOWN:
return
try: try:
self.sensor_value = float(new_state.state) self.sensor_value = None if new_state.state == STATE_UNKNOWN \
except ValueError: else float(new_state.state)
_LOGGER.error("State is not numerical") except (ValueError, TypeError):
self.sensor_value = None
_LOGGER.warning("State is not numerical")
hass.async_add_job(self.async_update_ha_state, True) hass.async_add_job(self.async_update_ha_state, True)
@ -118,23 +123,67 @@ class ThresholdSensor(BinarySensorDevice):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._device_class return self._device_class
@property
def threshold_type(self):
"""Return the type of threshold this sensor represents."""
if self._threshold_lower and self._threshold_upper:
return TYPE_RANGE
elif self._threshold_lower:
return TYPE_LOWER
elif self._threshold_upper:
return TYPE_UPPER
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
return { return {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_THRESHOLD: self._threshold,
ATTR_HYSTERESIS: self._hysteresis, ATTR_HYSTERESIS: self._hysteresis,
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER, ATTR_LOWER: self._threshold_lower,
ATTR_POSITION: self._state_position,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_TYPE: self.threshold_type,
ATTR_UPPER: self._threshold_upper,
} }
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Get the latest data and updates the states.""" """Get the latest data and updates the states."""
if self._hysteresis == 0 and self.sensor_value == self._threshold: def below(threshold):
"""Determine if the sensor value is below a threshold."""
return self.sensor_value < (threshold - self._hysteresis)
def above(threshold):
"""Determine if the sensor value is above a threshold."""
return self.sensor_value > (threshold + self._hysteresis)
if self.sensor_value is None:
self._state_position = POSITION_UNKNOWN
self._state = False self._state = False
elif self.sensor_value > (self._threshold + self._hysteresis):
self._state = self.is_upper elif self.threshold_type == TYPE_LOWER:
elif self.sensor_value < (self._threshold - self._hysteresis): if below(self._threshold_lower):
self._state = not self.is_upper self._state_position = POSITION_BELOW
self._state = True
elif above(self._threshold_lower):
self._state_position = POSITION_ABOVE
self._state = False
elif self.threshold_type == TYPE_UPPER:
if above(self._threshold_upper):
self._state_position = POSITION_ABOVE
self._state = True
elif below(self._threshold_upper):
self._state_position = POSITION_BELOW
self._state = False
elif self.threshold_type == TYPE_RANGE:
if below(self._threshold_lower):
self._state_position = POSITION_BELOW
self._state = False
if above(self._threshold_upper):
self._state_position = POSITION_ABOVE
self._state = False
elif above(self._threshold_lower) and below(self._threshold_upper):
self._state_position = POSITION_IN_RANGE
self._state = True

View file

@ -11,21 +11,19 @@ import math
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
BinarySensorDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, CONF_ENTITY_ID,
CONF_FRIENDLY_NAME, STATE_UNKNOWN)
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
DEVICE_CLASSES_SCHEMA)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME,
CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME,
STATE_UNKNOWN)
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.13.3'] REQUIREMENTS = ['numpy==1.14.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,21 +34,21 @@ ATTR_INVERT = 'invert'
ATTR_SAMPLE_DURATION = 'sample_duration' ATTR_SAMPLE_DURATION = 'sample_duration'
ATTR_SAMPLE_COUNT = 'sample_count' ATTR_SAMPLE_COUNT = 'sample_count'
CONF_SENSORS = 'sensors'
CONF_ATTRIBUTE = 'attribute' CONF_ATTRIBUTE = 'attribute'
CONF_INVERT = 'invert'
CONF_MAX_SAMPLES = 'max_samples' CONF_MAX_SAMPLES = 'max_samples'
CONF_MIN_GRADIENT = 'min_gradient' CONF_MIN_GRADIENT = 'min_gradient'
CONF_INVERT = 'invert'
CONF_SAMPLE_DURATION = 'sample_duration' CONF_SAMPLE_DURATION = 'sample_duration'
CONF_SENSORS = 'sensors'
SENSOR_SCHEMA = vol.Schema({ SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_INVERT, default=False): cv.boolean,
vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int, vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int,
vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float), vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
vol.Optional(CONF_INVERT, default=False): cv.boolean,
vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int, vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
}) })
@ -129,11 +127,11 @@ class SensorTrend(BinarySensorDevice):
return { return {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
ATTR_FRIENDLY_NAME: self._name, ATTR_FRIENDLY_NAME: self._name,
ATTR_INVERT: self._invert,
ATTR_GRADIENT: self._gradient, ATTR_GRADIENT: self._gradient,
ATTR_INVERT: self._invert,
ATTR_MIN_GRADIENT: self._min_gradient, ATTR_MIN_GRADIENT: self._min_gradient,
ATTR_SAMPLE_DURATION: self._sample_duration,
ATTR_SAMPLE_COUNT: len(self.samples), ATTR_SAMPLE_COUNT: len(self.samples),
ATTR_SAMPLE_DURATION: self._sample_duration,
} }
@property @property

View file

@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
excludes = config.get(CONF_EXCLUDES) excludes = config.get(CONF_EXCLUDES)
days_offset = config.get(CONF_OFFSET) days_offset = config.get(CONF_OFFSET)
year = (datetime.now() + timedelta(days=days_offset)).year year = (get_date(datetime.today()) + timedelta(days=days_offset)).year
obj_holidays = getattr(holidays, country)(years=year) obj_holidays = getattr(holidays, country)(years=year)
if province: if province:
@ -99,6 +99,11 @@ def day_to_string(day):
return None return None
def get_date(date):
"""Return date. Needed for testing."""
return date
class IsWorkdaySensor(BinarySensorDevice): class IsWorkdaySensor(BinarySensorDevice):
"""Implementation of a Workday sensor.""" """Implementation of a Workday sensor."""
@ -156,7 +161,7 @@ class IsWorkdaySensor(BinarySensorDevice):
self._state = False self._state = False
# Get iso day of the week (1 = Monday, 7 = Sunday) # Get iso day of the week (1 = Monday, 7 = Sunday)
date = datetime.today() + timedelta(days=self._days_offset) date = get_date(datetime.today()) + timedelta(days=self._days_offset)
day = date.isoweekday() - 1 day = date.isoweekday() - 1
day_of_week = day_to_string(day) day_of_week = day_to_string(day)

0
homeassistant/components/calendar/demo.py Executable file → Normal file
View file

View file

@ -9,7 +9,6 @@ https://home-assistant.io/components/calendar.todoist/
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
@ -17,7 +16,6 @@ from homeassistant.components.calendar import (
CalendarEventDevice, PLATFORM_SCHEMA) CalendarEventDevice, PLATFORM_SCHEMA)
from homeassistant.components.google import ( from homeassistant.components.google import (
CONF_DEVICE_ID) CONF_DEVICE_ID)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
CONF_ID, CONF_NAME, CONF_TOKEN) CONF_ID, CONF_NAME, CONF_TOKEN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -178,10 +176,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(project_devices) add_devices(project_devices)
# Services:
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def handle_new_task(call): def handle_new_task(call):
"""Called when a user creates a new Todoist Task from HASS.""" """Called when a user creates a new Todoist Task from HASS."""
project_name = call.data[PROJECT_NAME] project_name = call.data[PROJECT_NAME]
@ -215,7 +209,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task, hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task,
descriptions[DOMAIN][SERVICE_NEW_TASK],
schema=NEW_TASK_SERVICE_SCHEMA) schema=NEW_TASK_SERVICE_SCHEMA)

View file

@ -12,7 +12,6 @@ from datetime import timedelta
import logging import logging
import hashlib import hashlib
from random import SystemRandom from random import SystemRandom
import os
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
@ -21,7 +20,6 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE) from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -190,19 +188,14 @@ def async_setup(hass, config):
except OSError as err: except OSError as err:
_LOGGER.error("Can't write image to file: %s", err) _LOGGER.error("Can't write image to file: %s", err)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service, DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_ENABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA) schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service, DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_DISABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA) schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service, DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service,
descriptions.get(SERVICE_SNAPSHOT),
schema=CAMERA_SERVICE_SNAPSHOT) schema=CAMERA_SERVICE_SNAPSHOT)
return True return True

View file

@ -1,51 +1,40 @@
"""Support for viewing the camera feed from a DoorBird video doorbell.""" """
Support for viewing the camera feed from a DoorBird video doorbell.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.doorbird/
"""
import asyncio import asyncio
import datetime import datetime
import logging import logging
import voluptuous as vol
import aiohttp import aiohttp
import async_timeout import async_timeout
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.camera import Camera
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
DEPENDENCIES = ['doorbird'] DEPENDENCIES = ['doorbird']
_CAMERA_LIVE = "DoorBird Live"
_CAMERA_LAST_VISITOR = "DoorBird Last Ring" _CAMERA_LAST_VISITOR = "DoorBird Last Ring"
_LIVE_INTERVAL = datetime.timedelta(seconds=1) _CAMERA_LIVE = "DoorBird Live"
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1) _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_TIMEOUT = 10 # seconds _TIMEOUT = 10 # seconds
CONF_SHOW_LAST_VISITOR = 'last_visitor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SHOW_LAST_VISITOR, default=False): cv.boolean
})
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the DoorBird camera platform.""" """Set up the DoorBird camera platform."""
device = hass.data.get(DOORBIRD_DOMAIN) device = hass.data.get(DOORBIRD_DOMAIN)
async_add_devices([
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LIVE) DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL),
entities = [DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, DoorBirdCamera(
_LIVE_INTERVAL)] device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
_LAST_VISITOR_INTERVAL),
if config.get(CONF_SHOW_LAST_VISITOR): ])
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LAST_VISITOR)
entities.append(DoorBirdCamera(device.history_image_url(1),
_CAMERA_LAST_VISITOR,
_LAST_VISITOR_INTERVAL))
async_add_devices(entities)
_LOGGER.info("Added DoorBird camera(s)")
class DoorBirdCamera(Camera): class DoorBirdCamera(Camera):
@ -75,7 +64,6 @@ class DoorBirdCamera(Camera):
try: try:
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop):
response = yield from websession.get(self._url) response = yield from websession.get(self._url)

6
homeassistant/components/camera/mqtt.py Executable file → Normal file
View file

@ -60,11 +60,9 @@ class MqttCamera(Camera):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name return self._name
@asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe MQTT events. """Subscribe MQTT events."""
This method must be run in the event loop and returns a coroutine.
"""
@callback @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""Handle new MQTT messages.""" """Handle new MQTT messages."""

View file

@ -82,6 +82,7 @@ class UnifiVideoCamera(Camera):
self.is_streaming = False self.is_streaming = False
self._connect_addr = None self._connect_addr = None
self._camera = None self._camera = None
self._motion_status = False
@property @property
def name(self): def name(self):
@ -94,6 +95,12 @@ class UnifiVideoCamera(Camera):
caminfo = self._nvr.get_camera(self._uuid) caminfo = self._nvr.get_camera(self._uuid)
return caminfo['recordingSettings']['fullTimeRecordEnabled'] return caminfo['recordingSettings']['fullTimeRecordEnabled']
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
caminfo = self._nvr.get_camera(self._uuid)
return caminfo['recordingSettings']['motionRecordEnabled']
@property @property
def brand(self): def brand(self):
"""Return the brand of this camera.""" """Return the brand of this camera."""
@ -165,3 +172,26 @@ class UnifiVideoCamera(Camera):
raise raise
return _get_image() return _get_image()
def set_motion_detection(self, mode):
"""Set motion detection on or off."""
from uvcclient.nvr import NvrError
if mode is True:
set_mode = 'motion'
else:
set_mode = 'none'
try:
self._nvr.set_recordmode(self._uuid, set_mode)
self._motion_status = mode
except NvrError as err:
_LOGGER.error("Unable to set recordmode to " + set_mode)
_LOGGER.debug(err)
def enable_motion_detection(self):
"""Enable motion detection in camera."""
self.set_motion_detection(True)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
self.set_motion_detection(False)

View file

@ -7,12 +7,10 @@ https://home-assistant.io/components/climate/
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import os
import functools as ft import functools as ft
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
@ -21,9 +19,9 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS) STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, )
DOMAIN = 'climate' DOMAIN = 'climate'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -63,6 +61,7 @@ SUPPORT_HOLD_MODE = 256
SUPPORT_SWING_MODE = 512 SUPPORT_SWING_MODE = 512
SUPPORT_AWAY_MODE = 1024 SUPPORT_AWAY_MODE = 1024
SUPPORT_AUX_HEAT = 2048 SUPPORT_AUX_HEAT = 2048
SUPPORT_ON_OFF = 4096
ATTR_CURRENT_TEMPERATURE = 'current_temperature' ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_MAX_TEMP = 'max_temp' ATTR_MAX_TEMP = 'max_temp'
@ -92,6 +91,10 @@ CONVERTIBLE_ATTRIBUTE = [
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ON_OFF_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SET_AWAY_MODE_SCHEMA = vol.Schema({ SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean, vol.Required(ATTR_AWAY_MODE): cv.boolean,
@ -240,10 +243,6 @@ def async_setup(hass, config):
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config) yield from component.async_setup(config)
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine @asyncio.coroutine
def async_away_mode_set_service(service): def async_away_mode_set_service(service):
"""Set away mode on target climate devices.""" """Set away mode on target climate devices."""
@ -267,7 +266,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service, DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA) schema=SET_AWAY_MODE_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -290,7 +288,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service, DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
descriptions.get(SERVICE_SET_HOLD_MODE),
schema=SET_HOLD_MODE_SCHEMA) schema=SET_HOLD_MODE_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -316,7 +313,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service, DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT),
schema=SET_AUX_HEAT_SCHEMA) schema=SET_AUX_HEAT_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -348,7 +344,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service, DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE),
schema=SET_TEMPERATURE_SCHEMA) schema=SET_TEMPERATURE_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -370,7 +365,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service, DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY),
schema=SET_HUMIDITY_SCHEMA) schema=SET_HUMIDITY_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -392,7 +386,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service, DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA) schema=SET_FAN_MODE_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -414,7 +407,6 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service, DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE),
schema=SET_OPERATION_MODE_SCHEMA) schema=SET_OPERATION_MODE_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
@ -436,9 +428,34 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service, DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE),
schema=SET_SWING_MODE_SCHEMA) schema=SET_SWING_MODE_SCHEMA)
@asyncio.coroutine
def async_on_off_service(service):
"""Handle on/off calls."""
target_climate = component.async_extract_from_service(service)
update_tasks = []
for climate in target_climate:
if service.service == SERVICE_TURN_ON:
yield from climate.async_turn_on()
elif service.service == SERVICE_TURN_OFF:
yield from climate.async_turn_off()
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_on_off_service,
schema=ON_OFF_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_on_off_service,
schema=ON_OFF_SERVICE_SCHEMA)
return True return True
@ -449,8 +466,12 @@ class ClimateDevice(Entity):
@property @property
def state(self): def state(self):
"""Return the current state.""" """Return the current state."""
if self.is_on is False:
return STATE_OFF
if self.current_operation: if self.current_operation:
return self.current_operation return self.current_operation
if self.is_on:
return STATE_ON
return STATE_UNKNOWN return STATE_UNKNOWN
@property @property
@ -594,6 +615,11 @@ class ClimateDevice(Entity):
"""Return the current hold mode, e.g., home, away, temp.""" """Return the current hold mode, e.g., home, away, temp."""
return None return None
@property
def is_on(self):
"""Return true if on."""
return None
@property @property
def is_aux_heat_on(self): def is_aux_heat_on(self):
"""Return true if aux heater.""" """Return true if aux heater."""
@ -730,6 +756,28 @@ class ClimateDevice(Entity):
""" """
return self.hass.async_add_job(self.turn_aux_heat_off) return self.hass.async_add_job(self.turn_aux_heat_off)
def turn_on(self):
"""Turn device on."""
raise NotImplementedError()
def async_turn_on(self):
"""Turn device on.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.turn_on)
def turn_off(self):
"""Turn device off."""
raise NotImplementedError()
def async_turn_off(self):
"""Turn device off.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.turn_off)
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""

View file

@ -0,0 +1,257 @@
"""
Support for the Daikin HVAC.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.daikin/
"""
import logging
import re
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE,
ATTR_CURRENT_TEMPERATURE, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_SWING_MODE, STATE_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL,
STATE_DRY, STATE_FAN_ONLY
)
from homeassistant.components.daikin import (
daikin_api_setup,
ATTR_TARGET_TEMPERATURE,
ATTR_INSIDE_TEMPERATURE,
ATTR_OUTSIDE_TEMPERATURE
)
from homeassistant.const import (
CONF_HOST, CONF_NAME,
TEMP_CELSIUS,
ATTR_TEMPERATURE
)
REQUIREMENTS = ['pydaikin==0.4']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE |
SUPPORT_SWING_MODE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=None): cv.string,
})
HA_STATE_TO_DAIKIN = {
STATE_FAN_ONLY: 'fan',
STATE_DRY: 'dry',
STATE_COOL: 'cool',
STATE_HEAT: 'hot',
STATE_AUTO: 'auto',
STATE_OFF: 'off',
}
HA_ATTR_TO_DAIKIN = {
ATTR_OPERATION_MODE: 'mode',
ATTR_FAN_MODE: 'f_rate',
ATTR_SWING_MODE: 'f_dir',
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Daikin HVAC platform."""
if discovery_info is not None:
host = discovery_info.get('ip')
name = None
_LOGGER.info("Discovered a Daikin AC on %s", host)
else:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
_LOGGER.info("Added Daikin AC on %s", host)
api = daikin_api_setup(hass, host, name)
add_devices([DaikinClimate(api)], True)
class DaikinClimate(ClimateDevice):
"""Representation of a Daikin HVAC."""
def __init__(self, api):
"""Initialize the climate device."""
from pydaikin import appliance
self._api = api
self._force_refresh = False
self._list = {
ATTR_OPERATION_MODE: list(
map(str.title, set(HA_STATE_TO_DAIKIN.values()))
),
ATTR_FAN_MODE: list(
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
)
),
ATTR_SWING_MODE: list(
map(
str.title,
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])
)
),
}
def get(self, key):
"""Retrieve device settings from API library cache."""
value = None
cast_to_float = False
if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE,
ATTR_CURRENT_TEMPERATURE]:
value = self._api.device.values.get('htemp')
cast_to_float = True
if key == ATTR_TARGET_TEMPERATURE:
value = self._api.device.values.get('stemp')
cast_to_float = True
elif key == ATTR_OUTSIDE_TEMPERATURE:
value = self._api.device.values.get('otemp')
cast_to_float = True
elif key == ATTR_FAN_MODE:
value = self._api.device.represent('f_rate')[1].title()
elif key == ATTR_SWING_MODE:
value = self._api.device.represent('f_dir')[1].title()
elif key == ATTR_OPERATION_MODE:
# Daikin can return also internal states auto-1 or auto-7
# and we need to translate them as AUTO
value = re.sub(
'[^a-z]',
'',
self._api.device.represent('mode')[1]
).title()
if value is None:
_LOGGER.warning("Invalid value requested for key %s", key)
else:
if value == "-" or value == "--":
value = None
elif cast_to_float:
try:
value = float(value)
except ValueError:
value = None
return value
def set(self, settings):
"""Set device settings using API."""
values = {}
for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE,
ATTR_OPERATION_MODE]:
value = settings.get(attr)
if value is None:
continue
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
if daikin_attr is not None:
if value.title() in self._list[attr]:
values[daikin_attr] = value.lower()
else:
_LOGGER.error("Invalid value %s for %s", attr, value)
# temperature
elif attr == ATTR_TEMPERATURE:
try:
values['stemp'] = str(int(value))
except ValueError:
_LOGGER.error("Invalid temperature %s", value)
if values:
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."""
return SUPPORT_FLAGS
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self._api.name
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self.get(ATTR_CURRENT_TEMPERATURE)
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.get(ATTR_TARGET_TEMPERATURE)
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
def set_temperature(self, **kwargs):
"""Set new target temperature."""
self.set(kwargs)
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self.get(ATTR_OPERATION_MODE)
@property
def operation_list(self):
"""Return the list of available operation modes."""
return self._list.get(ATTR_OPERATION_MODE)
def set_operation_mode(self, operation_mode):
"""Set HVAC mode."""
self.set({ATTR_OPERATION_MODE: operation_mode})
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self.get(ATTR_FAN_MODE)
def set_fan_mode(self, fan):
"""Set fan mode."""
self.set({ATTR_FAN_MODE: fan})
@property
def fan_list(self):
"""List of available fan modes."""
return self._list.get(ATTR_FAN_MODE)
@property
def current_swing_mode(self):
"""Return the fan setting."""
return self.get(ATTR_SWING_MODE)
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self.set({ATTR_SWING_MODE: swing_mode})
@property
def swing_list(self):
"""List of available swing modes."""
return self._list.get(ATTR_SWING_MODE)
def update(self):
"""Retrieve latest state."""
self._api.update(no_throttle=self._force_refresh)
self._force_refresh = False

View file

@ -9,14 +9,15 @@ from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE, SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
SUPPORT_ON_OFF)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT | SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -56,6 +57,7 @@ class DemoClimate(ClimateDevice):
self._swing_list = ['Auto', '1', '2', '3', 'Off'] self._swing_list = ['Auto', '1', '2', '3', 'Off']
self._target_temperature_high = target_temp_high self._target_temperature_high = target_temp_high
self._target_temperature_low = target_temp_low self._target_temperature_low = target_temp_low
self._on = True
@property @property
def supported_features(self): def supported_features(self):
@ -132,6 +134,11 @@ class DemoClimate(ClimateDevice):
"""Return true if aux heat is on.""" """Return true if aux heat is on."""
return self._aux return self._aux
@property
def is_on(self):
"""Return true if the device is on."""
return self._on
@property @property
def current_fan_mode(self): def current_fan_mode(self):
"""Return the fan setting.""" """Return the fan setting."""
@ -206,3 +213,13 @@ class DemoClimate(ClimateDevice):
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
self._aux = False self._aux = False
self.schedule_update_ha_state() self.schedule_update_ha_state()
def turn_on(self):
"""Turn on."""
self._on = True
self.schedule_update_ha_state()
def turn_off(self):
"""Turn off."""
self._on = False
self.schedule_update_ha_state()

View file

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.ecobee/ https://home-assistant.io/components/climate.ecobee/
""" """
import logging import logging
from os import path
import voluptuous as vol import voluptuous as vol
@ -17,7 +16,6 @@ from homeassistant.components.climate import (
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH) SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_CONFIGURING = {} _CONFIGURING = {}
@ -96,17 +94,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
thermostat.schedule_update_ha_state(True) thermostat.schedule_update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA) schema=SET_FAN_MIN_ON_TIME_SCHEMA)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
descriptions.get(SERVICE_RESUME_PROGRAM),
schema=RESUME_PROGRAM_SCHEMA) schema=RESUME_PROGRAM_SCHEMA)

View file

@ -0,0 +1,228 @@
"""
Support for Rheem EcoNet water heaters.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.econet/
"""
import datetime
import logging
import voluptuous as vol
from homeassistant.components.climate import (
DOMAIN,
PLATFORM_SCHEMA,
STATE_ECO, STATE_GAS, STATE_ELECTRIC,
STATE_HEAT_PUMP, STATE_HIGH_DEMAND,
STATE_OFF, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE,
ClimateDevice)
from homeassistant.const import (ATTR_ENTITY_ID,
CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyeconet==0.0.4']
_LOGGER = logging.getLogger(__name__)
ATTR_VACATION_START = 'next_vacation_start_date'
ATTR_VACATION_END = 'next_vacation_end_date'
ATTR_ON_VACATION = 'on_vacation'
ATTR_TODAYS_ENERGY_USAGE = 'todays_energy_usage'
ATTR_IN_USE = 'in_use'
ATTR_START_DATE = 'start_date'
ATTR_END_DATE = 'end_date'
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
SERVICE_ADD_VACATION = 'econet_add_vacation'
SERVICE_DELETE_VACATION = 'econet_delete_vacation'
ADD_VACATION_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_START_DATE): cv.positive_int,
vol.Required(ATTR_END_DATE): cv.positive_int,
})
DELETE_VACATION_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
ECONET_DATA = 'econet'
HA_STATE_TO_ECONET = {
STATE_ECO: 'Energy Saver',
STATE_ELECTRIC: 'Electric',
STATE_HEAT_PUMP: 'Heat Pump',
STATE_GAS: 'gas',
STATE_HIGH_DEMAND: 'High Demand',
STATE_OFF: 'Off',
}
ECONET_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_ECONET.items()}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the EcoNet water heaters."""
from pyeconet.api import PyEcoNet
hass.data[ECONET_DATA] = {}
hass.data[ECONET_DATA]['water_heaters'] = []
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
econet = PyEcoNet(username, password)
water_heaters = econet.get_water_heaters()
hass_water_heaters = [
EcoNetWaterHeater(water_heater) for water_heater in water_heaters]
add_devices(hass_water_heaters)
hass.data[ECONET_DATA]['water_heaters'].extend(hass_water_heaters)
def service_handle(service):
"""Handler for services."""
entity_ids = service.data.get('entity_id')
all_heaters = hass.data[ECONET_DATA]['water_heaters']
_heaters = [
x for x in all_heaters
if not entity_ids or x.entity_id in entity_ids]
for _water_heater in _heaters:
if service.service == SERVICE_ADD_VACATION:
start = service.data.get(ATTR_START_DATE)
end = service.data.get(ATTR_END_DATE)
_water_heater.add_vacation(start, end)
if service.service == SERVICE_DELETE_VACATION:
for vacation in _water_heater.water_heater.vacations:
vacation.delete()
_water_heater.schedule_update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_ADD_VACATION,
service_handle,
schema=ADD_VACATION_SCHEMA)
hass.services.register(DOMAIN, SERVICE_DELETE_VACATION,
service_handle,
schema=DELETE_VACATION_SCHEMA)
class EcoNetWaterHeater(ClimateDevice):
"""Representation of an EcoNet water heater."""
def __init__(self, water_heater):
"""Initialize the water heater."""
self.water_heater = water_heater
@property
def name(self):
"""Return the device name."""
return self.water_heater.name
@property
def available(self):
"""Return if the the device is online or not."""
return self.water_heater.is_connected
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
data = {}
vacations = self.water_heater.get_vacations()
if vacations:
data[ATTR_VACATION_START] = vacations[0].start_date
data[ATTR_VACATION_END] = vacations[0].end_date
data[ATTR_ON_VACATION] = self.water_heater.is_on_vacation
todays_usage = self.water_heater.total_usage_for_today
if todays_usage:
data[ATTR_TODAYS_ENERGY_USAGE] = todays_usage
data[ATTR_IN_USE] = self.water_heater.in_use
return data
@property
def current_operation(self):
"""
Return current operation as one of the following.
["eco", "heat_pump",
"high_demand", "electric_only"]
"""
current_op = ECONET_STATE_TO_HA.get(self.water_heater.mode)
return current_op
@property
def operation_list(self):
"""List of available operation modes."""
op_list = []
modes = self.water_heater.supported_modes
for mode in modes:
ha_mode = ECONET_STATE_TO_HA.get(mode)
if ha_mode is not None:
op_list.append(ha_mode)
else:
error = "Invalid operation mode mapping. " + mode + \
" doesn't map. Please report this."
_LOGGER.error(error)
return op_list
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_HEATER
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
if target_temp is not None:
self.water_heater.set_target_set_point(target_temp)
else:
_LOGGER.error("A target temperature must be provided.")
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode)
if op_mode_to_set is not None:
self.water_heater.set_mode(op_mode_to_set)
else:
_LOGGER.error("An operation mode must be provided.")
def add_vacation(self, start, end):
"""Add a vacation to this water heater."""
if not start:
start = datetime.datetime.now()
else:
start = datetime.datetime.fromtimestamp(start)
end = datetime.datetime.fromtimestamp(end)
self.water_heater.set_vacation_mode(start, end)
def update(self):
"""Get the latest date."""
self.water_heater.update_state()
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.water_heater.set_point
@property
def min_temp(self):
"""Return the minimum temperature."""
return self.water_heater.min_set_point
@property
def max_temp(self):
"""Return the maximum temperature."""
return self.water_heater.max_set_point

View file

@ -12,9 +12,9 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE) SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
@ -30,6 +30,7 @@ DEPENDENCIES = ['switch', 'sensor']
DEFAULT_TOLERANCE = 0.3 DEFAULT_TOLERANCE = 0.3
DEFAULT_NAME = 'Generic Thermostat' DEFAULT_NAME = 'Generic Thermostat'
DEFAULT_AWAY_TEMP = 16
CONF_HEATER = 'heater' CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor' CONF_SENSOR = 'target_sensor'
@ -42,7 +43,9 @@ CONF_COLD_TOLERANCE = 'cold_tolerance'
CONF_HOT_TOLERANCE = 'hot_tolerance' CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive' CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode' CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE CONF_AWAY_TEMP = 'away_temp'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_OPERATION_MODE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HEATER): cv.entity_id, vol.Required(CONF_HEATER): cv.entity_id,
@ -60,7 +63,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_KEEP_ALIVE): vol.All( vol.Optional(CONF_KEEP_ALIVE): vol.All(
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_INITIAL_OPERATION_MODE): vol.Optional(CONF_INITIAL_OPERATION_MODE):
vol.In([STATE_AUTO, STATE_OFF]) vol.In([STATE_AUTO, STATE_OFF]),
vol.Optional(CONF_AWAY_TEMP,
default=DEFAULT_AWAY_TEMP): vol.Coerce(float)
}) })
@ -79,11 +84,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hot_tolerance = config.get(CONF_HOT_TOLERANCE) hot_tolerance = config.get(CONF_HOT_TOLERANCE)
keep_alive = config.get(CONF_KEEP_ALIVE) keep_alive = config.get(CONF_KEEP_ALIVE)
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE) initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
away_temp = config.get(CONF_AWAY_TEMP)
async_add_devices([GenericThermostat( async_add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, cold_tolerance, target_temp, ac_mode, min_cycle_duration, cold_tolerance,
hot_tolerance, keep_alive, initial_operation_mode)]) hot_tolerance, keep_alive, initial_operation_mode, away_temp)])
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice):
@ -92,7 +98,7 @@ class GenericThermostat(ClimateDevice):
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
cold_tolerance, hot_tolerance, keep_alive, cold_tolerance, hot_tolerance, keep_alive,
initial_operation_mode): initial_operation_mode, away_temp):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.hass = hass self.hass = hass
self._name = name self._name = name
@ -103,17 +109,26 @@ class GenericThermostat(ClimateDevice):
self._hot_tolerance = hot_tolerance self._hot_tolerance = hot_tolerance
self._keep_alive = keep_alive self._keep_alive = keep_alive
self._initial_operation_mode = initial_operation_mode self._initial_operation_mode = initial_operation_mode
self._saved_target_temp = target_temp if target_temp is not None \
else away_temp
if self.ac_mode:
self._current_operation = STATE_COOL
self._operation_list = [STATE_COOL, STATE_OFF]
else:
self._current_operation = STATE_HEAT
self._operation_list = [STATE_HEAT, STATE_OFF]
if initial_operation_mode == STATE_OFF: if initial_operation_mode == STATE_OFF:
self._enabled = False self._enabled = False
else: else:
self._enabled = True self._enabled = True
self._active = False self._active = False
self._cur_temp = None self._cur_temp = None
self._min_temp = min_temp self._min_temp = min_temp
self._max_temp = max_temp self._max_temp = max_temp
self._target_temp = target_temp self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit self._unit = hass.config.units.temperature_unit
self._away_temp = away_temp
self._is_away = False
async_track_state_change( async_track_state_change(
hass, sensor_entity_id, self._async_sensor_changed) hass, sensor_entity_id, self._async_sensor_changed)
@ -124,10 +139,6 @@ class GenericThermostat(ClimateDevice):
async_track_time_interval( async_track_time_interval(
hass, self._async_keep_alive, self._keep_alive) hass, self._async_keep_alive, self._keep_alive)
sensor_state = hass.states.get(sensor_entity_id)
if sensor_state:
self._async_update_temp(sensor_state)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Run when entity about to be added.""" """Run when entity about to be added."""
@ -137,14 +148,37 @@ class GenericThermostat(ClimateDevice):
if old_state is not None: if old_state is not None:
# If we have no initial temperature, restore # If we have no initial temperature, restore
if self._target_temp is None: if self._target_temp is None:
# If we have a previously saved temperature
if old_state.attributes[ATTR_TEMPERATURE] is None:
if self.ac_mode:
self._target_temp = self.max_temp
else:
self._target_temp = self.min_temp
_LOGGER.warning('Undefined target temperature, \
falling back to %s', self._target_temp)
else:
self._target_temp = float( self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE]) old_state.attributes[ATTR_TEMPERATURE])
self._is_away = True if str(
# If we have no initial operation mode, restore old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON else False
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
self._current_operation = STATE_OFF
self._enabled = False
if self._initial_operation_mode is None: if self._initial_operation_mode is None:
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF: if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
self._enabled = False self._enabled = False
@property
def state(self):
"""Return the current state."""
if self._is_device_active:
return self.current_operation
else:
if self._enabled:
return STATE_IDLE
else:
return STATE_OFF
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""
@ -167,15 +201,8 @@ class GenericThermostat(ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation."""
if not self._enabled: return self._current_operation
return STATE_OFF
if self.ac_mode:
cooling = self._active and self._is_device_active
return STATE_COOL if cooling else STATE_IDLE
heating = self._active and self._is_device_active
return STATE_HEAT if heating else STATE_IDLE
@property @property
def target_temperature(self): def target_temperature(self):
@ -185,14 +212,20 @@ class GenericThermostat(ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""List of available operation modes.""" """List of available operation modes."""
return [STATE_AUTO, STATE_OFF] return self._operation_list
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
if operation_mode == STATE_AUTO: if operation_mode == STATE_HEAT:
self._current_operation = STATE_HEAT
self._enabled = True
self._async_control_heating()
elif operation_mode == STATE_COOL:
self._current_operation = STATE_COOL
self._enabled = True self._enabled = True
self._async_control_heating() self._async_control_heating()
elif operation_mode == STATE_OFF: elif operation_mode == STATE_OFF:
self._current_operation = STATE_OFF
self._enabled = False self._enabled = False
if self._is_device_active: if self._is_device_active:
self._heater_turn_off() self._heater_turn_off()
@ -252,7 +285,7 @@ class GenericThermostat(ClimateDevice):
@callback @callback
def _async_keep_alive(self, time): def _async_keep_alive(self, time):
"""Call at constant intervals for keep-alive purposes.""" """Call at constant intervals for keep-alive purposes."""
if self.current_operation in [STATE_COOL, STATE_HEAT]: if self._is_device_active:
self._heater_turn_on() self._heater_turn_on()
else: else:
self._heater_turn_off() self._heater_turn_off()
@ -347,3 +380,23 @@ class GenericThermostat(ClimateDevice):
data = {ATTR_ENTITY_ID: self.heater_entity_id} data = {ATTR_ENTITY_ID: self.heater_entity_id}
self.hass.async_add_job( self.hass.async_add_job(
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data)) self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._is_away
def turn_away_mode_on(self):
"""Turn away mode on by setting it on away hold indefinitely."""
self._is_away = True
self._saved_target_temp = self._target_temp
self._target_temp = self._away_temp
self._async_control_heating()
self.schedule_update_ha_state()
def turn_away_mode_off(self):
"""Turn away off."""
self._is_away = False
self._target_temp = self._saved_target_temp
self._async_control_heating()
self.schedule_update_ha_state()

View file

@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.hive/
""" """
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE from homeassistant.components.hive import DATA_HIVE
@ -16,7 +16,9 @@ HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL', HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
STATE_ON: 'ON', STATE_OFF: 'OFF'} STATE_ON: 'ON', STATE_OFF: 'OFF'}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_OPERATION_MODE |
SUPPORT_AUX_HEAT)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -134,6 +136,43 @@ class HiveClimateEntity(ClimateDevice):
for entity in self.session.entities: for entity in self.session.entities:
entity.handle_update(self.data_updatesource) entity.handle_update(self.data_updatesource)
@property
def is_aux_heat_on(self):
"""Return true if auxiliary heater is on."""
boost_status = None
if self.device_type == "Heating":
boost_status = self.session.heating.get_boost(self.node_id)
elif self.device_type == "HotWater":
boost_status = self.session.hotwater.get_boost(self.node_id)
return boost_status == "ON"
def turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
target_boost_time = 30
if self.device_type == "Heating":
curtemp = self.session.heating.current_temperature(self.node_id)
curtemp = round(curtemp * 2) / 2
target_boost_temperature = curtemp + 0.5
self.session.heating.turn_boost_on(self.node_id,
target_boost_time,
target_boost_temperature)
elif self.device_type == "HotWater":
self.session.hotwater.turn_boost_on(self.node_id,
target_boost_time)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
if self.device_type == "Heating":
self.session.heating.turn_boost_off(self.node_id)
elif self.device_type == "HotWater":
self.session.hotwater.turn_boost_off(self.node_id)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def update(self): def update(self):
"""Update all Node data frome Hive.""" """Update all Node data frome Hive."""
self.session.core.update_data(self.node_id) self.session.core.update_data(self.node_id)

View file

@ -8,7 +8,8 @@ import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE) SUPPORT_OPERATION_MODE)
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES from homeassistant.components.homematic import (
HMDevice, ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT)
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
@ -39,6 +40,7 @@ HM_HUMI_MAP = [
] ]
HM_CONTROL_MODE = 'CONTROL_MODE' HM_CONTROL_MODE = 'CONTROL_MODE'
HM_IP_CONTROL_MODE = 'SET_POINT_MODE'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
@ -75,11 +77,25 @@ class HMThermostat(HMDevice, ClimateDevice):
if HM_CONTROL_MODE not in self._data: if HM_CONTROL_MODE not in self._data:
return None return None
# read state and search set_point_mode = self._data.get('SET_POINT_MODE', -1)
for mode, state in HM_STATE_MAP.items(): control_mode = self._data.get('CONTROL_MODE', -1)
code = getattr(self._hmdevice, mode, 0) boost_mode = self._data.get('BOOST_MODE', False)
if self._data.get('CONTROL_MODE') == code:
return state # boost mode is active
if boost_mode:
return STATE_BOOST
# HM ip etrv 2 uses the set_point_mode to say if its
# auto or manual
elif not set_point_mode == -1:
code = set_point_mode
# Other devices use the control_mode
else:
code = control_mode
# get the name of the mode
name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code]
return name.lower()
@property @property
def operation_list(self): def operation_list(self):
@ -125,6 +141,7 @@ class HMThermostat(HMDevice, ClimateDevice):
if state == operation_mode: if state == operation_mode:
code = getattr(self._hmdevice, mode, 0) code = getattr(self._hmdevice, mode, 0)
self._hmdevice.MODE = code self._hmdevice.MODE = code
return
@property @property
def min_temp(self): def min_temp(self):
@ -141,7 +158,8 @@ class HMThermostat(HMDevice, ClimateDevice):
self._state = next(iter(self._hmdevice.WRITENODE.keys())) self._state = next(iter(self._hmdevice.WRITENODE.keys()))
self._data[self._state] = STATE_UNKNOWN self._data[self._state] = STATE_UNKNOWN
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE: if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \
HM_IP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
for node in self._hmdevice.SENSORNODE.keys(): for node in self._hmdevice.SENSORNODE.keys():

View file

@ -159,6 +159,11 @@ class KNXClimate(ClimateDevice):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""
return self.device.name return self.device.name
@property
def available(self):
"""Return True if entity is available."""
return self.hass.data[DATA_KNX].connected
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed within KNX.""" """No polling needed within KNX."""

View file

@ -20,8 +20,9 @@ from homeassistant.components.climate import (
SUPPORT_AUX_HEAT) SUPPORT_AUX_HEAT)
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN, from homeassistant.components.mqtt import (
MQTT_BASE_PLATFORM_SCHEMA) CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH) SPEED_HIGH)
@ -93,7 +94,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
}) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine @asyncio.coroutine
@ -134,19 +135,25 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
STATE_OFF, STATE_OFF, False, STATE_OFF, STATE_OFF, False,
config.get(CONF_SEND_IF_OFF), config.get(CONF_SEND_IF_OFF),
config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF)) config.get(CONF_PAYLOAD_OFF),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))
]) ])
class MqttClimate(ClimateDevice): class MqttClimate(MqttAvailability, ClimateDevice):
"""Representation of a demo climate device.""" """Representation of a demo climate device."""
def __init__(self, hass, name, topic, qos, retain, mode_list, def __init__(self, hass, name, topic, qos, retain, mode_list,
fan_mode_list, swing_mode_list, target_temperature, away, fan_mode_list, swing_mode_list, target_temperature, away,
hold, current_fan_mode, current_swing_mode, hold, current_fan_mode, current_swing_mode,
current_operation, aux, send_if_off, payload_on, current_operation, aux, send_if_off, payload_on,
payload_off): payload_off, availability_topic, payload_available,
payload_not_available):
"""Initialize the climate device.""" """Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self.hass = hass self.hass = hass
self._name = name self._name = name
self._topic = topic self._topic = topic
@ -169,8 +176,11 @@ class MqttClimate(ClimateDevice):
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
@asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Handle being added to home assistant.""" """Handle being added to home assistant."""
yield from super().async_added_to_hass()
@callback @callback
def handle_current_temp_received(topic, payload, qos): def handle_current_temp_received(topic, payload, qos):
"""Handle current temperature coming via MQTT.""" """Handle current temperature coming via MQTT."""

0
homeassistant/components/climate/mysensors.py Executable file → Normal file
View file

5
homeassistant/components/climate/netatmo.py Executable file → Normal file
View file

@ -79,11 +79,6 @@ class NetatmoThermostat(ClimateDevice):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return self._name
@property
def state(self):
"""Return the state of the device."""
return self._target_temperature
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""

View file

@ -0,0 +1,227 @@
"""
Support for NuHeat thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nuheat/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice,
DOMAIN,
SUPPORT_HOLD_MODE,
SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE,
STATE_AUTO,
STATE_HEAT,
STATE_IDLE)
from homeassistant.components.nuheat import DOMAIN as NUHEAT_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
DEPENDENCIES = ["nuheat"]
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:thermometer"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
# Hold modes
MODE_AUTO = STATE_AUTO # Run device schedule
MODE_HOLD_TEMPERATURE = "temperature"
MODE_TEMPORARY_HOLD = "temporary_temperature"
OPERATION_LIST = [STATE_HEAT, STATE_IDLE]
SCHEDULE_HOLD = 3
SCHEDULE_RUN = 1
SCHEDULE_TEMPORARY_HOLD = 2
SERVICE_RESUME_PROGRAM = "nuheat_resume_program"
RESUME_PROGRAM_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
})
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE |
SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NuHeat thermostat(s)."""
if discovery_info is None:
return
temperature_unit = hass.config.units.temperature_unit
api, serial_numbers = hass.data[NUHEAT_DOMAIN]
thermostats = [
NuHeatThermostat(api, serial_number, temperature_unit)
for serial_number in serial_numbers
]
add_devices(thermostats, True)
def resume_program_set_service(service):
"""Resume the program on the target thermostats."""
entity_id = service.data.get(ATTR_ENTITY_ID)
if entity_id:
target_thermostats = [device for device in thermostats
if device.entity_id in entity_id]
else:
target_thermostats = thermostats
for thermostat in target_thermostats:
thermostat.resume_program()
thermostat.schedule_update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
schema=RESUME_PROGRAM_SCHEMA)
class NuHeatThermostat(ClimateDevice):
"""Representation of a NuHeat Thermostat."""
def __init__(self, api, serial_number, temperature_unit):
"""Initialize the thermostat."""
self._thermostat = api.get_thermostat(serial_number)
self._temperature_unit = temperature_unit
self._force_update = False
@property
def name(self):
"""Return the name of the thermostat."""
return self._thermostat.room
@property
def icon(self):
"""Return the icon to use in the frontend."""
return ICON
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def temperature_unit(self):
"""Return the unit of measurement."""
if self._temperature_unit == "C":
return TEMP_CELSIUS
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
if self._temperature_unit == "C":
return self._thermostat.celsius
return self._thermostat.fahrenheit
@property
def current_operation(self):
"""Return current operation. ie. heat, idle."""
if self._thermostat.heating:
return STATE_HEAT
return STATE_IDLE
@property
def min_temp(self):
"""Return the minimum supported temperature for the thermostat."""
if self._temperature_unit == "C":
return self._thermostat.min_celsius
return self._thermostat.min_fahrenheit
@property
def max_temp(self):
"""Return the maximum supported temperature for the thermostat."""
if self._temperature_unit == "C":
return self._thermostat.max_celsius
return self._thermostat.max_fahrenheit
@property
def target_temperature(self):
"""Return the currently programmed temperature."""
if self._temperature_unit == "C":
return self._thermostat.target_celsius
return self._thermostat.target_fahrenheit
@property
def current_hold_mode(self):
"""Return current hold mode."""
schedule_mode = self._thermostat.schedule_mode
if schedule_mode == SCHEDULE_RUN:
return MODE_AUTO
if schedule_mode == SCHEDULE_HOLD:
return MODE_HOLD_TEMPERATURE
if schedule_mode == SCHEDULE_TEMPORARY_HOLD:
return MODE_TEMPORARY_HOLD
return MODE_AUTO
@property
def operation_list(self):
"""Return list of possible operation modes."""
return OPERATION_LIST
def resume_program(self):
"""Resume the thermostat's programmed schedule."""
self._thermostat.resume_schedule()
self._force_update = True
def set_hold_mode(self, hold_mode, **kwargs):
"""Update the hold mode of the thermostat."""
if hold_mode == MODE_AUTO:
schedule_mode = SCHEDULE_RUN
if hold_mode == MODE_HOLD_TEMPERATURE:
schedule_mode = SCHEDULE_HOLD
if hold_mode == MODE_TEMPORARY_HOLD:
schedule_mode = SCHEDULE_TEMPORARY_HOLD
self._thermostat.schedule_mode = schedule_mode
self._force_update = True
def set_temperature(self, **kwargs):
"""Set a new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if self._temperature_unit == "C":
self._thermostat.target_celsius = temperature
else:
self._thermostat.target_fahrenheit = temperature
_LOGGER.debug(
"Setting NuHeat thermostat temperature to %s %s",
temperature, self.temperature_unit)
self._force_update = True
def update(self):
"""Get the latest state from the thermostat."""
if self._force_update:
self._throttled_update(no_throttle=True)
self._force_update = False
else:
self._throttled_update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _throttled_update(self, **kwargs):
"""Get the latest state from the thermostat with a throttle."""
self._thermostat.get_data()

View file

@ -13,37 +13,49 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID,
STATE_ON, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA, ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_AUX_HEAT) SUPPORT_ON_OFF)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
REQUIREMENTS = ['pysensibo==1.0.1'] REQUIREMENTS = ['pysensibo==1.0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ALL = 'all' ALL = 'all'
TIMEOUT = 10 TIMEOUT = 10
SERVICE_ASSUME_STATE = 'sensibo_assume_state'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]),
}) })
ASSUME_STATE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_STATE): cv.string,
})
_FETCH_FIELDS = ','.join([ _FETCH_FIELDS = ','.join([
'room{name}', 'measurements', 'remoteCapabilities', 'room{name}', 'measurements', 'remoteCapabilities',
'acState', 'connectionStatus{isAlive}', 'temperatureUnit']) 'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS _INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | FIELD_TO_FLAG = {
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE | 'fanLevel': SUPPORT_FAN_MODE,
SUPPORT_AUX_HEAT) 'mode': SUPPORT_OPERATION_MODE,
'swing': SUPPORT_SWING_MODE,
'targetTemperature': SUPPORT_TARGET_TEMPERATURE,
'on': SUPPORT_ON_OFF,
}
@asyncio.coroutine @asyncio.coroutine
@ -68,6 +80,28 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if devices: if devices:
async_add_devices(devices) async_add_devices(devices)
@asyncio.coroutine
def async_assume_state(service):
"""Set state according to external service call.."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
target_climate = [device for device in devices
if device.entity_id in entity_ids]
else:
target_climate = devices
update_tasks = []
for climate in target_climate:
yield from climate.async_assume_state(
service.data.get(ATTR_STATE))
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_ASSUME_STATE, async_assume_state,
schema=ASSUME_STATE_SCHEMA)
class SensiboClimate(ClimateDevice): class SensiboClimate(ClimateDevice):
"""Representation of a Sensibo device.""" """Representation of a Sensibo device."""
@ -80,12 +114,13 @@ class SensiboClimate(ClimateDevice):
""" """
self._client = client self._client = client
self._id = data['id'] self._id = data['id']
self._external_state = None
self._do_update(data) self._do_update(data)
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return self._supported_features
def _do_update(self, data): def _do_update(self, data):
self._name = data['room']['name'] self._name = data['room']['name']
@ -106,6 +141,15 @@ class SensiboClimate(ClimateDevice):
else: else:
self._temperature_unit = self.unit_of_measurement self._temperature_unit = self.unit_of_measurement
self._temperatures_list = [] self._temperatures_list = []
self._supported_features = 0
for key in self._ac_states:
if key in FIELD_TO_FLAG:
self._supported_features |= FIELD_TO_FLAG[key]
@property
def state(self):
"""Return the current state."""
return self._external_state or super().state
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -188,7 +232,7 @@ class SensiboClimate(ClimateDevice):
return self._name return self._name
@property @property
def is_aux_heat_on(self): def is_on(self):
"""Return true if AC is on.""" """Return true if AC is on."""
return self._ac_states['on'] return self._ac_states['on']
@ -196,13 +240,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._temperatures_list[0] \ return self._temperatures_list[0] \
if len(self._temperatures_list) else super.min_temp() if len(self._temperatures_list) else super().min_temp()
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._temperatures_list[-1] \ return self._temperatures_list[-1] \
if len(self._temperatures_list) else super.max_temp() if len(self._temperatures_list) else super().max_temp()
@asyncio.coroutine @asyncio.coroutine
def async_set_temperature(self, **kwargs): def async_set_temperature(self, **kwargs):
@ -226,42 +270,62 @@ class SensiboClimate(ClimateDevice):
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property( yield from self._client.async_set_ac_state_property(
self._id, 'targetTemperature', temperature) self._id, 'targetTemperature', temperature, self._ac_states)
@asyncio.coroutine @asyncio.coroutine
def async_set_fan_mode(self, fan): def async_set_fan_mode(self, fan):
"""Set new target fan mode.""" """Set new target fan mode."""
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property( yield from self._client.async_set_ac_state_property(
self._id, 'fanLevel', fan) self._id, 'fanLevel', fan, self._ac_states)
@asyncio.coroutine @asyncio.coroutine
def async_set_operation_mode(self, operation_mode): def async_set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property( yield from self._client.async_set_ac_state_property(
self._id, 'mode', operation_mode) self._id, 'mode', operation_mode, self._ac_states)
@asyncio.coroutine @asyncio.coroutine
def async_set_swing_mode(self, swing_mode): def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property( yield from self._client.async_set_ac_state_property(
self._id, 'swing', swing_mode) self._id, 'swing', swing_mode, self._ac_states)
@asyncio.coroutine @asyncio.coroutine
def async_turn_aux_heat_on(self): def async_on(self):
"""Turn Sensibo unit on.""" """Turn Sensibo unit on."""
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property( yield from self._client.async_set_ac_state_property(
self._id, 'on', True) self._id, 'on', True, self._ac_states)
@asyncio.coroutine @asyncio.coroutine
def async_turn_aux_heat_off(self): def async_off(self):
"""Turn Sensibo unit on.""" """Turn Sensibo unit on."""
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property( yield from self._client.async_set_ac_state_property(
self._id, 'on', False) self._id, 'on', False, self._ac_states)
@asyncio.coroutine
def async_assume_state(self, state):
"""Set external state."""
change_needed = (state != STATE_OFF and not self.is_on) \
or (state == STATE_OFF and self.is_on)
if change_needed:
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
self._id,
'on',
state != STATE_OFF, # value
self._ac_states,
True # assumed_state
)
if state in [STATE_ON, STATE_OFF]:
self._external_state = None
else:
self._external_state = state
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):

View file

@ -80,7 +80,22 @@ set_swing_mode:
example: 'climate.nest' example: 'climate.nest'
swing_mode: swing_mode:
description: New value of swing mode. description: New value of swing mode.
example: 1 example:
turn_on:
description: Turn climate device on.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
turn_off:
description: Turn climate device off.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
ecobee_set_fan_min_on_time: ecobee_set_fan_min_on_time:
description: Set the minimum fan on time. description: Set the minimum fan on time.
fields: fields:
@ -100,3 +115,40 @@ ecobee_resume_program:
resume_all: resume_all:
description: Resume all events and return to the scheduled program. This default to false which removes only the top event. description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
example: true example: true
nuheat_resume_program:
description: Resume the programmed schedule.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
econet_add_vacation:
description: Add a vacation to your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.water_heater'
start_date:
description: The timestamp of when the vacation should start. (Optional, defaults to now)
example: 1513186320
end_date:
description: The timestamp of when the vacation should end.
example: 1513445520
econet_delete_vacation:
description: Delete your existing vacation from your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.water_heater'
sensibo_assume_state:
description: Set Sensibo device to external state.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'climate.kitchen'
state:
description: State to set.
example: 'idle'

View file

@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.tado/
""" """
import logging import logging
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE from homeassistant.const import ATTR_TEMPERATURE
@ -192,6 +192,11 @@ class TadoClimate(ClimateDevice):
"""Return true if away mode is on.""" """Return true if away mode is on."""
return self._is_away return self._is_away
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return PRECISION_TENTHS
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""

View file

@ -0,0 +1,90 @@
"""
Platform for Roth Touchline heat pump controller.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/climate.touchline/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pytouchline==0.6']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Touchline devices."""
from pytouchline import PyTouchline
host = config[CONF_HOST]
py_touchline = PyTouchline()
number_of_devices = int(py_touchline.get_number_of_devices(host))
devices = []
for device_id in range(0, number_of_devices):
devices.append(Touchline(PyTouchline(device_id)))
add_devices(devices, True)
class Touchline(ClimateDevice):
"""Representation of a Touchline device."""
def __init__(self, touchline_thermostat):
"""Initialize the climate device."""
self.unit = touchline_thermostat
self._name = None
self._current_temperature = None
self._target_temperature = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def update(self):
"""Update unit attributes."""
self.unit.update()
self._name = self.unit.get_name()
self._current_temperature = self.unit.get_current_temperature()
self._target_temperature = self.unit.get_target_temperature()
@property
def should_poll(self):
"""Return the polling state."""
return True
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, **kwargs):
"""Set new target temperature."""
if kwargs.get(ATTR_TEMPERATURE) is not None:
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
self.unit.set_target_temperature(self._target_temperature)

0
homeassistant/components/climate/zwave.py Executable file → Normal file
View file

View file

@ -5,13 +5,18 @@ import json
import logging import logging
import os import os
import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE) EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
from homeassistant.helpers import entityfilter from homeassistant.helpers import entityfilter
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as ga_sh
from . import http_api, iot from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
@ -21,22 +26,46 @@ REQUIREMENTS = ['warrant==0.6.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa' CONF_ALEXA = 'alexa'
CONF_ALEXA_FILTER = 'filter' CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
CONF_ALIASES = 'aliases'
MODE_DEV = 'development' MODE_DEV = 'development'
DEFAULT_MODE = 'production' DEFAULT_MODE = 'production'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({ CONF_ENTITY_CONFIG = 'entity_config'
ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string,
vol.Optional(alexa_sh.CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(alexa_sh.CONF_NAME): cv.string,
})
GOOGLE_ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT),
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
})
ASSISTANT_SCHEMA = vol.Schema({
vol.Optional( vol.Optional(
CONF_ALEXA_FILTER, CONF_FILTER,
default=lambda: entityfilter.generate_filter([], [], [], []) default=lambda: entityfilter.generate_filter([], [], [], [])
): entityfilter.FILTER_SCHEMA, ): entityfilter.FILTER_SCHEMA,
}) })
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
})
GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}
})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Optional(CONF_MODE, default=DEFAULT_MODE):
@ -46,7 +75,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str, vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -55,22 +85,26 @@ CONFIG_SCHEMA = vol.Schema({
def async_setup(hass, config): def async_setup(hass, config):
"""Initialize the Home Assistant cloud.""" """Initialize the Home Assistant cloud."""
if DOMAIN in config: if DOMAIN in config:
kwargs = config[DOMAIN] kwargs = dict(config[DOMAIN])
else: else:
kwargs = {CONF_MODE: DEFAULT_MODE} kwargs = {CONF_MODE: DEFAULT_MODE}
if CONF_ALEXA not in kwargs: alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
if CONF_GOOGLE_ACTIONS not in kwargs:
kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({})
kwargs[CONF_ALEXA] = alexa_sh.Config(
should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
)
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
@asyncio.coroutine success = yield from cloud.initialize()
def init_cloud(event):
"""Initialize connection."""
yield from cloud.initialize()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud) if not success:
return False
yield from http_api.async_setup(hass) yield from http_api.async_setup(hass)
return True return True
@ -79,12 +113,16 @@ def async_setup(hass, config):
class Cloud: class Cloud:
"""Store the configuration of the cloud connection.""" """Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None, def __init__(self, hass, mode, alexa, google_actions,
region=None, relayer=None, alexa=None): cognito_client_id=None, user_pool_id=None, region=None,
relayer=None):
"""Create an instance of Cloud.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
self.alexa_config = alexa self.alexa_config = alexa
self._google_actions = google_actions
self._gactions_config = None
self.jwt_keyset = None
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
@ -104,11 +142,6 @@ class Cloud:
self.region = info['region'] self.region = info['region']
self.relayer = info['relayer'] self.relayer = info['relayer']
@property
def cognito_email_based(self):
"""Return if cognito is email based."""
return not self.user_pool_id.endswith('GmV')
@property @property
def is_logged_in(self): def is_logged_in(self):
"""Get if cloud is logged in.""" """Get if cloud is logged in."""
@ -128,37 +161,44 @@ class Cloud:
@property @property
def claims(self): def claims(self):
"""Get the claims from the id token.""" """Return the claims from the id token."""
from jose import jwt return self._decode_claims(self.id_token)
return jwt.get_unverified_claims(self.id_token)
@property @property
def user_info_path(self): def user_info_path(self):
"""Get path to the stored auth.""" """Get path to the stored auth."""
return self.path('{}_auth.json'.format(self.mode)) return self.path('{}_auth.json'.format(self.mode))
@property
def gactions_config(self):
"""Return the Google Assistant config."""
if self._gactions_config is None:
conf = self._google_actions
def should_expose(entity):
"""If an entity should be exposed."""
return conf['filter'](entity.entity_id)
self._gactions_config = ga_sh.Config(
should_expose=should_expose,
agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG),
)
return self._gactions_config
@asyncio.coroutine @asyncio.coroutine
def initialize(self): def initialize(self):
"""Initialize and load cloud info.""" """Initialize and load cloud info."""
def load_config(): jwt_success = yield from self._fetch_jwt_keyset()
"""Load the configuration."""
# 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 jwt_success:
if os.path.isfile(user_info): return False
with open(user_info, 'rt') as file:
info = json.loads(file.read())
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
yield from self.hass.async_add_job(load_config) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
self._start_cloud)
if self.id_token is not None: return True
yield from self.iot.connect()
def path(self, *parts): def path(self, *parts):
"""Get config path inside cloud dir. """Get config path inside cloud dir.
@ -175,6 +215,7 @@ class Cloud:
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
self._gactions_config = None
yield from self.hass.async_add_job( yield from self.hass.async_add_job(
lambda: os.remove(self.user_info_path)) lambda: os.remove(self.user_info_path))
@ -187,3 +228,79 @@ class Cloud:
'access_token': self.access_token, 'access_token': self.access_token,
'refresh_token': self.refresh_token, 'refresh_token': self.refresh_token,
}, indent=4)) }, indent=4))
def _start_cloud(self, event):
"""Start the cloud component."""
# 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
with open(user_info, 'rt') as file:
info = json.loads(file.read())
# Validate tokens
try:
for token in 'id_token', 'access_token':
self._decode_claims(info[token])
except ValueError as err: # Raised when token is invalid
_LOGGER.warning('Found invalid token %s: %s', token, err)
return
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
self.hass.add_job(self.iot.connect())
@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""
session = async_get_clientsession(self.hass)
url = ("https://cognito-idp.us-east-1.amazonaws.com/"
"{}/.well-known/jwks.json".format(self.user_pool_id))
try:
with async_timeout.timeout(10, loop=self.hass.loop):
req = yield from session.get(url)
self.jwt_keyset = yield from req.json()
return True
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error fetching Cognito keyset: %s", err)
return False
def _decode_claims(self, token):
"""Decode the claims in a token."""
from jose import jwt, exceptions as jose_exceptions
try:
header = jwt.get_unverified_header(token)
except jose_exceptions.JWTError as err:
raise ValueError(str(err)) from None
kid = header.get("kid")
if kid is None:
raise ValueError('No kid in header')
# Locate the key for this kid
key = None
for key_dict in self.jwt_keyset["keys"]:
if key_dict["kid"] == kid:
key = key_dict
break
if not key:
raise ValueError(
"Unable to locate kid ({}) in keyset".format(kid))
try:
return jwt.decode(
token, key, audience=self.cognito_client_id, options={
'verify_exp': False,
})
except jose_exceptions.JWTError as err:
raise ValueError(str(err)) from None

View file

@ -1,5 +1,4 @@
"""Package to communicate with the authentication API.""" """Package to communicate with the authentication API."""
import hashlib
import logging import logging
@ -58,11 +57,6 @@ def _map_aws_exception(err):
return ex(err.response['Error']['Message']) return ex(err.response['Error']['Message'])
def _generate_username(email):
"""Generate a username from an email address."""
return hashlib.sha512(email.encode('utf-8')).hexdigest()
def register(cloud, email, password): def register(cloud, email, password):
"""Register a new account.""" """Register a new account."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
@ -72,10 +66,7 @@ def register(cloud, email, password):
# https://github.com/capless/warrant/pull/82 # https://github.com/capless/warrant/pull/82
cognito.add_base_attributes() cognito.add_base_attributes()
try: try:
if cloud.cognito_email_based:
cognito.register(email, password) cognito.register(email, password)
else:
cognito.register(_generate_username(email), password)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
@ -86,11 +77,22 @@ def confirm_register(cloud, confirmation_code, email):
cognito = _cognito(cloud) cognito = _cognito(cloud)
try: try:
if cloud.cognito_email_based:
cognito.confirm_sign_up(confirmation_code, email) cognito.confirm_sign_up(confirmation_code, email)
else: except ClientError as err:
cognito.confirm_sign_up(confirmation_code, raise _map_aws_exception(err)
_generate_username(email))
def resend_email_confirm(cloud, email):
"""Resend email confirmation."""
from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=email)
try:
cognito.client.resend_confirmation_code(
Username=email,
ClientId=cognito.client_id
)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
@ -99,10 +101,7 @@ def forgot_password(cloud, email):
"""Initiate forgotten password flow.""" """Initiate forgotten password flow."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email) cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.initiate_forgot_password() cognito.initiate_forgot_password()
@ -114,10 +113,7 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password.""" """Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email) cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.confirm_forgot_password(confirmation_code, new_password) cognito.confirm_forgot_password(confirmation_code, new_password)

View file

@ -23,6 +23,7 @@ def async_setup(hass):
hass.http.register_view(CloudAccountView) hass.http.register_view(CloudAccountView)
hass.http.register_view(CloudRegisterView) hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudConfirmRegisterView) hass.http.register_view(CloudConfirmRegisterView)
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView) hass.http.register_view(CloudForgotPasswordView)
hass.http.register_view(CloudConfirmForgotPasswordView) hass.http.register_view(CloudConfirmForgotPasswordView)
@ -172,6 +173,29 @@ class CloudConfirmRegisterView(HomeAssistantView):
return self.json_message('ok') return self.json_message('ok')
class CloudResendConfirmView(HomeAssistantView):
"""Resend email confirmation code."""
url = '/api/cloud/resend_confirm'
name = 'api:cloud:resend_confirm'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle resending confirm email code request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.resend_email_confirm, cloud, data['email'])
return self.json_message('ok')
class CloudForgotPasswordView(HomeAssistantView): class CloudForgotPasswordView(HomeAssistantView):
"""View to start Forgot Password flow..""" """View to start Forgot Password flow.."""
@ -228,6 +252,6 @@ def _account_data(cloud):
return { return {
'email': claims['email'], 'email': claims['email'],
'sub_exp': claims.get('custom:sub-exp'), 'sub_exp': claims['custom:sub-exp'],
'cloud': cloud.iot.state, 'cloud': cloud.iot.state,
} }

View file

@ -5,7 +5,8 @@ import logging
from aiohttp import hdrs, client_exceptions, WSMsgType from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home from homeassistant.components.alexa import smart_home as alexa
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api from . import auth_api
@ -78,7 +79,7 @@ class CloudIoT:
yield from hass.async_add_job(auth_api.check_token, self.cloud) yield from hass.async_add_job(auth_api.check_token, self.cloud)
self.client = client = yield from session.ws_connect( self.client = client = yield from session.ws_connect(
self.cloud.relayer, headers={ self.cloud.relayer, heartbeat=55, headers={
hdrs.AUTHORIZATION: hdrs.AUTHORIZATION:
'Bearer {}'.format(self.cloud.id_token) 'Bearer {}'.format(self.cloud.id_token)
}) })
@ -204,9 +205,18 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine @asyncio.coroutine
def async_handle_alexa(hass, cloud, payload): def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa.""" """Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass, result = yield from alexa.async_handle_message(hass, cloud.alexa_config,
cloud.alexa_config, payload)
payload)) return result
@HANDLERS.register('google_actions')
@asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions."""
result = yield from ga.async_handle_message(hass, cloud.gactions_config,
payload)
return result
@HANDLERS.register('cloud') @HANDLERS.register('cloud')

View file

@ -0,0 +1,90 @@
"""
Support for Coinbase.
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
from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['coinbase==2.0.6']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'coinbase'
CONF_API_SECRET = 'api_secret'
CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
DATA_COINBASE = 'coinbase_cache'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_SECRET): cv.string,
vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]):
vol.All(cv.ensure_list, [cv.string])
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Coinbase component.
Will automatically setup sensors to support
wallets discovered on the network.
"""
api_key = config[DOMAIN].get(CONF_API_KEY)
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)
if not hasattr(coinbase_data, 'accounts'):
return False
for account in coinbase_data.accounts.data:
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)
continue
native = coinbase_data.exchange_rates.currency
load_platform(hass,
'sensor',
DOMAIN,
{'native_currency': native,
'exchange_currency': currency},
config)
return True
class CoinbaseData(object):
"""Get the latest data and update the states."""
def __init__(self, api_key, api_secret):
"""Init the coinbase data object."""
from coinbase.wallet.client import Client
self.client = Client(api_key, api_secret)
self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from coinbase."""
from coinbase.wallet.error import AuthenticationError
try:
self.accounts = self.client.get_accounts()
self.exchange_rates = self.client.get_exchange_rates()
except AuthenticationError as coinbase_error:
_LOGGER.error("Authentication error connecting"
" to coinbase: %s", coinbase_error)

View file

@ -12,20 +12,27 @@ import warnings
import voluptuous as vol import voluptuous as vol
from homeassistant import core from homeassistant import core
from homeassistant.loader import bind_hass from homeassistant.components import http
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
from homeassistant.helpers import intent, config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.components import http from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
REQUIREMENTS = ['fuzzywuzzy==0.16.0']
REQUIREMENTS = ['fuzzywuzzy==0.15.1'] _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
ATTR_TEXT = 'text' ATTR_TEXT = 'text'
DEPENDENCIES = ['http']
DOMAIN = 'conversation' DOMAIN = 'conversation'
INTENT_TURN_OFF = 'HassTurnOff'
INTENT_TURN_ON = 'HassTurnOn'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)') REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REGEX_TYPE = type(re.compile(''))
SERVICE_PROCESS = 'process' SERVICE_PROCESS = 'process'
@ -39,12 +46,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
}) })
})}, extra=vol.ALLOW_EXTRA) })}, extra=vol.ALLOW_EXTRA)
INTENT_TURN_ON = 'HassTurnOn'
INTENT_TURN_OFF = 'HassTurnOff'
REGEX_TYPE = type(re.compile(''))
_LOGGER = logging.getLogger(__name__)
@core.callback @core.callback
@bind_hass @bind_hass

View file

@ -6,12 +6,10 @@ at https://home-assistant.io/components/counter/
""" """
import asyncio import asyncio
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -133,20 +131,12 @@ def async_setup(hass, config):
if tasks: if tasks:
yield from asyncio.wait(tasks, loop=hass.loop) yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_INCREMENT, async_handler_service, DOMAIN, SERVICE_INCREMENT, async_handler_service)
descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_DECREMENT, async_handler_service, DOMAIN, SERVICE_DECREMENT, async_handler_service)
descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_RESET, async_handler_service, DOMAIN, SERVICE_RESET, async_handler_service)
descriptions[SERVICE_RESET], SERVICE_SCHEMA)
yield from component.async_add_entities(entities) yield from component.async_add_entities(entities)
return True return True

View file

@ -8,11 +8,9 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -179,16 +177,12 @@ def async_setup(hass, config):
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD: for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get( schema = SERVICE_TO_METHOD[service_name].get(
'schema', COVER_SERVICE_SCHEMA) 'schema', COVER_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service, DOMAIN, service_name, async_handle_cover_service,
descriptions.get(service_name), schema=schema) schema=schema)
return True return True

View file

@ -8,8 +8,10 @@ import logging
from typing import Callable # noqa from typing import Callable # noqa
from homeassistant.components.cover import CoverDevice, DOMAIN from homeassistant.components.cover import CoverDevice, DOMAIN
import homeassistant.components.isy994 as isy from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN ISYDevice)
from homeassistant.const import (
STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN)
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -17,44 +19,32 @@ _LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = { VALUE_TO_STATE = {
0: STATE_CLOSED, 0: STATE_CLOSED,
101: STATE_UNKNOWN, 101: STATE_UNKNOWN,
102: 'stopped',
103: STATE_CLOSING,
104: STATE_OPENING
} }
UOM = ['97']
STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening', 'stopped']
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType, def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None): add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 cover platform.""" """Set up the ISY994 cover platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error("A connection has not been made to the ISY controller")
return False
devices = [] devices = []
for node in hass.data[ISY994_NODES][DOMAIN]:
for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES):
devices.append(ISYCoverDevice(node)) devices.append(ISYCoverDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []): for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
try: devices.append(ISYCoverProgram(name, status, actions))
status = program[isy.KEY_STATUS]
actions = program[isy.KEY_ACTIONS]
assert actions.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
devices.append(ISYCoverProgram(program.name, status, actions))
add_devices(devices) add_devices(devices)
class ISYCoverDevice(isy.ISYDevice, CoverDevice): class ISYCoverDevice(ISYDevice, CoverDevice):
"""Representation of an ISY994 cover device.""" """Representation of an ISY994 cover device."""
def __init__(self, node: object): def __init__(self, node: object):
"""Initialize the ISY994 cover device.""" """Initialize the ISY994 cover device."""
isy.ISYDevice.__init__(self, node) super().__init__(node)
@property @property
def current_cover_position(self) -> int: def current_cover_position(self) -> int:
@ -90,7 +80,7 @@ class ISYCoverProgram(ISYCoverDevice):
def __init__(self, name: str, node: object, actions: object) -> None: def __init__(self, name: str, node: object, actions: object) -> None:
"""Initialize the ISY994 cover program.""" """Initialize the ISY994 cover program."""
ISYCoverDevice.__init__(self, node) super().__init__(node)
self._name = name self._name = name
self._actions = actions self._actions = actions

View file

@ -124,6 +124,11 @@ class KNXCover(CoverDevice):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""
return self.device.name return self.device.name
@property
def available(self):
"""Return True if entity is available."""
return self.hass.data[DATA_KNX].connected
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed within KNX.""" """No polling needed within KNX."""

View file

@ -21,8 +21,9 @@ from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN) STATE_CLOSED, STATE_UNKNOWN)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic) CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
valid_publish_topic, valid_subscribe_topic, MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,8 +38,6 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template'
CONF_PAYLOAD_OPEN = 'payload_open' CONF_PAYLOAD_OPEN = 'payload_open'
CONF_PAYLOAD_CLOSE = 'payload_close' CONF_PAYLOAD_CLOSE = 'payload_close'
CONF_PAYLOAD_STOP = 'payload_stop' CONF_PAYLOAD_STOP = 'payload_stop'
CONF_PAYLOAD_AVAILABLE = 'payload_available'
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
CONF_STATE_OPEN = 'state_open' CONF_STATE_OPEN = 'state_open'
CONF_STATE_CLOSED = 'state_closed' CONF_STATE_CLOSED = 'state_closed'
CONF_TILT_CLOSED_POSITION = 'tilt_closed_value' CONF_TILT_CLOSED_POSITION = 'tilt_closed_value'
@ -52,8 +51,6 @@ DEFAULT_NAME = 'MQTT Cover'
DEFAULT_PAYLOAD_OPEN = 'OPEN' DEFAULT_PAYLOAD_OPEN = 'OPEN'
DEFAULT_PAYLOAD_CLOSE = 'CLOSE' DEFAULT_PAYLOAD_CLOSE = 'CLOSE'
DEFAULT_PAYLOAD_STOP = 'STOP' DEFAULT_PAYLOAD_STOP = 'STOP'
DEFAULT_PAYLOAD_AVAILABLE = 'online'
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
DEFAULT_RETAIN = False DEFAULT_RETAIN = False
DEFAULT_TILT_CLOSED_POSITION = 0 DEFAULT_TILT_CLOSED_POSITION = 0
@ -73,16 +70,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template, vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_AVAILABILITY_TOPIC, default=None): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
vol.Optional(CONF_PAYLOAD_AVAILABLE,
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
@ -98,7 +90,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, default=DEFAULT_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_TILT_INVERT_STATE, vol.Optional(CONF_TILT_INVERT_STATE,
default=DEFAULT_TILT_INVERT_STATE): cv.boolean, default=DEFAULT_TILT_INVERT_STATE): cv.boolean,
}) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine @asyncio.coroutine
@ -143,7 +135,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
)]) )])
class MqttCover(CoverDevice): class MqttCover(MqttAvailability, CoverDevice):
"""Representation of a cover that can be controlled using MQTT.""" """Representation of a cover that can be controlled using MQTT."""
def __init__(self, name, state_topic, command_topic, availability_topic, def __init__(self, name, state_topic, command_topic, availability_topic,
@ -154,21 +146,19 @@ class MqttCover(CoverDevice):
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
tilt_invert, position_topic, set_position_template): tilt_invert, position_topic, set_position_template):
"""Initialize the cover.""" """Initialize the cover."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._position = None self._position = None
self._state = None self._state = None
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
self._command_topic = command_topic self._command_topic = command_topic
self._availability_topic = availability_topic
self._available = True if availability_topic is None else False
self._tilt_command_topic = tilt_command_topic self._tilt_command_topic = tilt_command_topic
self._tilt_status_topic = tilt_status_topic self._tilt_status_topic = tilt_status_topic
self._qos = qos self._qos = qos
self._payload_open = payload_open self._payload_open = payload_open
self._payload_close = payload_close self._payload_close = payload_close
self._payload_stop = payload_stop self._payload_stop = payload_stop
self._payload_available = payload_available
self._payload_not_available = payload_not_available
self._state_open = state_open self._state_open = state_open
self._state_closed = state_closed self._state_closed = state_closed
self._retain = retain self._retain = retain
@ -186,10 +176,9 @@ class MqttCover(CoverDevice):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe MQTT events. """Subscribe MQTT events."""
yield from super().async_added_to_hass()
This method is a coroutine.
"""
@callback @callback
def tilt_updated(topic, payload, qos): def tilt_updated(topic, payload, qos):
"""Handle tilt updates.""" """Handle tilt updates."""
@ -266,11 +255,6 @@ class MqttCover(CoverDevice):
"""Return the name of the cover.""" """Return the name of the cover."""
return self._name return self._name
@property
def available(self) -> bool:
"""Return if cover is available."""
return self._available
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""

View file

@ -4,12 +4,29 @@ Support for RFXtrx cover components.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.rfxtrx/ https://home-assistant.io/components/cover.rfxtrx/
""" """
import voluptuous as vol
import homeassistant.components.rfxtrx as rfxtrx import homeassistant.components.rfxtrx as rfxtrx
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.components.rfxtrx import (
CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS,
CONF_SIGNAL_REPETITIONS, CONF_DEVICES)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['rfxtrx'] DEPENDENCIES = ['rfxtrx']
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean
})
},
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
vol.Coerce(int),
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):

View file

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/ https://home-assistant.io/components/cover.tahoma/
""" """
import logging import logging
from datetime import timedelta
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
from homeassistant.components.tahoma import ( from homeassistant.components.tahoma import (
@ -14,6 +15,8 @@ DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers.""" """Set up Tahoma covers."""
@ -70,4 +73,15 @@ class TahomaCover(TahomaDevice, CoverDevice):
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
if self.tahoma_device.type == \
'io:RollerShutterWithLowSpeedManagementIOComponent':
self.apply_action('setPosition', 'secured')
else:
self.apply_action('stopIdentify') self.apply_action('stopIdentify')
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent':
return 'window'
else:
return None

0
homeassistant/components/cover/tellstick.py Executable file → Normal file
View file

View file

@ -63,10 +63,15 @@ COVER_SCHEMA = vol.Schema({
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids vol.Optional(CONF_ENTITY_ID): cv.entity_ids
}) })
COVER_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
COVER_SCHEMA,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
}) })

View file

@ -41,7 +41,7 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
return self.current_cover_position < 0 return self.current_cover_position <= 0
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""

View file

@ -0,0 +1,138 @@
"""
Platform for the Daikin AC.
For more details about this component, please refer to the documentation
https://home-assistant.io/components/daikin/
"""
import logging
from datetime import timedelta
from socket import timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.discovery import SERVICE_DAIKIN
from homeassistant.const import (
CONF_HOSTS, CONF_ICON, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TYPE
)
from homeassistant.helpers import discovery
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle
REQUIREMENTS = ['pydaikin==0.4']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'daikin'
HTTP_RESOURCES = ['aircon/get_sensor_info', 'aircon/get_control_info']
ATTR_TARGET_TEMPERATURE = 'target_temperature'
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
COMPONENT_TYPES = ['climate', 'sensor']
SENSOR_TYPE_TEMPERATURE = 'temperature'
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
CONF_NAME: 'Inside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
},
ATTR_OUTSIDE_TEMPERATURE: {
CONF_NAME: 'Outside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
}
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(
CONF_HOSTS, default=[]
): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(
CONF_MONITORED_CONDITIONS,
default=list(SENSOR_TYPES.keys())
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)])
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Establish connection with Daikin."""
def discovery_dispatch(service, discovery_info):
"""Dispatcher for Daikin discovery events."""
host = discovery_info.get('ip')
if daikin_api_setup(hass, host) is None:
return
for component in COMPONENT_TYPES:
load_platform(hass, component, DOMAIN, discovery_info,
config)
discovery.listen(hass, SERVICE_DAIKIN, discovery_dispatch)
for host in config.get(DOMAIN, {}).get(CONF_HOSTS, []):
if daikin_api_setup(hass, host) is None:
continue
discovery_info = {
'ip': host,
CONF_MONITORED_CONDITIONS:
config[DOMAIN][CONF_MONITORED_CONDITIONS]
}
load_platform(hass, 'sensor', DOMAIN, discovery_info, config)
return True
def daikin_api_setup(hass, host, name=None):
"""Create a Daikin instance only once."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
api = hass.data[DOMAIN].get(host)
if api is None:
from pydaikin import appliance
try:
device = appliance.Appliance(host)
except timeout:
_LOGGER.error("Connection to Daikin could not be established")
return False
if name is None:
name = device.values['name']
api = DaikinApi(device, name)
return api
class DaikinApi(object):
"""Keep the Daikin instance in one place and centralize the update."""
def __init__(self, device, name):
"""Initialize the Daikin Handle."""
self.device = device
self.name = name
self.ip_address = device.ip
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Pull the latest data from Daikin."""
try:
for resource in HTTP_RESOURCES:
self.device.values.update(
self.device.get_resource(resource)
)
except timeout:
_LOGGER.warning(
"Connection failed for %s", self.ip_address
)

View file

@ -0,0 +1,170 @@
"""
Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.helpers import config_validation as cv
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==23']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port,
})
}, extra=vol.ALLOW_EXTRA)
SERVICE_FIELD = 'field'
SERVICE_DATA = 'data'
SERVICE_SCHEMA = vol.Schema({
vol.Required(SERVICE_FIELD): cv.string,
vol.Required(SERVICE_DATA): cv.string,
})
CONFIG_INSTRUCTIONS = """
Unlock your deCONZ gateway to register with Home Assistant.
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
2. Press "Unlock Gateway" button
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
"""
@asyncio.coroutine
def async_setup(hass, config):
"""Setup services and configuration for deCONZ component."""
result = False
config_file = yield from hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
@asyncio.coroutine
def async_deconz_discovered(service, discovery_info):
"""Called when deCONZ gateway has been found."""
deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
yield from async_request_configuration(hass, config, deconz_config)
if config_file:
result = yield from async_setup_deconz(hass, config, config_file)
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config:
result = yield from async_setup_deconz(hass, config, deconz_config)
else:
yield from async_request_configuration(hass, config, deconz_config)
return True
if not result:
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
return True
@asyncio.coroutine
def async_setup_deconz(hass, config, deconz_config):
"""Setup deCONZ session.
Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ.
"""
from pydeconz import DeconzSession
websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config)
result = yield from deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ.")
return False
hass.data[DOMAIN] = deconz
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(discovery.async_load_platform(
hass, component, DOMAIN, {}, config))
deconz.start()
@asyncio.coroutine
def async_configure(call):
"""Set attribute of device in deCONZ.
Field is a string representing a specific device in deCONZ
e.g. field='/lights/1/state'.
Data is a json object with what data you want to alter
e.g. data={'on': true}.
{
"field": "/lights/1/state",
"data": {"on": true}
}
See Dresden Elektroniks REST API documentation for details:
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
"""
deconz = hass.data[DOMAIN]
field = call.data.get(SERVICE_FIELD)
data = call.data.get(SERVICE_DATA)
yield from deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, 'configure', async_configure,
schema=SERVICE_SCHEMA)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz.close)
return True
@asyncio.coroutine
def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
@asyncio.coroutine
def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = yield from async_setup_deconz(hass, config, deconz_config)
if result:
yield from hass.async_add_job(save_json,
hass.config.path(CONFIG_FILE),
deconz_config)
configurator.async_request_done(request_id)
return
else:
configurator.async_notify_errors(
request_id, "Couldn't load configuration.")
else:
configurator.async_notify_errors(
request_id, "Couldn't get an API key.")
return
instructions = CONFIG_INSTRUCTIONS.format(
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
request_id = configurator.async_request_config(
"deCONZ", async_configuration_callback,
description=instructions,
entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway",
)

View file

@ -0,0 +1,10 @@
configure:
description: Set attribute of device in Deconz. See Dresden Elektroniks REST API documentation for details http://dresden-elektronik.github.io/deconz-rest-doc/rest/
fields:
field:
description: Field is a string representing a specific device in Deconz.
example: '/lights/1/state'
data:
description: Data is a json object with what data you want to alter.
example: '{"on": true}'

View file

@ -7,7 +7,6 @@ https://home-assistant.io/components/device_tracker/
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import os
from typing import Any, List, Sequence, Callable from typing import Any, List, Sequence, Callable
import aiohttp import aiohttp
@ -81,6 +80,8 @@ ATTR_VENDOR = 'vendor'
SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router' SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
@ -88,7 +89,7 @@ NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
})) }))
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME, vol.Optional(CONF_CONSIDER_HOME,
default=DEFAULT_CONSIDER_HOME): vol.All( default=DEFAULT_CONSIDER_HOME): vol.All(
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
@ -131,8 +132,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
conf = config.get(DOMAIN, []) conf = config.get(DOMAIN, [])
conf = conf[0] if conf else {} conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {}) defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
track_new = conf.get(CONF_TRACK_NEW)
if track_new is None:
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = yield from async_load_config(yaml_path, hass, consider_home) devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker( tracker = DeviceTracker(
@ -204,12 +208,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)} ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
yield from tracker.async_see(**args) yield from tracker.async_see(**args)
descriptions = yield from hass.async_add_job( hass.services.async_register(DOMAIN, SERVICE_SEE, async_see_service)
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
# restore # restore
yield from tracker.async_setup_tracked_device() yield from tracker.async_setup_tracked_device()
@ -227,7 +226,8 @@ class DeviceTracker(object):
self.devices = {dev.dev_id: dev for dev in devices} self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home self.consider_home = consider_home
self.track_new = defaults.get(CONF_TRACK_NEW, track_new) self.track_new = track_new if track_new is not None \
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
self.defaults = defaults self.defaults = defaults
self.group = None self.group = None
self._is_updating = asyncio.Lock(loop=hass.loop) self._is_updating = asyncio.Lock(loop=hass.loop)

View file

@ -67,6 +67,15 @@ _IP_NEIGH_REGEX = re.compile(
r'\s?(router)?' r'\s?(router)?'
r'(?P<status>(\w+))') r'(?P<status>(\w+))')
_ARP_CMD = 'arp -n'
_ARP_REGEX = re.compile(
r'.+\s' +
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
r'.+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
r'\s' +
r'.*')
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
@ -76,7 +85,22 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases') def _parse_lines(lines, regex):
"""Parse the lines using the given regular expression.
If a line can't be parsed it is logged and skipped in the output.
"""
results = []
for line in lines:
match = regex.search(line)
if not match:
_LOGGER.debug("Could not parse row: %s", line)
continue
results.append(match.groupdict())
return results
Device = namedtuple('Device', ['mac', 'ip', 'name'])
class AsusWrtDeviceScanner(DeviceScanner): class AsusWrtDeviceScanner(DeviceScanner):
@ -121,16 +145,13 @@ class AsusWrtDeviceScanner(DeviceScanner):
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() self._update_info()
return [client['mac'] for client in self.last_results] return list(self.last_results.keys())
def get_device_name(self, device): def get_device_name(self, device):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
if not self.last_results: if device not in self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None return None
return self.last_results[device].name
def _update_info(self): def _update_info(self):
"""Ensure the information from the ASUSWRT router is up to date. """Ensure the information from the ASUSWRT router is up to date.
@ -145,74 +166,88 @@ class AsusWrtDeviceScanner(DeviceScanner):
if not data: if not data:
return False return False
active_clients = [client for client in data.values() if self.last_results = data
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE' or
client['status'] == 'IN_ASSOCLIST']
self.last_results = active_clients
return True return True
def get_asuswrt_data(self): def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result.""" """Retrieve data from ASUSWRT.
result = self.connection.get_result()
if not result:
return {}
Calls various commands on the router and returns the superset of all
responses. Some commands will not work on some routers.
"""
devices = {} devices = {}
if self.mode == 'ap': devices.update(self._get_wl())
for lease in result.leases: devices = self._get_arp(devices)
match = _WL_REGEX.search(lease.decode('utf-8')) devices = self._get_neigh(devices)
if not self.mode == 'ap':
devices.update(self._get_leases(devices))
return devices
if not match: def _get_wl(self):
_LOGGER.warning("Could not parse wl row: %s", lease) lines = self.connection.run_command(_WL_CMD)
continue if not lines:
return {}
host = '' result = _parse_lines(lines, _WL_REGEX)
devices = {}
devices[match.group('mac').upper()] = { for device in result:
'host': host, mac = device['mac'].upper()
'status': 'IN_ASSOCLIST', devices[mac] = Device(mac, None, None)
'ip': '', return devices
'mac': match.group('mac').upper(),
}
else:
for lease in result.leases:
if lease.startswith(b'duid '):
continue
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
def _get_leases(self, cur_devices):
lines = self.connection.run_command(_LEASES_CMD)
if not lines:
return {}
lines = [line for line in lines if not line.startswith('duid ')]
result = _parse_lines(lines, _LEASES_REGEX)
devices = {}
for device in result:
# For leases where the client doesn't set a hostname, ensure it # For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line. # is blank and not '*', which breaks entity_id down the line.
host = match.group('host') host = device['host']
if host == '*': if host == '*':
host = '' host = ''
mac = device['mac'].upper()
devices[match.group('mac')] = { if mac in cur_devices:
'host': host, devices[mac] = Device(mac, device['ip'], host)
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
for neighbor in result.neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse neighbor row: %s",
neighbor)
continue
if match.group('mac') in devices:
devices[match.group('mac')]['status'] = (
match.group('status'))
return devices return devices
def _get_neigh(self, cur_devices):
lines = self.connection.run_command(_IP_NEIGH_CMD)
if not lines:
return {}
result = _parse_lines(lines, _IP_NEIGH_REGEX)
devices = {}
for device in result:
if device['mac']:
mac = device['mac'].upper()
devices[mac] = Device(mac, None, None)
else:
cur_devices = {
k: v for k, v in
cur_devices.items() if v.ip != device['ip']
}
cur_devices.update(devices)
return cur_devices
def _get_arp(self, cur_devices):
lines = self.connection.run_command(_ARP_CMD)
if not lines:
return {}
result = _parse_lines(lines, _ARP_REGEX)
devices = {}
for device in result:
if device['mac']:
mac = device['mac'].upper()
devices[mac] = Device(mac, device['ip'], None)
else:
cur_devices = {
k: v for k, v in
cur_devices.items() if v.ip != device['ip']
}
cur_devices.update(devices)
return cur_devices
class _Connection: class _Connection:
def __init__(self): def __init__(self):
@ -247,8 +282,8 @@ class SshConnection(_Connection):
self._ssh_key = ssh_key self._ssh_key = ssh_key
self._ap = ap self._ap = ap
def get_result(self): def run_command(self, command):
"""Retrieve a single AsusWrtResult through an SSH connection. """Run commands through an SSH connection.
Connect to the SSH server if not currently connected, otherwise Connect to the SSH server if not currently connected, otherwise
use the existing connection. use the existing connection.
@ -258,19 +293,10 @@ class SshConnection(_Connection):
try: try:
if not self.connected: if not self.connected:
self.connect() self.connect()
if self._ap: self._ssh.sendline(command)
neighbors = ['']
self._ssh.sendline(_WL_CMD)
self._ssh.prompt() self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1] lines = self._ssh.before.split(b'\n')[1:-1]
else: return [line.decode('utf-8') for line in lines]
self._ssh.sendline(_IP_NEIGH_CMD)
self._ssh.prompt()
neighbors = self._ssh.before.split(b'\n')[1:-1]
self._ssh.sendline(_LEASES_CMD)
self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1]
return AsusWrtResult(neighbors, leases_result)
except exceptions.EOF as err: except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?") _LOGGER.error("Connection refused. SSH enabled?")
self.disconnect() self.disconnect()
@ -326,8 +352,8 @@ class TelnetConnection(_Connection):
self._ap = ap self._ap = ap
self._prompt_string = None self._prompt_string = None
def get_result(self): def run_command(self, command):
"""Retrieve a single AsusWrtResult through a Telnet connection. """Run a command through a Telnet connection.
Connect to the Telnet server if not currently connected, otherwise Connect to the Telnet server if not currently connected, otherwise
use the existing connection. use the existing connection.
@ -336,18 +362,10 @@ class TelnetConnection(_Connection):
if not self.connected: if not self.connected:
self.connect() self.connect()
self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii')) self._telnet.write('{}\n'.format(command).encode('ascii'))
neighbors = (self._telnet.read_until(self._prompt_string). data = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1]) split(b'\n')[1:-1])
if self._ap: return [line.decode('utf-8') for line in data]
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
else:
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
return AsusWrtResult(neighbors, leases_result)
except EOFError: except EOFError:
_LOGGER.error("Unexpected response from router") _LOGGER.error("Unexpected response from router")
self.disconnect() self.disconnect()

View file

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
PLATFORM_SCHEMA, load_config PLATFORM_SCHEMA, load_config, SOURCE_TYPE_BLUETOOTH_LE
) )
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -54,7 +54,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
new_devices[address] = 1 new_devices[address] = 1
return return
see(mac=BLE_PREFIX + address, host_name=name.strip("\x00")) see(mac=BLE_PREFIX + address, host_name=name.strip("\x00"),
source_type=SOURCE_TYPE_BLUETOOTH_LE)
def discover_ble_devices(): def discover_ble_devices():
"""Discover Bluetooth LE devices.""" """Discover Bluetooth LE devices."""

View file

@ -12,7 +12,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW) load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH)
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,7 +33,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
def see_device(device): def see_device(device):
"""Mark a device as seen.""" """Mark a device as seen."""
see(mac=BT_PREFIX + device[0], host_name=device[1]) see(mac=BT_PREFIX + device[0], host_name=device[1],
source_type=SOURCE_TYPE_BLUETOOTH)
def discover_devices(): def discover_devices():
"""Discover Bluetooth devices.""" """Discover Bluetooth devices."""

0
homeassistant/components/device_tracker/geofency.py Executable file → Normal file
View file

View file

@ -5,23 +5,37 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/ https://home-assistant.io/components/device_tracker.gpslogger/
""" """
import asyncio import asyncio
from functools import partial
import logging import logging
from hmac import compare_digest
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY from aiohttp.web import Request, HTTPUnauthorized # NOQA
from homeassistant.components.http import HomeAssistantView import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY
)
from homeassistant.components.http import (
CONF_API_PASSWORD, HomeAssistantView
)
# pylint: disable=unused-import # pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA) DOMAIN, PLATFORM_SCHEMA
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config, see, discovery_info=None):
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application.""" """Set up an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(see)) hass.http.register_view(GPSLoggerView(async_see, config))
return True return True
@ -32,26 +46,36 @@ class GPSLoggerView(HomeAssistantView):
url = '/api/gpslogger' url = '/api/gpslogger'
name = 'api:gpslogger' name = 'api:gpslogger'
def __init__(self, see): def __init__(self, async_see, config):
"""Initialize GPSLogger url endpoints.""" """Initialize GPSLogger url endpoints."""
self.see = see self.async_see = async_see
self._password = config.get(CONF_PASSWORD)
# this component does not require external authentication if
# password is set
self.requires_auth = self._password is None
@asyncio.coroutine @asyncio.coroutine
def get(self, request): def get(self, request: Request):
"""Handle for GPSLogger message received as GET.""" """Handle for GPSLogger message received as GET."""
res = yield from self._handle(request.app['hass'], request.query) hass = request.app['hass']
return res data = request.query
if self._password is not None:
authenticated = CONF_API_PASSWORD in data and compare_digest(
self._password,
data[CONF_API_PASSWORD]
)
if not authenticated:
raise HTTPUnauthorized()
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle GPSLogger requests."""
if 'latitude' not in data or 'longitude' not in data: if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.', return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data: if 'device' not in data:
_LOGGER.error("Device id not specified") _LOGGER.error("Device id not specified")
return ('Device id not specified.', HTTP_UNPROCESSABLE_ENTITY) return ('Device id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '') device = data['device'].replace('-', '')
gps_location = (data['latitude'], data['longitude']) gps_location = (data['latitude'], data['longitude'])
@ -75,10 +99,11 @@ class GPSLoggerView(HomeAssistantView):
if 'activity' in data: if 'activity' in data:
attrs['activity'] = data['activity'] attrs['activity'] = data['activity']
yield from hass.async_add_job( hass.async_add_job(self.async_see(
partial(self.see, dev_id=device, dev_id=device,
gps=gps_location, battery=battery, gps=gps_location, battery=battery,
gps_accuracy=accuracy, gps_accuracy=accuracy,
attributes=attrs)) attributes=attrs
))
return 'Setting location for {}'.format(device) return 'Setting location for {}'.format(device)

View file

@ -32,19 +32,27 @@ CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CONF_SECRET = 'secret' CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints' CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
CONF_MQTT_TOPIC = 'mqtt_topic'
CONF_REGION_MAPPING = 'region_mapping'
CONF_EVENTS_ONLY = 'events_only'
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
OWNTRACKS_TOPIC = 'owntracks/#' DEFAULT_OWNTRACKS_TOPIC = 'owntracks/#'
REGION_MAPPING = {}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float), vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean, vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
vol.Optional(CONF_EVENTS_ONLY, default=False): cv.boolean,
vol.Optional(CONF_MQTT_TOPIC, default=DEFAULT_OWNTRACKS_TOPIC):
mqtt.valid_subscribe_topic,
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All( vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
cv.ensure_list, [cv.string]), cv.ensure_list, [cv.string]),
vol.Optional(CONF_SECRET): vol.Any( vol.Optional(CONF_SECRET): vol.Any(
vol.Schema({vol.Optional(cv.string): cv.string}), vol.Schema({vol.Optional(cv.string): cv.string}),
cv.string) cv.string),
vol.Optional(CONF_REGION_MAPPING, default=REGION_MAPPING): dict
}) })
@ -82,31 +90,39 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
yield from async_handle_message(hass, context, message) yield from async_handle_message(hass, context, message)
yield from mqtt.async_subscribe( yield from mqtt.async_subscribe(
hass, OWNTRACKS_TOPIC, async_handle_mqtt_message, 1) hass, context.mqtt_topic, async_handle_mqtt_message, 1)
return True return True
def _parse_topic(topic): def _parse_topic(topic, subscribe_topic):
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple. """Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
Async friendly. Async friendly.
""" """
subscription = subscribe_topic.split('/')
try: try:
_, user, device, *_ = topic.split('/', 3) user_index = subscription.index('#')
except ValueError: except ValueError:
_LOGGER.error("Can't parse subscription topic: '%s'", subscribe_topic)
raise
topic_list = topic.split('/')
try:
user, device = topic_list[user_index], topic_list[user_index + 1]
except IndexError:
_LOGGER.error("Can't parse topic: '%s'", topic) _LOGGER.error("Can't parse topic: '%s'", topic)
raise raise
return user, device return user, device
def _parse_see_args(message): def _parse_see_args(message, subscribe_topic):
"""Parse the OwnTracks location parameters, into the format see expects. """Parse the OwnTracks location parameters, into the format see expects.
Async friendly. Async friendly.
""" """
user, device = _parse_topic(message['topic']) user, device = _parse_topic(message['topic'], subscribe_topic)
dev_id = slugify('{}_{}'.format(user, device)) dev_id = slugify('{}_{}'.format(user, device))
kwargs = { kwargs = {
'dev_id': dev_id, 'dev_id': dev_id,
@ -185,16 +201,20 @@ def context_from_config(async_see, config):
waypoint_import = config.get(CONF_WAYPOINT_IMPORT) waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
secret = config.get(CONF_SECRET) secret = config.get(CONF_SECRET)
region_mapping = config.get(CONF_REGION_MAPPING)
events_only = config.get(CONF_EVENTS_ONLY)
mqtt_topic = config.get(CONF_MQTT_TOPIC)
return OwnTracksContext(async_see, secret, max_gps_accuracy, return OwnTracksContext(async_see, secret, max_gps_accuracy,
waypoint_import, waypoint_whitelist) waypoint_import, waypoint_whitelist,
region_mapping, events_only, mqtt_topic)
class OwnTracksContext: class OwnTracksContext:
"""Hold the current OwnTracks context.""" """Hold the current OwnTracks context."""
def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints, def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints,
waypoint_whitelist): waypoint_whitelist, region_mapping, events_only, mqtt_topic):
"""Initialize an OwnTracks context.""" """Initialize an OwnTracks context."""
self.async_see = async_see self.async_see = async_see
self.secret = secret self.secret = secret
@ -203,6 +223,9 @@ class OwnTracksContext:
self.regions_entered = defaultdict(list) self.regions_entered = defaultdict(list)
self.import_waypoints = import_waypoints self.import_waypoints = import_waypoints
self.waypoint_whitelist = waypoint_whitelist self.waypoint_whitelist = waypoint_whitelist
self.region_mapping = region_mapping
self.events_only = events_only
self.mqtt_topic = mqtt_topic
@callback @callback
def async_valid_accuracy(self, message): def async_valid_accuracy(self, message):
@ -267,7 +290,11 @@ def async_handle_location_message(hass, context, message):
if not context.async_valid_accuracy(message): if not context.async_valid_accuracy(message):
return return
dev_id, kwargs = _parse_see_args(message) if context.events_only:
_LOGGER.debug("Location update ignored due to events_only setting")
return
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
if context.regions_entered[dev_id]: if context.regions_entered[dev_id]:
_LOGGER.debug( _LOGGER.debug(
@ -283,7 +310,7 @@ def async_handle_location_message(hass, context, message):
def _async_transition_message_enter(hass, context, message, location): def _async_transition_message_enter(hass, context, message, location):
"""Execute enter event.""" """Execute enter event."""
zone = hass.states.get("zone.{}".format(slugify(location))) zone = hass.states.get("zone.{}".format(slugify(location)))
dev_id, kwargs = _parse_see_args(message) dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
if zone is None and message.get('t') == 'b': if zone is None and message.get('t') == 'b':
# Not a HA zone, and a beacon so mobile beacon. # Not a HA zone, and a beacon so mobile beacon.
@ -309,7 +336,7 @@ def _async_transition_message_enter(hass, context, message, location):
@asyncio.coroutine @asyncio.coroutine
def _async_transition_message_leave(hass, context, message, location): def _async_transition_message_leave(hass, context, message, location):
"""Execute leave event.""" """Execute leave event."""
dev_id, kwargs = _parse_see_args(message) dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
regions = context.regions_entered[dev_id] regions = context.regions_entered[dev_id]
if location in regions: if location in regions:
@ -352,6 +379,12 @@ def async_handle_transition_message(hass, context, message):
# OwnTracks uses - at the start of a beacon zone # OwnTracks uses - at the start of a beacon zone
# to switch on 'hold mode' - ignore this # to switch on 'hold mode' - ignore this
location = message['desc'].lstrip("-") location = message['desc'].lstrip("-")
# Create a layer of indirection for Owntracks instances that may name
# regions differently than their HA names
if location in context.region_mapping:
location = context.region_mapping[location]
if location.lower() == 'home': if location.lower() == 'home':
location = STATE_HOME location = STATE_HOME
@ -398,7 +431,7 @@ def async_handle_waypoints_message(hass, context, message):
return return
if context.waypoint_whitelist is not None: if context.waypoint_whitelist is not None:
user = _parse_topic(message['topic'])[0] user = _parse_topic(message['topic'], context.mqtt_topic)[0]
if user not in context.waypoint_whitelist: if user not in context.waypoint_whitelist:
return return
@ -410,7 +443,7 @@ def async_handle_waypoints_message(hass, context, message):
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic']) _LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
name_base = ' '.join(_parse_topic(message['topic'])) name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic))
for wayp in wayps: for wayp in wayps:
yield from async_handle_waypoint(hass, name_base, wayp) yield from async_handle_waypoint(hass, name_base, wayp)

View file

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks_http/ https://home-assistant.io/components/device_tracker.owntracks_http/
""" """
import asyncio import asyncio
import re
from aiohttp.web_exceptions import HTTPInternalServerError from aiohttp.web_exceptions import HTTPInternalServerError
@ -43,8 +44,11 @@ class OwnTracksView(HomeAssistantView):
"""Handle an OwnTracks message.""" """Handle an OwnTracks message."""
hass = request.app['hass'] hass = request.app['hass']
subscription = self.context.mqtt_topic
topic = re.sub('/#$', '', subscription)
message = yield from request.json() message = yield from request.json()
message['topic'] = 'owntracks/{}/{}'.format(user, device) message['topic'] = '{}/{}/{}'.format(topic, user, device)
try: try:
yield from async_handle_message(hass, self.context, message) yield from async_handle_message(hass, self.context, message)

View file

@ -13,8 +13,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER) PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
from homeassistant.helpers.event import track_point_in_utc_time SOURCE_TYPE_ROUTER)
from homeassistant import util from homeassistant import util
from homeassistant import const from homeassistant import const
@ -70,16 +70,21 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Host objects and return the update function.""" """Set up the Host objects and return the update function."""
hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
config[const.CONF_HOSTS].items()] config[const.CONF_HOSTS].items()]
interval = timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + \ interval = config.get(CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL timedelta(seconds=len(hosts) *
_LOGGER.info("Started ping tracker with interval=%s on hosts: %s", config[CONF_PING_COUNT])
+ DEFAULT_SCAN_INTERVAL)
_LOGGER.debug("Started ping tracker with interval=%s on hosts: %s",
interval, ",".join([host.ip_address for host in hosts])) interval, ",".join([host.ip_address for host in hosts]))
def update(now): def update_interval(now):
"""Update all the hosts on every interval time.""" """Update all the hosts on every interval time."""
try:
for host in hosts: for host in hosts:
host.update(see) host.update(see)
track_point_in_utc_time(hass, update, util.dt.utcnow() + interval) finally:
return True hass.helpers.event.track_point_in_utc_time(
update_interval, util.dt.utcnow() + interval)
return update(util.dt.utcnow()) update_interval(None)
return True

View file

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.2'] REQUIREMENTS = ['pysnmp==4.4.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -19,7 +19,7 @@ from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==1.0.0'] REQUIREMENTS = ['pytile==1.1.0']
CLIENT_UUID_CONFIG_FILE = '.tile.conf' CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth' DEFAULT_ICON = 'mdi:bluetooth'
@ -29,14 +29,15 @@ ATTR_ALTITUDE = 'altitude'
ATTR_CONNECTION_STATE = 'connection_state' ATTR_CONNECTION_STATE = 'connection_state'
ATTR_IS_DEAD = 'is_dead' ATTR_IS_DEAD = 'is_dead'
ATTR_IS_LOST = 'is_lost' ATTR_IS_LOST = 'is_lost'
ATTR_LAST_SEEN = 'last_seen'
ATTR_LAST_UPDATED = 'last_updated'
ATTR_RING_STATE = 'ring_state' ATTR_RING_STATE = 'ring_state'
ATTR_VOIP_STATE = 'voip_state' ATTR_VOIP_STATE = 'voip_state'
CONF_SHOW_INACTIVE = 'show_inactive'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
vol.Optional(CONF_MONITORED_VARIABLES): vol.Optional(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
}) })
@ -79,6 +80,7 @@ class TileDeviceScanner(DeviceScanner):
_LOGGER.debug('Client UUID: %s', self._client.client_uuid) _LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid) _LOGGER.debug('User UUID: %s', self._client.user_uuid)
self._show_inactive = config.get(CONF_SHOW_INACTIVE)
self._types = config.get(CONF_MONITORED_VARIABLES) self._types = config.get(CONF_MONITORED_VARIABLES)
self.devices = {} self.devices = {}
@ -91,29 +93,25 @@ class TileDeviceScanner(DeviceScanner):
def _update_info(self, now=None) -> None: def _update_info(self, now=None) -> None:
"""Update the device info.""" """Update the device info."""
device_data = self._client.get_tiles(type_whitelist=self._types) self.devices = self._client.get_tiles(
type_whitelist=self._types, show_inactive=self._show_inactive)
try: if not self.devices:
self.devices = device_data['result']
except KeyError:
_LOGGER.warning('No Tiles found') _LOGGER.warning('No Tiles found')
_LOGGER.debug(device_data)
return return
for info in self.devices.values(): for dev in self.devices:
dev_id = 'tile_{0}'.format(slugify(info['name'])) dev_id = 'tile_{0}'.format(slugify(dev['name']))
lat = info['tileState']['latitude'] lat = dev['tileState']['latitude']
lon = info['tileState']['longitude'] lon = dev['tileState']['longitude']
attrs = { attrs = {
ATTR_ALTITUDE: info['tileState']['altitude'], ATTR_ALTITUDE: dev['tileState']['altitude'],
ATTR_CONNECTION_STATE: info['tileState']['connection_state'], ATTR_CONNECTION_STATE: dev['tileState']['connection_state'],
ATTR_IS_DEAD: info['is_dead'], ATTR_IS_DEAD: dev['is_dead'],
ATTR_IS_LOST: info['tileState']['is_lost'], ATTR_IS_LOST: dev['tileState']['is_lost'],
ATTR_LAST_SEEN: info['tileState']['timestamp'], ATTR_RING_STATE: dev['tileState']['ring_state'],
ATTR_LAST_UPDATED: device_data['timestamp_ms'], ATTR_VOIP_STATE: dev['tileState']['voip_state'],
ATTR_RING_STATE: info['tileState']['ring_state'],
ATTR_VOIP_STATE: info['tileState']['voip_state'],
} }
self.see( self.see(

0
homeassistant/components/device_tracker/tplink.py Executable file → Normal file
View file

View file

@ -9,7 +9,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent, template from homeassistant.helpers import intent, template
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -33,6 +33,10 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
class DialogFlowError(HomeAssistantError):
"""Raised when a DialogFlow error happens."""
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up Dialogflow component.""" """Set up Dialogflow component."""
@ -51,57 +55,71 @@ class DialogflowIntentsView(HomeAssistantView):
def post(self, request): def post(self, request):
"""Handle Dialogflow.""" """Handle Dialogflow."""
hass = request.app['hass'] hass = request.app['hass']
data = yield from request.json() message = yield from request.json()
_LOGGER.debug("Received Dialogflow request: %s", data) _LOGGER.debug("Received Dialogflow request: %s", message)
req = data.get('result') try:
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)
if req is None: except DialogFlowError as err:
_LOGGER.error("Received invalid data from Dialogflow: %s", data) _LOGGER.warning(str(err))
return self.json_message( return self.json(dialogflow_error_response(
"Expected result value not received", HTTP_BAD_REQUEST) hass, message, str(err)))
except intent.UnknownIntent as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message,
"This intent is not yet configured within Home Assistant."))
except intent.InvalidSlotInfo as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message,
"Invalid slot information received for this intent."))
except intent.IntentError as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message, "Error handling intent."))
def dialogflow_error_response(hass, message, error):
"""Return a response saying the error message."""
dialogflow_response = DialogflowResponse(message['result']['parameters'])
dialogflow_response.add_speech(error)
return dialogflow_response.as_dict()
@asyncio.coroutine
def async_handle_message(hass, message):
"""Handle a DialogFlow message."""
req = message.get('result')
action_incomplete = req['actionIncomplete'] action_incomplete = req['actionIncomplete']
if action_incomplete: if action_incomplete:
return None return None
action = req.get('action') action = req.get('action', '')
parameters = req.get('parameters') parameters = req.get('parameters')
dialogflow_response = DialogflowResponse(parameters) dialogflow_response = DialogflowResponse(parameters)
if action == "": if action == "":
_LOGGER.warning("Received intent with empty action") raise DialogFlowError(
dialogflow_response.add_speech(
"You have not defined an action in your Dialogflow intent.") "You have not defined an action in your Dialogflow intent.")
return self.json(dialogflow_response)
try:
intent_response = yield from intent.async_handle( intent_response = yield from intent.async_handle(
hass, DOMAIN, action, hass, DOMAIN, action,
{key: {'value': value} for key, value {key: {'value': value} for key, value
in parameters.items()}) in parameters.items()})
except intent.UnknownIntent as err:
_LOGGER.warning("Received unknown intent %s", action)
dialogflow_response.add_speech(
"This intent is not yet configured within Home Assistant.")
return self.json(dialogflow_response)
except intent.InvalidSlotInfo as err:
_LOGGER.error("Received invalid slot data: %s", err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception("Error handling request for %s", action)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
if 'plain' in intent_response.speech: if 'plain' in intent_response.speech:
dialogflow_response.add_speech( dialogflow_response.add_speech(
intent_response.speech['plain']['speech']) intent_response.speech['plain']['speech'])
return self.json(dialogflow_response) return dialogflow_response.as_dict()
class DialogflowResponse(object): class DialogflowResponse(object):

View file

@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.12'] REQUIREMENTS = ['python-digitalocean==1.13.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -44,13 +44,19 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Set up the Digital Ocean component.""" """Set up the Digital Ocean component."""
import digitalocean
conf = config[DOMAIN] conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN) access_token = conf.get(CONF_ACCESS_TOKEN)
digital = DigitalOcean(access_token) digital = DigitalOcean(access_token)
try:
if not digital.manager.get_account(): if not digital.manager.get_account():
_LOGGER.error("No Digital Ocean account found for the given API Token") _LOGGER.error("No account found for the given API token")
return False
except digitalocean.baseapi.DataReadError:
_LOGGER.error("API token not valid for authentication")
return False return False
hass.data[DATA_DIGITAL_OCEAN] = digital hass.data[DATA_DIGITAL_OCEAN] = digital

View file

@ -37,6 +37,8 @@ SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue' SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_HASS_IOS_APP: ('ios', None),
@ -50,6 +52,7 @@ SERVICE_HANDLERS = {
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None), SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),

View file

@ -1,40 +1,54 @@
"""Support for a DoorBird video doorbell.""" """
Support for DoorBird device.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/doorbird/
"""
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['DoorBirdPy==0.1.0'] REQUIREMENTS = ['DoorBirdPy==0.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'doorbird' DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SENSOR_DOORBELL = 'doorbell'
def setup(hass, config): def setup(hass, config):
"""Set up the DoorBird component.""" """Set up the DoorBird component."""
from doorbirdpy import DoorBird
device_ip = config[DOMAIN].get(CONF_HOST) device_ip = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME) username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD) password = config[DOMAIN].get(CONF_PASSWORD)
from doorbirdpy import DoorBird
device = DoorBird(device_ip, username, password) device = DoorBird(device_ip, username, password)
status = device.ready() status = device.ready()
if status[0]: if status[0]:
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username) _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
hass.data[DOMAIN] = device hass.data[DOMAIN] = device
return True
elif status[1] == 401: elif status[1] == 401:
_LOGGER.error("Authorization rejected by DoorBird at %s", device_ip) _LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
return False return False
@ -42,3 +56,31 @@ def setup(hass, config):
_LOGGER.error("Could not connect to DoorBird at %s: Error %s", _LOGGER.error("Could not connect to DoorBird at %s: Error %s",
device_ip, str(status[1])) device_ip, str(status[1]))
return False return False
if config[DOMAIN].get(CONF_DOORBELL_EVENTS):
# Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView())
# This will make HA the only service that gets doorbell events
url = '{}{}/{}'.format(
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url)
return True
class DoorbirdRequestView(HomeAssistantView):
"""Provide a page for the device to call."""
url = API_URL
name = API_URL[1:].replace('/', ':')
extra_urls = [API_URL + '/{sensor}']
# pylint: disable=no-self-use
@asyncio.coroutine
def get(self, request, sensor):
"""Respond to requests from the device."""
hass = request.app['hass']
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor))
return 'OK'

View file

@ -6,13 +6,11 @@ https://home-assistant.io/components/eight_sleep/
""" """
import asyncio import asyncio
import logging import logging
import os
from datetime import timedelta from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS, CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS,
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP)
@ -159,10 +157,6 @@ def async_setup(hass, config):
CONF_BINARY_SENSORS: binary_sensors, CONF_BINARY_SENSORS: binary_sensors,
}, config)) }, config))
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine @asyncio.coroutine
def async_service_handler(service): def async_service_handler(service):
"""Handle eight sleep service calls.""" """Handle eight sleep service calls."""
@ -183,7 +177,6 @@ def async_setup(hass, config):
# Register services # Register services
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_HEAT_SET, async_service_handler, DOMAIN, SERVICE_HEAT_SET, async_service_handler,
descriptions[DOMAIN].get(SERVICE_HEAT_SET),
schema=SERVICE_EIGHT_SCHEMA) schema=SERVICE_EIGHT_SCHEMA)
@asyncio.coroutine @asyncio.coroutine

View file

@ -8,12 +8,10 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.components import group from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
SERVICE_TURN_OFF, ATTR_ENTITY_ID, SERVICE_TURN_OFF, ATTR_ENTITY_ID,
STATE_UNKNOWN) STATE_UNKNOWN)
@ -225,16 +223,10 @@ def async_setup(hass, config: dict):
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)
# Listen for fan service calls.
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD: for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get('schema') schema = SERVICE_TO_METHOD[service_name].get('schema')
hass.services.async_register( hass.services.async_register(
DOMAIN, service_name, async_handle_fan_service, DOMAIN, service_name, async_handle_fan_service, schema=schema)
descriptions.get(service_name), schema=schema)
return True return True

View file

@ -5,7 +5,6 @@ https://home-assistant.io/components/fan.dyson/
""" """
import logging import logging
import asyncio import asyncio
from os import path
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE,
@ -13,7 +12,6 @@ from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE,
DOMAIN) DOMAIN)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.components.dyson import DYSON_DEVICES
from homeassistant.config import load_yaml_config_file
DEPENDENCIES = ['dyson'] DEPENDENCIES = ['dyson']
@ -44,9 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(hass.data[DYSON_FAN_DEVICES]) add_devices(hass.data[DYSON_FAN_DEVICES])
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def service_handle(service): def service_handle(service):
"""Handle dyson services.""" """Handle dyson services."""
entity_id = service.data.get('entity_id') entity_id = service.data.get('entity_id')
@ -64,7 +59,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# Register dyson service(s) # Register dyson service(s)
hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE, hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE,
service_handle, service_handle,
descriptions.get(SERVICE_SET_NIGHT_MODE),
schema=DYSON_SET_NIGHT_MODE_SCHEMA) schema=DYSON_SET_NIGHT_MODE_SCHEMA)

Some files were not shown because too many files have changed in this diff Show more