diff --git a/.coveragerc b/.coveragerc index 96936655c51..a264bde79a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,9 +50,15 @@ omit = 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/deconz/* + homeassistant/components/*/deconz.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/daikin.py + homeassistant/components/*/daikin.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py @@ -296,6 +305,7 @@ omit = homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py homeassistant/components/camera/yi.py + homeassistant/components/climate/econet.py homeassistant/components/climate/ephember.py homeassistant/components/climate/eq3btsmart.py homeassistant/components/climate/flexit.py @@ -307,6 +317,7 @@ omit = homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py homeassistant/components/climate/sensibo.py + homeassistant/components/climate/touchline.py homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/knx.py @@ -365,8 +376,10 @@ omit = homeassistant/components/light/decora.py homeassistant/components/light/decora_wifi.py homeassistant/components/light/flux_led.py + homeassistant/components/light/greenwave.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py + homeassistant/components/light/iglo.py homeassistant/components/light/lifx.py homeassistant/components/light/lifx_legacy.py homeassistant/components/light/limitlessled.py @@ -476,6 +489,7 @@ omit = homeassistant/components/notify/yessssms.py homeassistant/components/nuimo_controller.py homeassistant/components/prometheus.py + homeassistant/components/rainbird.py homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py @@ -504,6 +518,7 @@ omit = homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py + homeassistant/components/sensor/discogs.py homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dte_energy_bridge.py @@ -517,7 +532,6 @@ omit = homeassistant/components/sensor/etherscan.py homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fedex.py - homeassistant/components/sensor/fido.py homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fritzbox_callmonitor.py @@ -532,7 +546,6 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py - homeassistant/components/sensor/hydroquebec.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/influxdb.py @@ -570,6 +583,7 @@ omit = homeassistant/components/sensor/pyload.py homeassistant/components/sensor/qnap.py homeassistant/components/sensor/radarr.py + homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/ripple.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py @@ -580,6 +594,7 @@ omit = homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/sma.py homeassistant/components/sensor/snmp.py + homeassistant/components/sensor/sochain.py homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/steam_online.py @@ -648,9 +663,9 @@ omit = homeassistant/components/vacuum/xiaomi_miio.py homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py + homeassistant/components/weather/darksky.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py - homeassistant/components/weather/yweather.py homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py homeassistant/components/zwave/util.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dd030c73d1a..43e1c399671 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,6 +11,7 @@ ``` ## Checklist: + - [ ] The code change is tested and works locally. 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) diff --git a/.gitignore b/.gitignore index e01de1b49b8..c8a6fed2ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ pip-selfcheck.json venv .venv Pipfile* +share/* # vimmy stuff *.swp diff --git a/CODEOWNERS b/CODEOWNERS index ac0f794482a..99c103b1298 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -53,10 +53,11 @@ homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth +homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 -homeassistant/components/sensor/miflora.py @danielhiversen +homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen 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/xiaomi_aqara.py @danielhiversen @syssi +homeassistant/components/*/axis.py @kane610 homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline +homeassistant/components/*/deconz.py @kane610 homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342 diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index b5ac57080d1..6db147a5f59 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -10,7 +10,6 @@ Component design guidelines: import asyncio import itertools as it import logging -import os import homeassistant.core as ha import homeassistant.config as conf_util @@ -111,11 +110,6 @@ def async_reload_core_config(hass): @asyncio.coroutine def async_setup(hass, config): """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 def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" @@ -155,14 +149,11 @@ def async_setup(hass, config): yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, - descriptions[ha.DOMAIN][SERVICE_TURN_OFF]) + ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, - descriptions[ha.DOMAIN][SERVICE_TURN_ON]) + ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) hass.services.async_register( - ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, - descriptions[ha.DOMAIN][SERVICE_TOGGLE]) + ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) @asyncio.coroutine 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.services.async_register( - ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service, - descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP]) + ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) hass.services.async_register( - ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service, - descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART]) + ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) hass.services.async_register( - ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service, - descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG]) + ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) @asyncio.coroutine def async_handle_reload_config(call): @@ -209,7 +197,6 @@ def async_setup(hass, config): hass, conf.get(ha.DOMAIN) or {}) hass.services.async_register( - ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config, - descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG]) + ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config) return True diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index b4c6adcc887..cbfee2ae215 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -7,11 +7,9 @@ https://home-assistant.io/components/abode/ import asyncio import logging from functools import partial -from os import path import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS, @@ -188,22 +186,16 @@ def setup_hass_services(hass): for device in target_devices: device.trigger() - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml'))[DOMAIN] - hass.services.register( DOMAIN, SERVICE_SETTINGS, change_setting, - descriptions.get(SERVICE_SETTINGS), schema=CHANGE_SETTING_SCHEMA) hass.services.register( DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, - descriptions.get(SERVICE_CAPTURE_IMAGE), schema=CAPTURE_IMAGE_SCHEMA) hass.services.register( DOMAIN, SERVICE_TRIGGER, trigger_quick_action, - descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SCHEMA) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 3d9de28ded3..20a4489da90 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation. https://home-assistant.io/components/ads/ """ -import os import threading import struct import logging @@ -14,7 +13,6 @@ from collections import namedtuple import voluptuous as vol from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ EVENT_HOMEASSISTANT_STOP -from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyads==2.2.6'] @@ -107,13 +105,8 @@ def setup(hass, config): except pyads.ADSError as 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( DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name, - descriptions[SERVICE_WRITE_DATA_BY_NAME], schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME ) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index f6fd3f3bea9..25e303cbe85 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/alarm_control_panel/ import asyncio from datetime import timedelta import logging -import os import voluptuous as vol @@ -15,7 +14,6 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, 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.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv @@ -148,14 +146,10 @@ def async_setup(hass, config): if update_tasks: 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: hass.services.async_register( DOMAIN, service, async_alarm_service_handler, - descriptions.get(service), schema=ALARM_SERVICE_SCHEMA) + schema=ALARM_SERVICE_SCHEMA) return True diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index d5fbbec5998..7126aa6f703 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -7,23 +7,39 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/ import asyncio import logging +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm +import homeassistant.helpers.config_validation as cv from homeassistant.components.alarmdecoder import ( DATA_AD, SIGNAL_PANEL_MESSAGE) from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) _LOGGER = logging.getLogger(__name__) 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): """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): @@ -34,6 +50,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): self._display = "" self._name = "Alarm Panel" 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 def async_added_to_hass(self): @@ -43,21 +68,25 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): def _message_callback(self, message): if message.alarm_sounding or message.fire_alarm: - if self._state != STATE_ALARM_TRIGGERED: - self._state = STATE_ALARM_TRIGGERED - self.schedule_update_ha_state() + self._state = STATE_ALARM_TRIGGERED elif message.armed_away: - if self._state != STATE_ALARM_ARMED_AWAY: - self._state = STATE_ALARM_ARMED_AWAY - self.schedule_update_ha_state() + self._state = STATE_ALARM_ARMED_AWAY elif message.armed_home: - if self._state != STATE_ALARM_ARMED_HOME: - self._state = STATE_ALARM_ARMED_HOME - self.schedule_update_ha_state() + self._state = STATE_ALARM_ARMED_HOME else: - if self._state != STATE_ALARM_DISARMED: - self._state = STATE_ALARM_DISARMED - self.schedule_update_ha_state() + 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() @property def name(self): @@ -79,20 +108,37 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" 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): """Send disarm command.""" if code: - _LOGGER.debug("alarm_disarm: sending %s1", str(code)) self.hass.data[DATA_AD].send("{!s}1".format(code)) def alarm_arm_away(self, code=None): """Send arm away command.""" if code: - _LOGGER.debug("alarm_arm_away: sending %s2", str(code)) self.hass.data[DATA_AD].send("{!s}2".format(code)) def alarm_arm_home(self, code=None): """Send arm home command.""" if code: - _LOGGER.debug("alarm_arm_home: sending %s3", str(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)) diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py old mode 100755 new mode 100644 index 291d4bc80b5..af91bc78e67 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['concord232==0.14'] +REQUIREMENTS = ['concord232==0.15'] _LOGGER = logging.getLogger(__name__) @@ -121,4 +121,4 @@ class Concord232Alarm(alarm.AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" - self._alarm.arm('auto') + self._alarm.arm('away') diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 82c26c98104..cb3da95e03a 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -16,9 +16,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, 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__) @@ -26,13 +26,15 @@ CONF_REPORT_SERVER_CODES = 'report_server_codes' CONF_REPORT_SERVER_ENABLED = 'report_server_enabled' CONF_REPORT_SERVER_PORT = 'report_server_port' CONF_REPORT_SERVER_CODES_IGNORE = 'ignore' +CONF_VERSION = 'version' DEFAULT_NAME = 'Egardia' DEFAULT_PORT = 80 DEFAULT_REPORT_SERVER_ENABLED = False DEFAULT_REPORT_SERVER_PORT = 52010 +DEFAULT_VERSION = 'GATE-01' DOMAIN = 'egardia' - +D_EGARDIASRV = 'egardiaserver' NOTIFICATION_ID = 'egardia_notification' NOTIFICATION_TITLE = 'Egardia' @@ -49,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): 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_PORT, default=DEFAULT_PORT): cv.port, 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): """Set up the Egardia platform.""" from pythonegardia import egardiadevice + from pythonegardia import egardiaserver name = config.get(CONF_NAME) 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_port = config.get(CONF_REPORT_SERVER_PORT) rs_codes = config.get(CONF_REPORT_SERVER_CODES) + version = config.get(CONF_VERSION) try: egardiasystem = egardiadevice.EgardiaDevice( - host, port, username, password, '') + host, port, username, password, '', version) except requests.exceptions.RequestException: raise exc.PlatformNotReady() except egardiadevice.UnauthorizedError: _LOGGER.error("Unable to authorize. Wrong password or username") - return False + return - add_devices([EgardiaAlarm( - name, egardiasystem, hass, rs_enabled, rs_port, rs_codes)], True) + eg_dev = EgardiaAlarm( + 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): """Representation of a Egardia alarm.""" - def __init__(self, name, egardiasystem, hass, rs_enabled=False, - rs_port=None, rs_codes=None): + def __init__(self, name, egardiasystem, + rs_enabled=False, rs_codes=None): """Initialize object.""" self._name = name self._egardiasystem = egardiasystem - self._status = STATE_UNKNOWN + self._status = None self._rs_enabled = rs_enabled - self._rs_port = rs_port - self._hass = hass - if rs_codes is not None: self._rs_codes = rs_codes[0] else: self._rs_codes = rs_codes - if self._rs_enabled: - self.listen_to_system_status() - @property def name(self): """Return the name of the device.""" @@ -123,19 +148,14 @@ class EgardiaAlarm(alarm.AlarmControlPanel): return True return False - def handle_system_status_event(self, event): + def handle_status_event(self, event): """Handle egardia_system_status_event.""" - if event.data.get('status') is not None: - statuscode = event.data.get('status') + statuscode = event.get('status') + if statuscode is not None: status = self.lookupstatusfromcode(statuscode) self.parsestatus(status) 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): """Look at the rs_codes and returns the status from the code.""" status = 'UNKNOWN' diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index 026d2324ed3..e5003f1ba1d 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/alarm_control_panel.envisalink/ """ import asyncio import logging -import os import voluptuous as vol @@ -14,7 +13,6 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.components.alarm_control_panel as alarm import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.components.envisalink import ( DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, 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: 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( alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, - descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA) + schema=ALARM_KEYPRESS_SCHEMA) return True diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index fca935388c1..a4559160e3b 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -17,7 +17,9 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, CONF_NAME, CONF_CODE) 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 _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_ARM_HOME), 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.""" 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.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -73,11 +81,11 @@ class MqttAlarm(alarm.AlarmControlPanel): self._payload_arm_away = payload_arm_away self._code = code + @asyncio.coroutine 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 def message_received(topic, payload, qos): """Run when new MQTT message has been received.""" @@ -89,7 +97,7 @@ class MqttAlarm(alarm.AlarmControlPanel): self._state = payload self.async_schedule_update_ha_state() - return mqtt.async_subscribe( + yield from mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @property diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 21378876d9b..bfd38c902d0 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -59,3 +59,13 @@ envisalink_alarm_keypress: keypress: description: 'String to send to the alarm panel (1-6 characters).' 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 diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py index 6e30a83d96a..bc7f1910803 100644 --- a/homeassistant/components/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder.py @@ -6,13 +6,16 @@ https://home-assistant.io/components/alarmdecoder/ """ import logging +from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP 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__) @@ -29,6 +32,7 @@ CONF_DEVICE_TYPE = 'type' CONF_PANEL_DISPLAY = 'panel_display' CONF_ZONE_NAME = 'name' CONF_ZONE_TYPE = 'type' +CONF_ZONE_RFID = 'rfid' CONF_ZONES = 'zones' DEFAULT_DEVICE_TYPE = 'socket' @@ -48,6 +52,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' +SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message' DEVICE_SOCKET_SCHEMA = vol.Schema({ vol.Required(CONF_DEVICE_TYPE): 'socket', @@ -64,7 +69,9 @@ DEVICE_USB_SCHEMA = vol.Schema({ ZONE_SCHEMA = vol.Schema({ 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({ DOMAIN: vol.Schema({ @@ -85,6 +92,7 @@ def setup(hass, config): conf = config.get(DOMAIN) + restart = False device = conf.get(CONF_DEVICE) display = conf.get(CONF_PANEL_DISPLAY) zones = conf.get(CONF_ZONES) @@ -98,13 +106,43 @@ def setup(hass, config): def stop_alarmdecoder(event): """Handle the shutdown of AlarmDecoder.""" _LOGGER.debug("Shutting down alarmdecoder") + nonlocal restart + restart = False 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): """Handle message from AlarmDecoder.""" hass.helpers.dispatcher.dispatcher_send( 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): """Handle zone fault from AlarmDecoder.""" hass.helpers.dispatcher.dispatcher_send( @@ -129,14 +167,15 @@ def setup(hass, config): return False controller.on_message += handle_message + controller.on_rfx_message += handle_rfx_message controller.on_zone_fault += zone_fault_callback controller.on_zone_restore += zone_restore_callback + controller.on_close += handle_closed_connection 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) load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 6356f429bed..27d1625fd6b 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -7,12 +7,10 @@ https://home-assistant.io/components/alert/ import asyncio from datetime import datetime, timedelta import logging -import os import voluptuous as vol from homeassistant.core import callback -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, 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]) 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 hass.services.async_register( 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( 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( 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()] if tasks: diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 3ade199aabb..8283b563591 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -9,15 +9,16 @@ import asyncio import enum import logging +from homeassistant.exceptions import HomeAssistantError from homeassistant.core import callback -from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.helpers import intent from homeassistant.components import http +from homeassistant.util.decorator import Registry from .const import DOMAIN, SYN_RESOLUTION_MATCH INTENTS_API_ENDPOINT = '/api/alexa' - +HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) @@ -47,6 +48,10 @@ def async_setup(hass): hass.http.register_view(AlexaIntentsView) +class UnknownRequest(HomeAssistantError): + """When an unknown Alexa request is passed in.""" + + class AlexaIntentsView(http.HomeAssistantView): """Handle Alexa requests.""" @@ -57,71 +62,112 @@ class AlexaIntentsView(http.HomeAssistantView): def post(self, request): """Handle Alexa.""" hass = request.app['hass'] - data = yield from request.json() + message = yield from request.json() - _LOGGER.debug('Received Alexa request: %s', data) - - req = data.get('request') - - if req is None: - _LOGGER.error('Received invalid data from Alexa: %s', data) - return self.json_message('Expected request value not received', - HTTP_BAD_REQUEST) - - req_type = req['type'] - - if req_type == 'SessionEndedRequest': - return None - - alexa_intent_info = req.get('intent') - alexa_response = AlexaResponse(hass, alexa_intent_info) - - if req_type != 'IntentRequest' and req_type != 'LaunchRequest': - _LOGGER.warning('Received unsupported request: %s', req_type) - 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('applicationId') - else: - intent_name = alexa_intent_info['name'] + _LOGGER.debug('Received Alexa request: %s', message) try: - intent_response = yield from intent.async_handle( - hass, DOMAIN, intent_name, - {key: {'value': value} for key, value - in alexa_response.variables.items()}) + 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))) + 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) + _LOGGER.warning(str(err)) + return self.json(intent_error_response( + 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_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) + return self.json(intent_error_response( + hass, message, + "Invalid slot information received for this intent.")) - for intent_speech, alexa_speech in SPEECH_MAPPINGS.items(): - if intent_speech in intent_response.speech: - alexa_response.add_speech( - alexa_speech, - intent_response.speech[intent_speech]['speech']) - break + except intent.IntentError as err: + _LOGGER.exception(str(err)) + return self.json(intent_error_response( + hass, message, "Error handling intent.")) - if 'simple' in intent_response.card: - alexa_response.add_card( - CardType.simple, intent_response.card['simple']['title'], - intent_response.card['simple']['content']) - return self.json(alexa_response) +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'] + + 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 + + +@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_response = AlexaResponse(hass, alexa_intent_info) + + if req['type'] == 'LaunchRequest': + intent_name = message.get('session', {}) \ + .get('application', {}) \ + .get('applicationId') + else: + intent_name = alexa_intent_info['name'] + + intent_response = yield from intent.async_handle( + hass, DOMAIN, intent_name, + {key: {'value': value} for key, value + in alexa_response.variables.items()}) + + for intent_speech, alexa_speech in SPEECH_MAPPINGS.items(): + if intent_speech in intent_response.speech: + alexa_response.add_speech( + alexa_speech, + intent_response.speech[intent_speech]['speech']) + break + + if 'simple' in intent_response.card: + alexa_response.add_card( + CardType.simple, intent_response.card['simple']['title'], + intent_response.card['simple']['content']) + + return alexa_response.as_dict() def resolve_slot_synonyms(key, request): diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 58888b19af7..3c14826037c 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,6 +1,5 @@ """Support for alexa Smart Home Skill API.""" import asyncio -from collections import namedtuple import logging import math from uuid import uuid4 @@ -27,10 +26,9 @@ API_EVENT = 'event' API_HEADER = 'header' API_PAYLOAD = 'payload' -ATTR_ALEXA_DESCRIPTION = 'alexa_description' -ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories' -ATTR_ALEXA_HIDDEN = 'alexa_hidden' -ATTR_ALEXA_NAME = 'alexa_name' +CONF_DESCRIPTION = 'description' +CONF_DISPLAY_CATEGORIES = 'display_categories' +CONF_NAME = 'name' 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 @@ -150,32 +154,28 @@ def async_api_discovery(hass, config, request): discovery_endpoints = [] 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", entity.entity_id) 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) if not class_data: continue - friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name) - description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION, - entity.entity_id) + entity_conf = config.entity_config.get(entity.entity_id, {}) + + friendly_name = entity_conf.get(CONF_NAME, entity.name) + description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id) # Required description as per Amazon Scene docs if entity.domain == scene.DOMAIN: scene_fmt = '{} (Scene connected via Home Assistant)' description = scene_fmt.format(description) - cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES - display_categories = entity.attributes.get(cat_key, class_data[0]) + display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES, + class_data[0]) endpoint = { 'displayCategories': [display_categories], @@ -243,7 +243,11 @@ def async_api_turn_on(hass, config, request, entity): if entity.domain == group.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 }, blocking=False) @@ -259,7 +263,11 @@ def async_api_turn_off(hass, config, request, entity): if entity.domain == group.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 }, blocking=False) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ecdc31c8bd7..f25b0cc130c 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -24,6 +24,7 @@ from homeassistant.const import ( __version__) from homeassistant.exceptions import TemplateError from homeassistant.helpers.state import AsyncTrackStates +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers import template from homeassistant.components.http import HomeAssistantView @@ -293,10 +294,11 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" - @ha.callback + @asyncio.coroutine def get(self, request): """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): @@ -355,10 +357,12 @@ class APITemplateView(HomeAssistantView): HTTP_BAD_REQUEST) +@asyncio.coroutine def async_services_json(hass): """Generate services data to JSONify.""" + descriptions = yield from async_get_all_descriptions(hass) 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): diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index bb6bfa0e9db..beacb3840ef 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -4,7 +4,6 @@ Support for Apple TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/apple_tv/ """ -import os import asyncio import logging @@ -12,7 +11,6 @@ import voluptuous as vol from typing import Union, TypeVar, Sequence 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 import discovery from homeassistant.components.discovery import SERVICE_APPLE_TV @@ -183,18 +181,12 @@ def async_setup(hass, config): if tasks: 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( DOMAIN, SERVICE_SCAN, async_service_handler, - descriptions.get(SERVICE_SCAN), schema=APPLE_TV_SCAN_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, - descriptions.get(SERVICE_AUTHENTICATE), schema=APPLE_TV_AUTHENTICATE_SCHEMA) return True diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index a78b334de0b..a928ed108c9 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -REQUIREMENTS = ['pyarlo==0.1.0'] +REQUIREMENTS = ['pyarlo==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 58c86ff0c6d..bc3c17e41da 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -7,14 +7,12 @@ https://home-assistant.io/components/automation/ import asyncio from functools import partial import logging -import os import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import CoreState from homeassistant.loader import bind_hass -from homeassistant import config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, 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) - descriptions = yield from hass.async_add_job( - conf_util.load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml') - ) - @asyncio.coroutine def trigger_service_handler(service_call): """Handle automation triggers.""" @@ -216,20 +209,20 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_TRIGGER, trigger_service_handler, - descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA) + schema=TRIGGER_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, - descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA) + schema=RELOAD_SERVICE_SCHEMA) hass.services.async_register( 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): hass.services.async_register( DOMAIN, service, turn_onoff_service_handler, - descriptions.get(service), schema=SERVICE_SCHEMA) + schema=SERVICE_SCHEMA) return True diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index e4d096d35fd..9243f960850 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -55,7 +55,7 @@ def async_trigger(hass, config, action): # 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 - from_s.last_changed == to_s.last_changed): + from_s.state == to_s.state): return if not time_delta: diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py index a7c820f23c7..23dbe052d1c 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -6,12 +6,10 @@ https://home-assistant.io/components/axis/ """ import logging -import os import voluptuous as vol from homeassistant.components.discovery import SERVICE_AXIS -from homeassistant.config import load_yaml_config_file from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -195,10 +193,6 @@ def setup(hass, config): if not setup_device(hass, config, device_config): _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): """Service to send a message.""" for _, device in AXIS_DEVICES.items(): @@ -216,7 +210,6 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_VAPIX_CALL, vapix_service, - descriptions[DOMAIN][SERVICE_VAPIX_CALL], schema=SERVICE_SCHEMA) return True diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index a0c141914ed..df271a7ebac 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -21,24 +21,27 @@ SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + '.{}' DEVICE_CLASSES = [ 'battery', # On means low, Off means normal - 'cold', # On means cold (or too cold) - 'connectivity', # On means connection present, Off = no connection - 'gas', # CO, CO2, etc. - 'heat', # On means hot (or too hot) - 'light', # Lightness threshold - 'moisture', # Specifically a wetness sensor - 'motion', # Motion sensor - 'moving', # On means moving, Off means stopped - 'occupancy', # On means occupied, Off means not occupied - 'opening', # Door, window, etc. + 'cold', # On means cold, Off means normal + 'connectivity', # On means connected, Off means disconnected + 'door', # On means open, Off means closed + 'garage_door', # On means open, Off means closed + 'gas', # On means gas detected, Off means no gas (clear) + 'heat', # On means hot, Off means normal + 'light', # On means light detected, Off means no light + 'moisture', # On means wet, Off means dry + 'motion', # On means motion detected, Off means no motion (clear) + '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 - 'power', # Power, over-current, etc + 'power', # On means power detected, Off means no power 'presence', # On means home, Off means away - 'problem', # On means there is a problem, Off means the status is OK - 'safety', # Generic on=unsafe, off=safe - 'smoke', # Smoke detector - 'sound', # On means sound detected, Off means no sound + 'problem', # On means problem detected, Off means no problem (OK) + 'safety', # On means unsafe, Off means safe + 'smoke', # On means smoke detected, Off means no smoke (clear) + 'sound', # On means sound detected, Off means no sound (clear) '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)) diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index f42d0de4bb0..f0c8ec2d97c 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -10,12 +10,22 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.alarmdecoder import ( 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'] _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): """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]) zone_type = device_config_data[CONF_ZONE_TYPE] 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) add_devices(devices) @@ -37,13 +49,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class AlarmDecoderBinarySensor(BinarySensorDevice): """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.""" self._zone_number = zone_number self._zone_type = zone_type - self._state = 0 + self._state = None self._name = zone_name - self._type = zone_type + self._rfid = zone_rfid + self._rfstate = None @asyncio.coroutine def async_added_to_hass(self): @@ -54,27 +67,34 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_ZONE_RESTORE, self._restore_callback) + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RFX_MESSAGE, self._rfx_message_callback) + @property def name(self): """Return the name of the entity.""" 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 def should_poll(self): """No polling needed.""" 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 def is_on(self): """Return true if sensor is on.""" @@ -96,3 +116,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): if zone is None or int(zone) == self._zone_number: self._state = 0 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() diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py old mode 100755 new mode 100644 index 73cf77f2b93..c8442491b29 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import (CONF_HOST, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['concord232==0.14'] +REQUIREMENTS = ['concord232==0.15'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py new file mode 100644 index 00000000000..97f78ff21d0 --- /dev/null +++ b/homeassistant/components/binary_sensor/deconz.py @@ -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 diff --git a/homeassistant/components/binary_sensor/doorbird.py b/homeassistant/components/binary_sensor/doorbird.py deleted file mode 100644 index 9a13687fc54..00000000000 --- a/homeassistant/components/binary_sensor/doorbird.py +++ /dev/null @@ -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() diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index e3b5d7b4369..89d9b7e5c8f 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -12,7 +12,8 @@ from typing import Callable # noqa from homeassistant.core import callback 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.helpers.typing import ConfigType 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__) -UOM = ['2', '78'] -STATES = [STATE_OFF, STATE_ON, 'true', 'false'] - ISY_DEVICE_TYPES = { 'moisture': ['16.8', '16.13', '16.14'], '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, add_devices: Callable[[list], None], discovery_info=None): """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_by_nid = {} child_nodes = [] - for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM, - states=STATES): + for node in hass.data[ISY994_NODES][DOMAIN]: if node.parent_node is None: device = ISYBinarySensorDevice(node) devices.append(device) @@ -87,13 +80,8 @@ def setup_platform(hass, config: ConfigType, device = ISYBinarySensorDevice(node) devices.append(device) - for program in isy.PROGRAMS.get(DOMAIN, []): - try: - status = program[isy.KEY_STATUS] - except (KeyError, AssertionError): - pass - else: - devices.append(ISYBinarySensorProgram(program.name, status)) + for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]: + devices.append(ISYBinarySensorProgram(name, status)) add_devices(devices) @@ -118,7 +106,7 @@ def _is_val_unknown(val): return val == -1*float('inf') -class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice): +class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): """Representation of an ISY994 binary sensor device. 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 -class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice): +class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): """Representation of the battery state of an ISY994 sensor.""" def __init__(self, node, parent_device) -> None: @@ -361,7 +349,7 @@ class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice): return attr -class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice): +class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice): """Representation of an ISY994 binary sensor program. This does not need all of the subnode logic in the device version of binary diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index 406f60f99bb..9e5ddf5cac4 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -129,6 +129,11 @@ class KNXBinarySensor(BinarySensorDevice): """Return the name of the KNX device.""" return self.device.name + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + @property def should_poll(self): """No polling needed within KNX.""" diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index c5fba72bde0..983c879338d 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -17,19 +17,15 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_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 _LOGGER = logging.getLogger(__name__) -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' - DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' -DEFAULT_PAYLOAD_AVAILABLE = 'online' -DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' 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_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_PAYLOAD_AVAILABLE, - default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @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.""" def __init__(self, name, state_topic, availability_topic, device_class, qos, payload_on, payload_off, payload_available, payload_not_available, value_template): """Initialize the MQTT binary sensor.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._state = None 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._payload_on = payload_on self._payload_off = payload_off - self._payload_available = payload_available - self._payload_not_available = payload_not_available self._qos = qos self._template = value_template + @asyncio.coroutine 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 def state_message_received(topic, payload, qos): """Handle a new received MQTT state message.""" @@ -111,21 +100,6 @@ class MqttBinarySensor(BinarySensorDevice): yield from mqtt.async_subscribe( 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 def should_poll(self): """Return the polling state.""" @@ -136,11 +110,6 @@ class MqttBinarySensor(BinarySensorDevice): """Return the name of the binary sensor.""" return self._name - @property - def available(self) -> bool: - """Return if the binary sensor is available.""" - return self._available - @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 1f8d0ebe2f7..e9cb40f6747 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -98,6 +98,11 @@ class RestBinarySensor(BinarySensorDevice): """Return the class of this sensor.""" return self._device_class + @property + def available(self): + """Return the availability of this sensor.""" + return self.rest.data is not None + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index edaee574232..4073cb9eac1 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -7,30 +7,40 @@ tested. Other types may need some work. """ import logging + 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.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 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"] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required("platform"): rfxtrx.DOMAIN, - vol.Optional(CONF_DEVICES, default={}): vol.All( - dict, rfxtrx.valid_binary_sensor), - vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + 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) @@ -46,17 +56,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if device_id in rfxtrx.RFX_DEVICES: continue - if entity[ATTR_DATA_BITS] is not None: - _LOGGER.info("Masked device id: %s", - rfxtrx.get_pt2262_deviceid(device_id, - entity[ATTR_DATA_BITS])) + if entity[CONF_DATA_BITS] is not None: + _LOGGER.debug("Masked device id: %s", + rfxtrx.get_pt2262_deviceid(device_id, + entity[ATTR_DATA_BITS])) - _LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)", - entity[ATTR_NAME], entity[CONF_DEVICE_CLASS]) + _LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)", + entity[ATTR_NAME], entity[CONF_DEVICE_CLASS]) device = RfxtrxBinarySensor(event, entity[ATTR_NAME], entity[CONF_DEVICE_CLASS], - entity[ATTR_FIREEVENT], + entity[ATTR_FIRE_EVENT], entity[ATTR_OFF_DELAY], entity[ATTR_DATA_BITS], entity[CONF_COMMAND_ON], @@ -82,15 +92,15 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if sensor is None: # Add the entity if not exists and automatic_add is True - if not config[ATTR_AUTOMATIC_ADD]: + if not config[CONF_AUTOMATIC_ADD]: return if event.device.packettype == 0x13: poss_dev = rfxtrx.find_possible_pt2262_device(device_id) if poss_dev is not None: poss_id = slugify(poss_dev.event.device.id_string.lower()) - _LOGGER.info("Found possible matching deviceid %s.", - poss_id) + _LOGGER.debug("Found possible matching deviceid %s.", + poss_id) pkt_id = "".join("{0:02x}".format(x) for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) @@ -107,11 +117,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): elif not isinstance(sensor, RfxtrxBinarySensor): return else: - _LOGGER.info("Binary sensor update " - "(Device_id: %s Class: %s Sub: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype) + _LOGGER.debug("Binary sensor update " + "(Device_id: %s Class: %s Sub: %s)", + slugify(event.device.id_string.lower()), + event.device.__class__.__name__, + event.device.subtype) if sensor.is_lighting4: if sensor.data_bits is not None: @@ -163,10 +173,8 @@ class RfxtrxBinarySensor(BinarySensorDevice): self._masked_id = rfxtrx.get_pt2262_deviceid( event.device.id_string.lower(), data_bits) - - def __str__(self): - """Return the name of the sensor.""" - return self._name + else: + self._masked_id = None @property def name(self): diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 16167a93b82..92213a9b590 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -38,6 +38,11 @@ SENSOR_SCHEMA = vol.Schema({ vol.All(cv.time_period, cv.positive_timedelta), }) +SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + SENSOR_SCHEMA, +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 5ca037767f2..36e8868661d 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -9,40 +9,48 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.const import ( - CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, - ATTR_ENTITY_ID, CONF_DEVICE_CLASS) + ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME, + STATE_UNKNOWN) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) ATTR_HYSTERESIS = 'hysteresis' +ATTR_LOWER = 'lower' +ATTR_POSITION = 'position' ATTR_SENSOR_VALUE = 'sensor_value' -ATTR_THRESHOLD = 'threshold' ATTR_TYPE = 'type' +ATTR_UPPER = 'upper' CONF_HYSTERESIS = 'hysteresis' CONF_LOWER = 'lower' -CONF_THRESHOLD = 'threshold' CONF_UPPER = 'upper' DEFAULT_NAME = 'Threshold' 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({ 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_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.""" entity_id = config.get(CONF_ENTITY_ID) 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) - limit_type = config.get(CONF_TYPE) device_class = config.get(CONF_DEVICE_CLASS) async_add_devices([ThresholdSensor( - hass, entity_id, name, threshold, - hysteresis, limit_type, device_class) - ], True) - - return True + hass, entity_id, name, lower, upper, hysteresis, device_class)], True) class ThresholdSensor(BinarySensorDevice): """Representation of a Threshold sensor.""" - def __init__(self, hass, entity_id, name, threshold, - hysteresis, limit_type, device_class): + def __init__(self, hass, entity_id, name, lower, upper, hysteresis, + device_class): """Initialize the Threshold sensor.""" self._hass = hass self._entity_id = entity_id - self.is_upper = limit_type == 'upper' self._name = name - self._threshold = threshold + self._threshold_lower = lower + self._threshold_upper = upper self._hysteresis = hysteresis 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 + @callback def async_threshold_sensor_state_listener( entity, old_state, new_state): """Handle sensor state changes.""" - if new_state.state == STATE_UNKNOWN: - return - try: - self.sensor_value = float(new_state.state) - except ValueError: - _LOGGER.error("State is not numerical") + self.sensor_value = None if new_state.state == STATE_UNKNOWN \ + else float(new_state.state) + except (ValueError, TypeError): + self.sensor_value = None + _LOGGER.warning("State is not numerical") 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 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 def device_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_ENTITY_ID: self._entity_id, - ATTR_SENSOR_VALUE: self.sensor_value, - ATTR_THRESHOLD: self._threshold, 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 def async_update(self): """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 - elif self.sensor_value > (self._threshold + self._hysteresis): - self._state = self.is_upper - elif self.sensor_value < (self._threshold - self._hysteresis): - self._state = not self.is_upper + + elif self.threshold_type == TYPE_LOWER: + if below(self._threshold_lower): + 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 diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 8b5660f54c5..031e0aa42e5 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -11,21 +11,19 @@ import math 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 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.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.13.3'] +REQUIREMENTS = ['numpy==1.14.0'] _LOGGER = logging.getLogger(__name__) @@ -36,21 +34,21 @@ ATTR_INVERT = 'invert' ATTR_SAMPLE_DURATION = 'sample_duration' ATTR_SAMPLE_COUNT = 'sample_count' -CONF_SENSORS = 'sensors' CONF_ATTRIBUTE = 'attribute' +CONF_INVERT = 'invert' CONF_MAX_SAMPLES = 'max_samples' CONF_MIN_GRADIENT = 'min_gradient' -CONF_INVERT = 'invert' CONF_SAMPLE_DURATION = 'sample_duration' +CONF_SENSORS = 'sensors' SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, 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_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, }) @@ -129,11 +127,11 @@ class SensorTrend(BinarySensorDevice): return { ATTR_ENTITY_ID: self._entity_id, ATTR_FRIENDLY_NAME: self._name, - ATTR_INVERT: self._invert, ATTR_GRADIENT: self._gradient, + ATTR_INVERT: self._invert, ATTR_MIN_GRADIENT: self._min_gradient, - ATTR_SAMPLE_DURATION: self._sample_duration, ATTR_SAMPLE_COUNT: len(self.samples), + ATTR_SAMPLE_DURATION: self._sample_duration, } @property diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index f48525d41a8..83dc51a2e0f 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): excludes = config.get(CONF_EXCLUDES) 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) if province: @@ -99,6 +99,11 @@ def day_to_string(day): return None +def get_date(date): + """Return date. Needed for testing.""" + return date + + class IsWorkdaySensor(BinarySensorDevice): """Implementation of a Workday sensor.""" @@ -156,7 +161,7 @@ class IsWorkdaySensor(BinarySensorDevice): self._state = False # 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_of_week = day_to_string(day) diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index eb9f0a2677e..ceab1e98dd4 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -9,7 +9,6 @@ https://home-assistant.io/components/calendar.todoist/ from datetime import datetime from datetime import timedelta import logging -import os import voluptuous as vol @@ -17,7 +16,6 @@ from homeassistant.components.calendar import ( CalendarEventDevice, PLATFORM_SCHEMA) from homeassistant.components.google import ( CONF_DEVICE_ID) -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_TOKEN) 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) - # Services: - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - def handle_new_task(call): """Called when a user creates a new Todoist Task from HASS.""" 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]) hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task, - descriptions[DOMAIN][SERVICE_NEW_TASK], schema=NEW_TASK_SERVICE_SCHEMA) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 110f9a11852..6839c2c3b9c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -12,7 +12,6 @@ from datetime import timedelta import logging import hashlib from random import SystemRandom -import os import aiohttp from aiohttp import web @@ -21,7 +20,6 @@ import voluptuous as vol from homeassistant.core import callback 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.loader import bind_hass from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -190,19 +188,14 @@ def async_setup(hass, config): except OSError as 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( 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( 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( DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service, - descriptions.get(SERVICE_SNAPSHOT), schema=CAMERA_SERVICE_SNAPSHOT) return True diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py index cf6b6b2871f..2ca962a8450 100644 --- a/homeassistant/components/camera/doorbird.py +++ b/homeassistant/components/camera/doorbird.py @@ -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 datetime import logging -import voluptuous as vol import aiohttp 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.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession DEPENDENCIES = ['doorbird'] -_CAMERA_LIVE = "DoorBird Live" _CAMERA_LAST_VISITOR = "DoorBird Last Ring" -_LIVE_INTERVAL = datetime.timedelta(seconds=1) +_CAMERA_LIVE = "DoorBird Live" _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1) +_LIVE_INTERVAL = datetime.timedelta(seconds=1) _LOGGER = logging.getLogger(__name__) _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 def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the DoorBird camera platform.""" device = hass.data.get(DOORBIRD_DOMAIN) - - _LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LIVE) - entities = [DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, - _LIVE_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)") + async_add_devices([ + DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL), + DoorBirdCamera( + device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR, + _LAST_VISITOR_INTERVAL), + ]) class DoorBirdCamera(Camera): @@ -75,7 +64,6 @@ class DoorBirdCamera(Camera): try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): response = yield from websession.get(self._url) diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py old mode 100755 new mode 100644 index 8d72ec35a28..b7a7510e0eb --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -60,11 +60,9 @@ class MqttCamera(Camera): """Return the name of this camera.""" return self._name + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe MQTT events. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe MQTT events.""" @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index 685b6d64364..8d79fa04a9a 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -82,6 +82,7 @@ class UnifiVideoCamera(Camera): self.is_streaming = False self._connect_addr = None self._camera = None + self._motion_status = False @property def name(self): @@ -94,6 +95,12 @@ class UnifiVideoCamera(Camera): caminfo = self._nvr.get_camera(self._uuid) 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 def brand(self): """Return the brand of this camera.""" @@ -165,3 +172,26 @@ class UnifiVideoCamera(Camera): raise 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) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f9ffe4faec9..bb714ad5f81 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -7,12 +7,10 @@ https://home-assistant.io/components/climate/ import asyncio from datetime import timedelta import logging -import os import functools as ft import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.temperature import display_temp as show_temp 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 import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, - TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS) - + ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, + STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, + PRECISION_TENTHS, ) DOMAIN = 'climate' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -63,6 +61,7 @@ SUPPORT_HOLD_MODE = 256 SUPPORT_SWING_MODE = 512 SUPPORT_AWAY_MODE = 1024 SUPPORT_AUX_HEAT = 2048 +SUPPORT_ON_OFF = 4096 ATTR_CURRENT_TEMPERATURE = 'current_temperature' ATTR_MAX_TEMP = 'max_temp' @@ -92,6 +91,10 @@ CONVERTIBLE_ATTRIBUTE = [ _LOGGER = logging.getLogger(__name__) +ON_OFF_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + SET_AWAY_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_AWAY_MODE): cv.boolean, @@ -240,10 +243,6 @@ def async_setup(hass, config): component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) 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 def async_away_mode_set_service(service): """Set away mode on target climate devices.""" @@ -267,7 +266,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service, - descriptions.get(SERVICE_SET_AWAY_MODE), schema=SET_AWAY_MODE_SCHEMA) @asyncio.coroutine @@ -290,7 +288,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service, - descriptions.get(SERVICE_SET_HOLD_MODE), schema=SET_HOLD_MODE_SCHEMA) @asyncio.coroutine @@ -316,7 +313,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service, - descriptions.get(SERVICE_SET_AUX_HEAT), schema=SET_AUX_HEAT_SCHEMA) @asyncio.coroutine @@ -348,7 +344,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service, - descriptions.get(SERVICE_SET_TEMPERATURE), schema=SET_TEMPERATURE_SCHEMA) @asyncio.coroutine @@ -370,7 +365,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service, - descriptions.get(SERVICE_SET_HUMIDITY), schema=SET_HUMIDITY_SCHEMA) @asyncio.coroutine @@ -392,7 +386,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service, - descriptions.get(SERVICE_SET_FAN_MODE), schema=SET_FAN_MODE_SCHEMA) @asyncio.coroutine @@ -414,7 +407,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service, - descriptions.get(SERVICE_SET_OPERATION_MODE), schema=SET_OPERATION_MODE_SCHEMA) @asyncio.coroutine @@ -436,9 +428,34 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service, - descriptions.get(SERVICE_SET_SWING_MODE), 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 @@ -449,8 +466,12 @@ class ClimateDevice(Entity): @property def state(self): """Return the current state.""" + if self.is_on is False: + return STATE_OFF if self.current_operation: return self.current_operation + if self.is_on: + return STATE_ON return STATE_UNKNOWN @property @@ -594,6 +615,11 @@ class ClimateDevice(Entity): """Return the current hold mode, e.g., home, away, temp.""" return None + @property + def is_on(self): + """Return true if on.""" + return None + @property def is_aux_heat_on(self): """Return true if aux heater.""" @@ -730,6 +756,28 @@ class ClimateDevice(Entity): """ 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 def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py new file mode 100644 index 00000000000..8f6df034b89 --- /dev/null +++ b/homeassistant/components/climate/daikin.py @@ -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 diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 4c4b57d42a3..2fe6ba0c874 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -9,14 +9,15 @@ from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + SUPPORT_ON_OFF) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT | SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF) 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._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low + self._on = True @property def supported_features(self): @@ -132,6 +134,11 @@ class DemoClimate(ClimateDevice): """Return true if aux heat is on.""" return self._aux + @property + def is_on(self): + """Return true if the device is on.""" + return self._on + @property def current_fan_mode(self): """Return the fan setting.""" @@ -206,3 +213,13 @@ class DemoClimate(ClimateDevice): """Turn auxiliary heater off.""" self._aux = False 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() diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index aae70a4f1f7..b0685b337be 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.ecobee/ """ import logging -from os import path import voluptuous as vol @@ -17,7 +16,6 @@ from homeassistant.components.climate import ( SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH) from homeassistant.const import ( 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 _CONFIGURING = {} @@ -96,17 +94,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): thermostat.schedule_update_ha_state(True) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - hass.services.register( 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) hass.services.register( DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, - descriptions.get(SERVICE_RESUME_PROGRAM), schema=RESUME_PROGRAM_SCHEMA) diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py new file mode 100644 index 00000000000..5620bcbfa11 --- /dev/null +++ b/homeassistant/components/climate/econet.py @@ -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 diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 6574a4d5396..fdfe56ca62c 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components.climate import ( - STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, - STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE) + STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, + ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) @@ -30,6 +30,7 @@ DEPENDENCIES = ['switch', 'sensor'] DEFAULT_TOLERANCE = 0.3 DEFAULT_NAME = 'Generic Thermostat' +DEFAULT_AWAY_TEMP = 16 CONF_HEATER = 'heater' CONF_SENSOR = 'target_sensor' @@ -42,7 +43,9 @@ CONF_COLD_TOLERANCE = 'cold_tolerance' CONF_HOT_TOLERANCE = 'hot_tolerance' CONF_KEEP_ALIVE = 'keep_alive' 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({ vol.Required(CONF_HEATER): cv.entity_id, @@ -60,7 +63,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_KEEP_ALIVE): vol.All( cv.time_period, cv.positive_timedelta), 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) keep_alive = config.get(CONF_KEEP_ALIVE) initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE) + away_temp = config.get(CONF_AWAY_TEMP) async_add_devices([GenericThermostat( hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, 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): @@ -92,7 +98,7 @@ class GenericThermostat(ClimateDevice): def __init__(self, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, cold_tolerance, hot_tolerance, keep_alive, - initial_operation_mode): + initial_operation_mode, away_temp): """Initialize the thermostat.""" self.hass = hass self._name = name @@ -103,17 +109,26 @@ class GenericThermostat(ClimateDevice): self._hot_tolerance = hot_tolerance self._keep_alive = keep_alive 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: self._enabled = False else: self._enabled = True - self._active = False self._cur_temp = None self._min_temp = min_temp self._max_temp = max_temp self._target_temp = target_temp self._unit = hass.config.units.temperature_unit + self._away_temp = away_temp + self._is_away = False async_track_state_change( hass, sensor_entity_id, self._async_sensor_changed) @@ -124,10 +139,6 @@ class GenericThermostat(ClimateDevice): async_track_time_interval( 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 def async_added_to_hass(self): """Run when entity about to be added.""" @@ -137,14 +148,37 @@ class GenericThermostat(ClimateDevice): if old_state is not None: # If we have no initial temperature, restore if self._target_temp is None: - self._target_temp = float( - old_state.attributes[ATTR_TEMPERATURE]) - - # If we have no initial operation mode, restore + # 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( + old_state.attributes[ATTR_TEMPERATURE]) + self._is_away = True if str( + 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 old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF: 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 def should_poll(self): """Return the polling state.""" @@ -167,15 +201,8 @@ class GenericThermostat(ClimateDevice): @property def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - if not self._enabled: - 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 + """Return current operation.""" + return self._current_operation @property def target_temperature(self): @@ -185,14 +212,20 @@ class GenericThermostat(ClimateDevice): @property def operation_list(self): """List of available operation modes.""" - return [STATE_AUTO, STATE_OFF] + return self._operation_list def set_operation_mode(self, 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._async_control_heating() elif operation_mode == STATE_OFF: + self._current_operation = STATE_OFF self._enabled = False if self._is_device_active: self._heater_turn_off() @@ -252,7 +285,7 @@ class GenericThermostat(ClimateDevice): @callback def _async_keep_alive(self, time): """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() else: self._heater_turn_off() @@ -347,3 +380,23 @@ class GenericThermostat(ClimateDevice): data = {ATTR_ENTITY_ID: self.heater_entity_id} self.hass.async_add_job( 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() diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py index 267657d56ce..8305e772869 100644 --- a/homeassistant/components/climate/hive.py +++ b/homeassistant/components/climate/hive.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.hive/ """ from homeassistant.components.climate import ( 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.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', 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): @@ -134,6 +136,43 @@ class HiveClimateEntity(ClimateDevice): for entity in self.session.entities: 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): """Update all Node data frome Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index 33a63b35530..b8fb7a984fa 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -8,7 +8,8 @@ import logging from homeassistant.components.climate import ( ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, 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 DEPENDENCIES = ['homematic'] @@ -39,6 +40,7 @@ HM_HUMI_MAP = [ ] HM_CONTROL_MODE = 'CONTROL_MODE' +HM_IP_CONTROL_MODE = 'SET_POINT_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: return None - # read state and search - for mode, state in HM_STATE_MAP.items(): - code = getattr(self._hmdevice, mode, 0) - if self._data.get('CONTROL_MODE') == code: - return state + set_point_mode = self._data.get('SET_POINT_MODE', -1) + control_mode = self._data.get('CONTROL_MODE', -1) + boost_mode = self._data.get('BOOST_MODE', False) + + # 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 def operation_list(self): @@ -125,6 +141,7 @@ class HMThermostat(HMDevice, ClimateDevice): if state == operation_mode: code = getattr(self._hmdevice, mode, 0) self._hmdevice.MODE = code + return @property def min_temp(self): @@ -141,7 +158,8 @@ class HMThermostat(HMDevice, ClimateDevice): self._state = next(iter(self._hmdevice.WRITENODE.keys())) 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 for node in self._hmdevice.SENSORNODE.keys(): diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index fb0de1e2de0..97bd3e9503c 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -159,6 +159,11 @@ class KNXClimate(ClimateDevice): """Return the name of the KNX device.""" return self.device.name + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + @property def should_poll(self): """No polling needed within KNX.""" diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index d571ebd39e4..ae71e5a48dc 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -20,8 +20,9 @@ from homeassistant.components.climate import ( SUPPORT_AUX_HEAT) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) -from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN, - MQTT_BASE_PLATFORM_SCHEMA) +from homeassistant.components.mqtt import ( + 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 from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) @@ -93,7 +94,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -134,19 +135,25 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): STATE_OFF, STATE_OFF, False, config.get(CONF_SEND_IF_OFF), 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.""" def __init__(self, hass, name, topic, qos, retain, mode_list, fan_mode_list, swing_mode_list, target_temperature, away, hold, current_fan_mode, current_swing_mode, current_operation, aux, send_if_off, payload_on, - payload_off): + payload_off, availability_topic, payload_available, + payload_not_available): """Initialize the climate device.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self.hass = hass self._name = name self._topic = topic @@ -169,8 +176,11 @@ class MqttClimate(ClimateDevice): self._payload_on = payload_on self._payload_off = payload_off + @asyncio.coroutine def async_added_to_hass(self): """Handle being added to home assistant.""" + yield from super().async_added_to_hass() + @callback def handle_current_temp_received(topic, payload, qos): """Handle current temperature coming via MQTT.""" diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py old mode 100755 new mode 100644 index 2166070a572..7155aaf5924 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -79,11 +79,6 @@ class NetatmoThermostat(ClimateDevice): """Return the name of the sensor.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._target_temperature - @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py new file mode 100644 index 00000000000..f41812dbaae --- /dev/null +++ b/homeassistant/components/climate/nuheat.py @@ -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() diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 624729249aa..870e2db6b42 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -13,37 +13,49 @@ import async_timeout import voluptuous as vol 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 ( - ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA, + ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, - SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE, - SUPPORT_AUX_HEAT) + SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, + SUPPORT_ON_OFF) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.temperature import convert as convert_temperature -REQUIREMENTS = ['pysensibo==1.0.1'] +REQUIREMENTS = ['pysensibo==1.0.2'] _LOGGER = logging.getLogger(__name__) ALL = 'all' TIMEOUT = 10 +SERVICE_ASSUME_STATE = 'sensibo_assume_state' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): 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([ 'room{name}', 'measurements', 'remoteCapabilities', 'acState', 'connectionStatus{isAlive}', 'temperatureUnit']) _INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE | - SUPPORT_AUX_HEAT) +FIELD_TO_FLAG = { + 'fanLevel': SUPPORT_FAN_MODE, + 'mode': SUPPORT_OPERATION_MODE, + 'swing': SUPPORT_SWING_MODE, + 'targetTemperature': SUPPORT_TARGET_TEMPERATURE, + 'on': SUPPORT_ON_OFF, +} @asyncio.coroutine @@ -68,6 +80,28 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if 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): """Representation of a Sensibo device.""" @@ -80,12 +114,13 @@ class SensiboClimate(ClimateDevice): """ self._client = client self._id = data['id'] + self._external_state = None self._do_update(data) @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._supported_features def _do_update(self, data): self._name = data['room']['name'] @@ -106,6 +141,15 @@ class SensiboClimate(ClimateDevice): else: self._temperature_unit = self.unit_of_measurement 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 def device_state_attributes(self): @@ -188,7 +232,7 @@ class SensiboClimate(ClimateDevice): return self._name @property - def is_aux_heat_on(self): + def is_on(self): """Return true if AC is on.""" return self._ac_states['on'] @@ -196,13 +240,13 @@ class SensiboClimate(ClimateDevice): def min_temp(self): """Return the minimum temperature.""" return self._temperatures_list[0] \ - if len(self._temperatures_list) else super.min_temp() + if len(self._temperatures_list) else super().min_temp() @property def max_temp(self): """Return the maximum temperature.""" 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 def async_set_temperature(self, **kwargs): @@ -226,42 +270,62 @@ class SensiboClimate(ClimateDevice): with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( - self._id, 'targetTemperature', temperature) + self._id, 'targetTemperature', temperature, self._ac_states) @asyncio.coroutine def async_set_fan_mode(self, fan): """Set new target fan mode.""" with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( - self._id, 'fanLevel', fan) + self._id, 'fanLevel', fan, self._ac_states) @asyncio.coroutine def async_set_operation_mode(self, operation_mode): """Set new target operation mode.""" with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( - self._id, 'mode', operation_mode) + self._id, 'mode', operation_mode, self._ac_states) @asyncio.coroutine def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( - self._id, 'swing', swing_mode) + self._id, 'swing', swing_mode, self._ac_states) @asyncio.coroutine - def async_turn_aux_heat_on(self): + def async_on(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( - self._id, 'on', True) + self._id, 'on', True, self._ac_states) @asyncio.coroutine - def async_turn_aux_heat_off(self): + def async_off(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): 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 def async_update(self): diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 193c5107575..fbb21962c6e 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -80,7 +80,22 @@ set_swing_mode: example: 'climate.nest' 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: description: Set the minimum fan on time. fields: @@ -100,3 +115,40 @@ ecobee_resume_program: resume_all: description: Resume all events and return to the scheduled program. This default to false which removes only the top event. 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' diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index a8054b838ef..25492cb0895 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.tado/ """ import logging -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ( ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.const import ATTR_TEMPERATURE @@ -192,6 +192,11 @@ class TadoClimate(ClimateDevice): """Return true if away mode is on.""" return self._is_away + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return PRECISION_TENTHS + @property def target_temperature(self): """Return the temperature we try to reach.""" diff --git a/homeassistant/components/climate/touchline.py b/homeassistant/components/climate/touchline.py new file mode 100644 index 00000000000..cc45e26a1cf --- /dev/null +++ b/homeassistant/components/climate/touchline.py @@ -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) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 58a2152f898..e497f4677e4 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -5,13 +5,18 @@ import json import logging import os +import aiohttp +import async_timeout import voluptuous as vol 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 config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util -from homeassistant.components.alexa import smart_home +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 .const import CONFIG_DIR, DOMAIN, SERVERS @@ -21,22 +26,46 @@ REQUIREMENTS = ['warrant==0.6.1'] _LOGGER = logging.getLogger(__name__) CONF_ALEXA = 'alexa' -CONF_ALEXA_FILTER = 'filter' +CONF_GOOGLE_ACTIONS = 'google_actions' +CONF_FILTER = 'filter' CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' +CONF_ALIASES = 'aliases' MODE_DEV = 'development' DEFAULT_MODE = 'production' 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( - CONF_ALEXA_FILTER, + CONF_FILTER, default=lambda: entityfilter.generate_filter([], [], [], []) ): 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({ DOMAIN: vol.Schema({ 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_REGION): 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) @@ -55,22 +85,26 @@ CONFIG_SCHEMA = vol.Schema({ def async_setup(hass, config): """Initialize the Home Assistant cloud.""" if DOMAIN in config: - kwargs = config[DOMAIN] + kwargs = dict(config[DOMAIN]) else: kwargs = {CONF_MODE: DEFAULT_MODE} - if CONF_ALEXA not in kwargs: - kwargs[CONF_ALEXA] = ALEXA_SCHEMA({}) + alexa_conf = kwargs.pop(CONF_ALEXA, None) or 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) - @asyncio.coroutine - def init_cloud(event): - """Initialize connection.""" - yield from cloud.initialize() + success = 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) return True @@ -79,12 +113,16 @@ def async_setup(hass, config): class Cloud: """Store the configuration of the cloud connection.""" - def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None, - region=None, relayer=None, alexa=None): + def __init__(self, hass, mode, alexa, google_actions, + cognito_client_id=None, user_pool_id=None, region=None, + relayer=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode self.alexa_config = alexa + self._google_actions = google_actions + self._gactions_config = None + self.jwt_keyset = None self.id_token = None self.access_token = None self.refresh_token = None @@ -104,11 +142,6 @@ class Cloud: self.region = info['region'] 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 def is_logged_in(self): """Get if cloud is logged in.""" @@ -128,37 +161,44 @@ class Cloud: @property def claims(self): - """Get the claims from the id token.""" - from jose import jwt - return jwt.get_unverified_claims(self.id_token) + """Return the claims from the id token.""" + return self._decode_claims(self.id_token) @property def user_info_path(self): """Get path to the stored auth.""" 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 def initialize(self): """Initialize and load cloud info.""" - def load_config(): - """Load the configuration.""" - # Ensure config dir exists - path = self.hass.config.path(CONFIG_DIR) - if not os.path.isdir(path): - os.mkdir(path) + jwt_success = yield from self._fetch_jwt_keyset() - user_info = self.user_info_path - if os.path.isfile(user_info): - 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'] + if not jwt_success: + return False - 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: - yield from self.iot.connect() + return True def path(self, *parts): """Get config path inside cloud dir. @@ -175,6 +215,7 @@ class Cloud: self.id_token = None self.access_token = None self.refresh_token = None + self._gactions_config = None yield from self.hass.async_add_job( lambda: os.remove(self.user_info_path)) @@ -187,3 +228,79 @@ class Cloud: 'access_token': self.access_token, 'refresh_token': self.refresh_token, }, 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 diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py index 9cad3ec77f3..500ff062a0f 100644 --- a/homeassistant/components/cloud/auth_api.py +++ b/homeassistant/components/cloud/auth_api.py @@ -1,5 +1,4 @@ """Package to communicate with the authentication API.""" -import hashlib import logging @@ -58,11 +57,6 @@ def _map_aws_exception(err): 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): """Register a new account.""" from botocore.exceptions import ClientError @@ -72,10 +66,7 @@ def register(cloud, email, password): # https://github.com/capless/warrant/pull/82 cognito.add_base_attributes() try: - if cloud.cognito_email_based: - cognito.register(email, password) - else: - cognito.register(_generate_username(email), password) + cognito.register(email, password) except ClientError as err: raise _map_aws_exception(err) @@ -86,11 +77,22 @@ def confirm_register(cloud, confirmation_code, email): cognito = _cognito(cloud) try: - if cloud.cognito_email_based: - cognito.confirm_sign_up(confirmation_code, email) - else: - cognito.confirm_sign_up(confirmation_code, - _generate_username(email)) + cognito.confirm_sign_up(confirmation_code, email) + except ClientError as err: + raise _map_aws_exception(err) + + +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: raise _map_aws_exception(err) @@ -99,10 +101,7 @@ def forgot_password(cloud, email): """Initiate forgotten password flow.""" from botocore.exceptions import ClientError - if cloud.cognito_email_based: - cognito = _cognito(cloud, username=email) - else: - cognito = _cognito(cloud, username=_generate_username(email)) + cognito = _cognito(cloud, username=email) try: 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.""" from botocore.exceptions import ClientError - if cloud.cognito_email_based: - cognito = _cognito(cloud, username=email) - else: - cognito = _cognito(cloud, username=_generate_username(email)) + cognito = _cognito(cloud, username=email) try: cognito.confirm_forgot_password(confirmation_code, new_password) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 27fd6f604c0..338e004ce52 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -23,6 +23,7 @@ def async_setup(hass): hass.http.register_view(CloudAccountView) hass.http.register_view(CloudRegisterView) hass.http.register_view(CloudConfirmRegisterView) + hass.http.register_view(CloudResendConfirmView) hass.http.register_view(CloudForgotPasswordView) hass.http.register_view(CloudConfirmForgotPasswordView) @@ -172,6 +173,29 @@ class CloudConfirmRegisterView(HomeAssistantView): 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): """View to start Forgot Password flow..""" @@ -228,6 +252,6 @@ def _account_data(cloud): return { 'email': claims['email'], - 'sub_exp': claims.get('custom:sub-exp'), + 'sub_exp': claims['custom:sub-exp'], 'cloud': cloud.iot.state, } diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 9c67c98cabf..ffe68c3c877 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -5,7 +5,8 @@ import logging from aiohttp import hdrs, client_exceptions, WSMsgType 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.helpers.aiohttp_client import async_get_clientsession from . import auth_api @@ -78,7 +79,7 @@ class CloudIoT: yield from hass.async_add_job(auth_api.check_token, self.cloud) self.client = client = yield from session.ws_connect( - self.cloud.relayer, headers={ + self.cloud.relayer, heartbeat=55, headers={ hdrs.AUTHORIZATION: 'Bearer {}'.format(self.cloud.id_token) }) @@ -204,9 +205,18 @@ def async_handle_message(hass, cloud, handler_name, payload): @asyncio.coroutine def async_handle_alexa(hass, cloud, payload): """Handle an incoming IoT message for Alexa.""" - return (yield from smart_home.async_handle_message(hass, - cloud.alexa_config, - payload)) + result = yield from alexa.async_handle_message(hass, cloud.alexa_config, + 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') diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py new file mode 100644 index 00000000000..bdb091325cf --- /dev/null +++ b/homeassistant/components/coinbase.py @@ -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) diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 064428c010c..5187b4782ef 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -12,20 +12,27 @@ import warnings import voluptuous as vol from homeassistant import core -from homeassistant.loader import bind_hass +from homeassistant.components import http from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.helpers import intent, config_validation as cv -from homeassistant.components import http +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import intent +from homeassistant.loader import bind_hass +REQUIREMENTS = ['fuzzywuzzy==0.16.0'] -REQUIREMENTS = ['fuzzywuzzy==0.15.1'] -DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) ATTR_TEXT = 'text' + +DEPENDENCIES = ['http'] DOMAIN = 'conversation' +INTENT_TURN_OFF = 'HassTurnOff' +INTENT_TURN_ON = 'HassTurnOn' + REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') +REGEX_TYPE = type(re.compile('')) SERVICE_PROCESS = 'process' @@ -39,12 +46,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({ }) })}, extra=vol.ALLOW_EXTRA) -INTENT_TURN_ON = 'HassTurnOn' -INTENT_TURN_OFF = 'HassTurnOff' -REGEX_TYPE = type(re.compile('')) - -_LOGGER = logging.getLogger(__name__) - @core.callback @bind_hass diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index aee94c069f6..2df17a4e50a 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -6,12 +6,10 @@ at https://home-assistant.io/components/counter/ """ import asyncio import logging -import os import voluptuous as vol 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.core import callback from homeassistant.helpers.entity import Entity @@ -133,20 +131,12 @@ def async_setup(hass, config): if tasks: 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( - DOMAIN, SERVICE_INCREMENT, async_handler_service, - descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA) + DOMAIN, SERVICE_INCREMENT, async_handler_service) hass.services.async_register( - DOMAIN, SERVICE_DECREMENT, async_handler_service, - descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA) + DOMAIN, SERVICE_DECREMENT, async_handler_service) hass.services.async_register( - DOMAIN, SERVICE_RESET, async_handler_service, - descriptions[SERVICE_RESET], SERVICE_SCHEMA) + DOMAIN, SERVICE_RESET, async_handler_service) yield from component.async_add_entities(entities) return True diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index ba60382ae64..1dfa0028ab8 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -8,11 +8,9 @@ import asyncio from datetime import timedelta import functools as ft import logging -import os import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity @@ -179,16 +177,12 @@ def async_setup(hass, config): if update_tasks: 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: schema = SERVICE_TO_METHOD[service_name].get( 'schema', COVER_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, service_name, async_handle_cover_service, - descriptions.get(service_name), schema=schema) + schema=schema) return True diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 4dd1c9be364..b187b8409c2 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -8,8 +8,10 @@ import logging from typing import Callable # noqa from homeassistant.components.cover import CoverDevice, DOMAIN -import homeassistant.components.isy994 as isy -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN +from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, + ISYDevice) +from homeassistant.const import ( + STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -17,44 +19,32 @@ _LOGGER = logging.getLogger(__name__) VALUE_TO_STATE = { 0: STATE_CLOSED, 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 def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """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 = [] - - for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES): + for node in hass.data[ISY994_NODES][DOMAIN]: devices.append(ISYCoverDevice(node)) - for program in isy.PROGRAMS.get(DOMAIN, []): - try: - 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)) + for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + devices.append(ISYCoverProgram(name, status, actions)) add_devices(devices) -class ISYCoverDevice(isy.ISYDevice, CoverDevice): +class ISYCoverDevice(ISYDevice, CoverDevice): """Representation of an ISY994 cover device.""" def __init__(self, node: object): """Initialize the ISY994 cover device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def current_cover_position(self) -> int: @@ -90,7 +80,7 @@ class ISYCoverProgram(ISYCoverDevice): def __init__(self, name: str, node: object, actions: object) -> None: """Initialize the ISY994 cover program.""" - ISYCoverDevice.__init__(self, node) + super().__init__(node) self._name = name self._actions = actions diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index b840c780645..d8313caeb5f 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -124,6 +124,11 @@ class KNXCover(CoverDevice): """Return the name of the KNX device.""" return self.device.name + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + @property def should_poll(self): """No polling needed within KNX.""" diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 0a49679b9c4..9b75f03c232 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -21,8 +21,9 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, - CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_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 _LOGGER = logging.getLogger(__name__) @@ -37,8 +38,6 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template' CONF_PAYLOAD_OPEN = 'payload_open' CONF_PAYLOAD_CLOSE = 'payload_close' CONF_PAYLOAD_STOP = 'payload_stop' -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_STATE_OPEN = 'state_open' CONF_STATE_CLOSED = 'state_closed' CONF_TILT_CLOSED_POSITION = 'tilt_closed_value' @@ -52,8 +51,6 @@ DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' DEFAULT_PAYLOAD_CLOSE = 'CLOSE' DEFAULT_PAYLOAD_STOP = 'STOP' -DEFAULT_PAYLOAD_AVAILABLE = 'online' -DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' DEFAULT_OPTIMISTIC = False DEFAULT_RETAIN = False 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_RETAIN, default=DEFAULT_RETAIN): cv.boolean, 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_NAME, default=DEFAULT_NAME): 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_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_CLOSED, default=STATE_CLOSED): cv.string, 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, vol.Optional(CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @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.""" 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_invert, position_topic, set_position_template): """Initialize the cover.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._position = None self._state = None self._name = name self._state_topic = state_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_status_topic = tilt_status_topic self._qos = qos self._payload_open = payload_open self._payload_close = payload_close self._payload_stop = payload_stop - self._payload_available = payload_available - self._payload_not_available = payload_not_available self._state_open = state_open self._state_closed = state_closed self._retain = retain @@ -186,10 +176,9 @@ class MqttCover(CoverDevice): @asyncio.coroutine 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 def tilt_updated(topic, payload, qos): """Handle tilt updates.""" @@ -266,11 +255,6 @@ class MqttCover(CoverDevice): """Return the name of the cover.""" return self._name - @property - def available(self) -> bool: - """Return if cover is available.""" - return self._available - @property def is_closed(self): """Return if the cover is closed.""" diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index 0e28d3ef701..66f2fde52f4 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -4,12 +4,29 @@ Support for RFXtrx cover components. For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.rfxtrx/ """ +import voluptuous as vol + 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'] -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): diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index ce668cfe876..9968e3d6503 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.tahoma/ """ import logging +from datetime import timedelta from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT from homeassistant.components.tahoma import ( @@ -14,6 +15,8 @@ DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=60) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Tahoma covers.""" @@ -70,4 +73,15 @@ class TahomaCover(TahomaDevice, CoverDevice): def stop_cover(self, **kwargs): """Stop the cover.""" - self.apply_action('stopIdentify') + if self.tahoma_device.type == \ + 'io:RollerShutterWithLowSpeedManagementIOComponent': + self.apply_action('setPosition', 'secured') + else: + 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 diff --git a/homeassistant/components/cover/tellstick.py b/homeassistant/components/cover/tellstick.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index 34aa636185e..a7db472f191 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -63,10 +63,15 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(POSITION_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 }) +COVER_SCHEMA = vol.All( + cv.deprecated(CONF_ENTITY_ID), + COVER_SCHEMA, +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), }) diff --git a/homeassistant/components/cover/xiaomi_aqara.py b/homeassistant/components/cover/xiaomi_aqara.py index 17d056a5010..5b51371346b 100644 --- a/homeassistant/components/cover/xiaomi_aqara.py +++ b/homeassistant/components/cover/xiaomi_aqara.py @@ -41,7 +41,7 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice): @property def is_closed(self): """Return if the cover is closed.""" - return self.current_cover_position < 0 + return self.current_cover_position <= 0 def close_cover(self, **kwargs): """Close the cover.""" diff --git a/homeassistant/components/daikin.py b/homeassistant/components/daikin.py new file mode 100644 index 00000000000..5808528ca5a --- /dev/null +++ b/homeassistant/components/daikin.py @@ -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 + ) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py new file mode 100644 index 00000000000..021febdc07c --- /dev/null +++ b/homeassistant/components/deconz/__init__.py @@ -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", + ) diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml new file mode 100644 index 00000000000..2e6593c6ea0 --- /dev/null +++ b/homeassistant/components/deconz/services.yaml @@ -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}' diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 28505900f14..2adee1e2330 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/device_tracker/ import asyncio from datetime import timedelta import logging -import os from typing import Any, List, Sequence, Callable import aiohttp @@ -81,6 +80,8 @@ ATTR_VENDOR = 'vendor' SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_ROUTER = 'router' +SOURCE_TYPE_BLUETOOTH = 'bluetooth' +SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ 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({ 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, default=DEFAULT_CONSIDER_HOME): vol.All( cv.time_period, cv.positive_timedelta), @@ -131,8 +132,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): conf = config.get(DOMAIN, []) conf = conf[0] if conf else {} 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, {}) + 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) tracker = DeviceTracker( @@ -204,12 +208,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)} yield from tracker.async_see(**args) - descriptions = yield from hass.async_add_job( - 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)) + hass.services.async_register(DOMAIN, SERVICE_SEE, async_see_service) # restore 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.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} 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.group = None self._is_updating = asyncio.Lock(loop=hass.loop) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index f2d2a4c74b5..f49f54b3622 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -67,6 +67,15 @@ _IP_NEIGH_REGEX = re.compile( r'\s?(router)?' r'(?P(\w+))') +_ARP_CMD = 'arp -n' +_ARP_REGEX = re.compile( + r'.+\s' + + r'\((?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' + + r'.+\s' + + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' + + r'\s' + + r'.*') + # pylint: disable=unused-argument def get_scanner(hass, config): @@ -76,7 +85,22 @@ def get_scanner(hass, config): 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): @@ -121,16 +145,13 @@ class AsusWrtDeviceScanner(DeviceScanner): def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return [client['mac'] for client in self.last_results] + return list(self.last_results.keys()) def get_device_name(self, device): """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 self.last_results[device].name def _update_info(self): """Ensure the information from the ASUSWRT router is up to date. @@ -145,74 +166,88 @@ class AsusWrtDeviceScanner(DeviceScanner): if not data: return False - active_clients = [client for client in data.values() if - client['status'] == 'REACHABLE' or - client['status'] == 'DELAY' or - client['status'] == 'STALE' or - client['status'] == 'IN_ASSOCLIST'] - self.last_results = active_clients + self.last_results = data return True def get_asuswrt_data(self): - """Retrieve data from ASUSWRT and return parsed result.""" - result = self.connection.get_result() - - if not result: - return {} + """Retrieve data from ASUSWRT. + Calls various commands on the router and returns the superset of all + responses. Some commands will not work on some routers. + """ devices = {} - if self.mode == 'ap': - for lease in result.leases: - match = _WL_REGEX.search(lease.decode('utf-8')) - - if not match: - _LOGGER.warning("Could not parse wl row: %s", lease) - continue - - host = '' - - devices[match.group('mac').upper()] = { - 'host': host, - 'status': 'IN_ASSOCLIST', - 'ip': '', - '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 - - # For leases where the client doesn't set a hostname, ensure it - # is blank and not '*', which breaks entity_id down the line. - host = match.group('host') - if host == '*': - host = '' - - devices[match.group('mac')] = { - 'host': 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')) - + devices.update(self._get_wl()) + devices = self._get_arp(devices) + devices = self._get_neigh(devices) + if not self.mode == 'ap': + devices.update(self._get_leases(devices)) return devices + def _get_wl(self): + lines = self.connection.run_command(_WL_CMD) + if not lines: + return {} + result = _parse_lines(lines, _WL_REGEX) + devices = {} + for device in result: + mac = device['mac'].upper() + devices[mac] = Device(mac, None, None) + return devices + + 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 + # is blank and not '*', which breaks entity_id down the line. + host = device['host'] + if host == '*': + host = '' + mac = device['mac'].upper() + if mac in cur_devices: + devices[mac] = Device(mac, device['ip'], host) + 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: def __init__(self): @@ -247,8 +282,8 @@ class SshConnection(_Connection): self._ssh_key = ssh_key self._ap = ap - def get_result(self): - """Retrieve a single AsusWrtResult through an SSH connection. + def run_command(self, command): + """Run commands through an SSH connection. Connect to the SSH server if not currently connected, otherwise use the existing connection. @@ -258,19 +293,10 @@ class SshConnection(_Connection): try: if not self.connected: self.connect() - if self._ap: - neighbors = [''] - self._ssh.sendline(_WL_CMD) - self._ssh.prompt() - leases_result = self._ssh.before.split(b'\n')[1:-1] - else: - 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) + self._ssh.sendline(command) + self._ssh.prompt() + lines = self._ssh.before.split(b'\n')[1:-1] + return [line.decode('utf-8') for line in lines] except exceptions.EOF as err: _LOGGER.error("Connection refused. SSH enabled?") self.disconnect() @@ -326,8 +352,8 @@ class TelnetConnection(_Connection): self._ap = ap self._prompt_string = None - def get_result(self): - """Retrieve a single AsusWrtResult through a Telnet connection. + def run_command(self, command): + """Run a command through a Telnet connection. Connect to the Telnet server if not currently connected, otherwise use the existing connection. @@ -336,18 +362,10 @@ class TelnetConnection(_Connection): if not self.connected: self.connect() - self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii')) - neighbors = (self._telnet.read_until(self._prompt_string). - split(b'\n')[1:-1]) - if self._ap: - 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) + self._telnet.write('{}\n'.format(command).encode('ascii')) + data = (self._telnet.read_until(self._prompt_string). + split(b'\n')[1:-1]) + return [line.decode('utf-8') for line in data] except EOFError: _LOGGER.error("Unexpected response from router") self.disconnect() diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index 22713cdc18e..19582822913 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( 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.helpers.config_validation as cv @@ -54,7 +54,8 @@ def setup_scanner(hass, config, see, discovery_info=None): new_devices[address] = 1 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(): """Discover Bluetooth LE devices.""" diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 9e0957e363f..a535d87105e 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -12,7 +12,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( 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 _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,8 @@ def setup_scanner(hass, config, see, discovery_info=None): def see_device(device): """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(): """Discover Bluetooth devices.""" diff --git a/homeassistant/components/device_tracker/geofency.py b/homeassistant/components/device_tracker/geofency.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index b88245ac9a5..1952e6d676d 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -5,23 +5,37 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.gpslogger/ """ import asyncio -from functools import partial import logging +from hmac import compare_digest -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY -from homeassistant.components.http import HomeAssistantView +from aiohttp.web import Request, HTTPUnauthorized # NOQA +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 from homeassistant.components.device_tracker import ( # NOQA - DOMAIN, PLATFORM_SCHEMA) + DOMAIN, PLATFORM_SCHEMA +) _LOGGER = logging.getLogger(__name__) 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.""" - hass.http.register_view(GPSLoggerView(see)) + hass.http.register_view(GPSLoggerView(async_see, config)) return True @@ -32,26 +46,36 @@ class GPSLoggerView(HomeAssistantView): url = '/api/gpslogger' name = 'api:gpslogger' - def __init__(self, see): + def __init__(self, async_see, config): """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 - def get(self, request): + def get(self, request: Request): """Handle for GPSLogger message received as GET.""" - res = yield from self._handle(request.app['hass'], request.query) - return res + hass = request.app['hass'] + 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: return ('Latitude and longitude not specified.', HTTP_UNPROCESSABLE_ENTITY) if 'device' not in data: _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('-', '') gps_location = (data['latitude'], data['longitude']) @@ -75,10 +99,11 @@ class GPSLoggerView(HomeAssistantView): if 'activity' in data: attrs['activity'] = data['activity'] - yield from hass.async_add_job( - partial(self.see, dev_id=device, - gps=gps_location, battery=battery, - gps_accuracy=accuracy, - attributes=attrs)) + hass.async_add_job(self.async_see( + dev_id=device, + gps=gps_location, battery=battery, + gps_accuracy=accuracy, + attributes=attrs + )) return 'Setting location for {}'.format(device) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 0c869dd4b57..32d677a59db 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -32,19 +32,27 @@ CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_SECRET = 'secret' CONF_WAYPOINT_IMPORT = 'waypoints' CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' +CONF_MQTT_TOPIC = 'mqtt_topic' +CONF_REGION_MAPPING = 'region_mapping' +CONF_EVENTS_ONLY = 'events_only' DEPENDENCIES = ['mqtt'] -OWNTRACKS_TOPIC = 'owntracks/#' +DEFAULT_OWNTRACKS_TOPIC = 'owntracks/#' +REGION_MAPPING = {} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float), 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( cv.ensure_list, [cv.string]), vol.Optional(CONF_SECRET): vol.Any( 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 mqtt.async_subscribe( - hass, OWNTRACKS_TOPIC, async_handle_mqtt_message, 1) + hass, context.mqtt_topic, async_handle_mqtt_message, 1) return True -def _parse_topic(topic): - """Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple. +def _parse_topic(topic, subscribe_topic): + """Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple. Async friendly. """ + subscription = subscribe_topic.split('/') try: - _, user, device, *_ = topic.split('/', 3) + user_index = subscription.index('#') 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) raise 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. Async friendly. """ - user, device = _parse_topic(message['topic']) + user, device = _parse_topic(message['topic'], subscribe_topic) dev_id = slugify('{}_{}'.format(user, device)) kwargs = { 'dev_id': dev_id, @@ -185,16 +201,20 @@ def context_from_config(async_see, config): waypoint_import = config.get(CONF_WAYPOINT_IMPORT) waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) 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, - waypoint_import, waypoint_whitelist) + waypoint_import, waypoint_whitelist, + region_mapping, events_only, mqtt_topic) class OwnTracksContext: """Hold the current OwnTracks context.""" 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.""" self.async_see = async_see self.secret = secret @@ -203,6 +223,9 @@ class OwnTracksContext: self.regions_entered = defaultdict(list) self.import_waypoints = import_waypoints self.waypoint_whitelist = waypoint_whitelist + self.region_mapping = region_mapping + self.events_only = events_only + self.mqtt_topic = mqtt_topic @callback 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): 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]: _LOGGER.debug( @@ -283,7 +310,7 @@ def async_handle_location_message(hass, context, message): def _async_transition_message_enter(hass, context, message, location): """Execute enter event.""" 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': # 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 def _async_transition_message_leave(hass, context, message, location): """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] 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 # to switch on 'hold mode' - ignore this 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': location = STATE_HOME @@ -398,7 +431,7 @@ def async_handle_waypoints_message(hass, context, message): return 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: return @@ -410,7 +443,7 @@ def async_handle_waypoints_message(hass, context, message): _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: yield from async_handle_waypoint(hass, name_base, wayp) diff --git a/homeassistant/components/device_tracker/owntracks_http.py b/homeassistant/components/device_tracker/owntracks_http.py index dcc3300cc12..d74e1fc6d95 100644 --- a/homeassistant/components/device_tracker/owntracks_http.py +++ b/homeassistant/components/device_tracker/owntracks_http.py @@ -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/ """ import asyncio +import re from aiohttp.web_exceptions import HTTPInternalServerError @@ -43,8 +44,11 @@ class OwnTracksView(HomeAssistantView): """Handle an OwnTracks message.""" hass = request.app['hass'] + subscription = self.context.mqtt_topic + topic = re.sub('/#$', '', subscription) + message = yield from request.json() - message['topic'] = 'owntracks/{}/{}'.format(user, device) + message['topic'] = '{}/{}/{}'.format(topic, user, device) try: yield from async_handle_message(hass, self.context, message) diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index 36f1ea06fd6..6a0cb18d55e 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -13,8 +13,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER) -from homeassistant.helpers.event import track_point_in_utc_time + PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, + SOURCE_TYPE_ROUTER) from homeassistant import util 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.""" hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in config[const.CONF_HOSTS].items()] - interval = timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + \ - DEFAULT_SCAN_INTERVAL - _LOGGER.info("Started ping tracker with interval=%s on hosts: %s", - interval, ",".join([host.ip_address for host in hosts])) + interval = config.get(CONF_SCAN_INTERVAL, + timedelta(seconds=len(hosts) * + 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])) - def update(now): + def update_interval(now): """Update all the hosts on every interval time.""" - for host in hosts: - host.update(see) - track_point_in_utc_time(hass, update, util.dt.utcnow() + interval) - return True + try: + for host in hosts: + host.update(see) + finally: + hass.helpers.event.track_point_in_utc_time( + update_interval, util.dt.utcnow() + interval) - return update(util.dt.utcnow()) + update_interval(None) + return True diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index add027e1823..c9c27fb2bfa 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -REQUIREMENTS = ['pysnmp==4.4.2'] +REQUIREMENTS = ['pysnmp==4.4.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index f27a950a49f..377686b6905 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -19,7 +19,7 @@ from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pytile==1.0.0'] +REQUIREMENTS = ['pytile==1.1.0'] CLIENT_UUID_CONFIG_FILE = '.tile.conf' DEFAULT_ICON = 'mdi:bluetooth' @@ -29,14 +29,15 @@ ATTR_ALTITUDE = 'altitude' ATTR_CONNECTION_STATE = 'connection_state' ATTR_IS_DEAD = 'is_dead' ATTR_IS_LOST = 'is_lost' -ATTR_LAST_SEEN = 'last_seen' -ATTR_LAST_UPDATED = 'last_updated' ATTR_RING_STATE = 'ring_state' ATTR_VOIP_STATE = 'voip_state' +CONF_SHOW_INACTIVE = 'show_inactive' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean, vol.Optional(CONF_MONITORED_VARIABLES): 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('User UUID: %s', self._client.user_uuid) + self._show_inactive = config.get(CONF_SHOW_INACTIVE) self._types = config.get(CONF_MONITORED_VARIABLES) self.devices = {} @@ -91,29 +93,25 @@ class TileDeviceScanner(DeviceScanner): def _update_info(self, now=None) -> None: """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: - self.devices = device_data['result'] - except KeyError: + if not self.devices: _LOGGER.warning('No Tiles found') - _LOGGER.debug(device_data) return - for info in self.devices.values(): - dev_id = 'tile_{0}'.format(slugify(info['name'])) - lat = info['tileState']['latitude'] - lon = info['tileState']['longitude'] + for dev in self.devices: + dev_id = 'tile_{0}'.format(slugify(dev['name'])) + lat = dev['tileState']['latitude'] + lon = dev['tileState']['longitude'] attrs = { - ATTR_ALTITUDE: info['tileState']['altitude'], - ATTR_CONNECTION_STATE: info['tileState']['connection_state'], - ATTR_IS_DEAD: info['is_dead'], - ATTR_IS_LOST: info['tileState']['is_lost'], - ATTR_LAST_SEEN: info['tileState']['timestamp'], - ATTR_LAST_UPDATED: device_data['timestamp_ms'], - ATTR_RING_STATE: info['tileState']['ring_state'], - ATTR_VOIP_STATE: info['tileState']['voip_state'], + ATTR_ALTITUDE: dev['tileState']['altitude'], + ATTR_CONNECTION_STATE: dev['tileState']['connection_state'], + ATTR_IS_DEAD: dev['is_dead'], + ATTR_IS_LOST: dev['tileState']['is_lost'], + ATTR_RING_STATE: dev['tileState']['ring_state'], + ATTR_VOIP_STATE: dev['tileState']['voip_state'], } self.see( diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py index 726b8d99e01..63205c5479c 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol -from homeassistant.const import HTTP_BAD_REQUEST +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, template from homeassistant.components.http import HomeAssistantView @@ -33,6 +33,10 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +class DialogFlowError(HomeAssistantError): + """Raised when a DialogFlow error happens.""" + + @asyncio.coroutine def async_setup(hass, config): """Set up Dialogflow component.""" @@ -51,57 +55,71 @@ class DialogflowIntentsView(HomeAssistantView): def post(self, request): """Handle Dialogflow.""" hass = request.app['hass'] - data = yield from request.json() + message = yield from request.json() - _LOGGER.debug("Received Dialogflow request: %s", data) - - req = data.get('result') - - if req is None: - _LOGGER.error("Received invalid data from Dialogflow: %s", data) - return self.json_message( - "Expected result value not received", HTTP_BAD_REQUEST) - - action_incomplete = req['actionIncomplete'] - - if action_incomplete: - return None - - action = req.get('action') - parameters = req.get('parameters') - dialogflow_response = DialogflowResponse(parameters) - - if action == "": - _LOGGER.warning("Received intent with empty action") - dialogflow_response.add_speech( - "You have not defined an action in your Dialogflow intent.") - return self.json(dialogflow_response) + _LOGGER.debug("Received Dialogflow request: %s", message) try: - intent_response = yield from intent.async_handle( - hass, DOMAIN, action, - {key: {'value': value} for key, value - in parameters.items()}) + response = yield from async_handle_message(hass, message) + return b'' if response is None else self.json(response) + + except DialogFlowError as err: + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, str(err))) 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) + _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.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) + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, + "Invalid slot information received for this intent.")) - if 'plain' in intent_response.speech: - dialogflow_response.add_speech( - intent_response.speech['plain']['speech']) + except intent.IntentError as err: + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, "Error handling intent.")) - return self.json(dialogflow_response) + +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'] + + if action_incomplete: + return None + + action = req.get('action', '') + parameters = req.get('parameters') + dialogflow_response = DialogflowResponse(parameters) + + if action == "": + raise DialogFlowError( + "You have not defined an action in your Dialogflow intent.") + + intent_response = yield from intent.async_handle( + hass, DOMAIN, action, + {key: {'value': value} for key, value + in parameters.items()}) + + if 'plain' in intent_response.speech: + dialogflow_response.add_speech( + intent_response.speech['plain']['speech']) + + return dialogflow_response.as_dict() class DialogflowResponse(object): diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index 6ba2c824859..bd03fb01975 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-digitalocean==1.12'] +REQUIREMENTS = ['python-digitalocean==1.13.2'] _LOGGER = logging.getLogger(__name__) @@ -44,13 +44,19 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Digital Ocean component.""" + import digitalocean + conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) digital = DigitalOcean(access_token) - if not digital.manager.get_account(): - _LOGGER.error("No Digital Ocean account found for the given API Token") + try: + if not digital.manager.get_account(): + _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 hass.data[DATA_DIGITAL_OCEAN] = digital diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index dde33aa10a2..0c3152db3d6 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -37,6 +37,8 @@ SERVICE_WINK = 'wink' SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' +SERVICE_DECONZ = 'deconz' +SERVICE_DAIKIN = 'daikin' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), @@ -50,6 +52,7 @@ SERVICE_HANDLERS = { SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_HUE: ('hue', None), + SERVICE_DECONZ: ('deconz', None), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index dcf99fe2933..56933d198f2 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -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 voluptuous as vol from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['DoorBirdPy==0.1.0'] +REQUIREMENTS = ['DoorBirdPy==0.1.2'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'doorbird' +API_URL = '/api/{}'.format(DOMAIN) + +CONF_DOORBELL_EVENTS = 'doorbell_events' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): 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) +SENSOR_DOORBELL = 'doorbell' + def setup(hass, config): """Set up the DoorBird component.""" + from doorbirdpy import DoorBird + device_ip = config[DOMAIN].get(CONF_HOST) username = config[DOMAIN].get(CONF_USERNAME) password = config[DOMAIN].get(CONF_PASSWORD) - from doorbirdpy import DoorBird device = DoorBird(device_ip, username, password) status = device.ready() if status[0]: _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username) hass.data[DOMAIN] = device - return True elif status[1] == 401: _LOGGER.error("Authorization rejected by DoorBird at %s", device_ip) return False @@ -42,3 +56,31 @@ def setup(hass, config): _LOGGER.error("Could not connect to DoorBird at %s: Error %s", device_ip, str(status[1])) 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' diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index dda556ba6a4..88cbf1bd57b 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -6,13 +6,11 @@ https://home-assistant.io/components/eight_sleep/ """ import asyncio import logging -import os from datetime import timedelta import voluptuous as vol from homeassistant.core import callback -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) @@ -159,10 +157,6 @@ def async_setup(hass, config): CONF_BINARY_SENSORS: binary_sensors, }, config)) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - @asyncio.coroutine def async_service_handler(service): """Handle eight sleep service calls.""" @@ -183,7 +177,6 @@ def async_setup(hass, config): # Register services hass.services.async_register( DOMAIN, SERVICE_HEAT_SET, async_service_handler, - descriptions[DOMAIN].get(SERVICE_HEAT_SET), schema=SERVICE_EIGHT_SCHEMA) @asyncio.coroutine diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 7710040ae99..eccc800319c 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -8,12 +8,10 @@ import asyncio from datetime import timedelta import functools as ft import logging -import os import voluptuous as vol from homeassistant.components import group -from homeassistant.config import load_yaml_config_file from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF, ATTR_ENTITY_ID, STATE_UNKNOWN) @@ -225,16 +223,10 @@ def async_setup(hass, config: dict): if update_tasks: 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: schema = SERVICE_TO_METHOD[service_name].get('schema') hass.services.async_register( - DOMAIN, service_name, async_handle_fan_service, - descriptions.get(service_name), schema=schema) + DOMAIN, service_name, async_handle_fan_service, schema=schema) return True diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index 0e0e3fdfaf3..f2630aa98d2 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/fan.dyson/ """ import logging import asyncio -from os import path import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, @@ -13,7 +12,6 @@ from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, DOMAIN) from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.dyson import DYSON_DEVICES -from homeassistant.config import load_yaml_config_file DEPENDENCIES = ['dyson'] @@ -44,9 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(hass.data[DYSON_FAN_DEVICES]) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - def service_handle(service): """Handle dyson services.""" 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) hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE, service_handle, - descriptions.get(SERVICE_SET_NIGHT_MODE), schema=DYSON_SET_NIGHT_MODE_SCHEMA) diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index 58c8caa331b..85e603c8c81 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -12,7 +12,6 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity) from homeassistant.helpers.entity import ToggleEntity import homeassistant.util as util -from homeassistant.util.json import load_json, save_json _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -20,8 +19,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['insteon_local'] DOMAIN = 'fan' -INSTEON_LOCAL_FANS_CONF = 'insteon_local_fans.conf' - MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) @@ -31,85 +28,34 @@ SUPPORT_INSTEON_LOCAL = SUPPORT_SET_SPEED def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Insteon local fan platform.""" insteonhub = hass.data['insteon_local'] - - conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF)) - if conf_fans: - for device_id in conf_fans: - setup_fan(device_id, conf_fans[device_id], insteonhub, hass, - add_devices) - - else: - linked = insteonhub.get_linked() - - for device_id in linked: - if (linked[device_id]['cat_type'] == 'dimmer' and - linked[device_id]['sku'] == '2475F' and - device_id not in conf_fans): - request_configuration(device_id, - insteonhub, - linked[device_id]['model_name'] + ' ' + - linked[device_id]['sku'], - hass, add_devices) - - -def request_configuration(device_id, insteonhub, model, hass, - add_devices_callback): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - # We got an error if this method is called while we are configuring - if device_id in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[device_id], 'Failed to register, please try again.') - + if discovery_info is None: return - def insteon_fan_config_callback(data): - """The actions to do when our configuration callback is called.""" - setup_fan(device_id, data.get('name'), insteonhub, hass, - add_devices_callback) + linked = discovery_info['linked'] + device_list = [] + for device_id in linked: + if (linked[device_id]['cat_type'] == 'dimmer' and + linked[device_id]['sku'] == '2475F'): + device = insteonhub.fan(device_id) + device_list.append( + InsteonLocalFanDevice(device) + ) - _CONFIGURING[device_id] = configurator.request_config( - 'Insteon ' + model + ' addr: ' + device_id, - insteon_fan_config_callback, - description=('Enter a name for ' + model + ' Fan addr: ' + device_id), - entity_picture='/static/images/config_insteon.png', - submit_caption='Confirm', - fields=[{'id': 'name', 'name': 'Name', 'type': ''}] - ) - - -def setup_fan(device_id, name, insteonhub, hass, add_devices_callback): - """Set up the fan.""" - if device_id in _CONFIGURING: - request_id = _CONFIGURING.pop(device_id) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.info("Device configuration done!") - - conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF)) - if device_id not in conf_fans: - conf_fans[device_id] = name - - save_json(hass.config.path(INSTEON_LOCAL_FANS_CONF), conf_fans) - - device = insteonhub.fan(device_id) - add_devices_callback([InsteonLocalFanDevice(device, name)]) + add_devices(device_list) class InsteonLocalFanDevice(FanEntity): """An abstract Class for an Insteon node.""" - def __init__(self, node, name): + def __init__(self, node): """Initialize the device.""" self.node = node - self.node.deviceName = name self._speed = SPEED_OFF @property def name(self): """Return the name of the node.""" - return self.node.deviceName + return self.node.device_id @property def unique_id(self): diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index a49952569a8..137bc400d0d 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -9,18 +9,13 @@ from typing import Callable from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH) -import homeassistant.components.isy994 as isy -from homeassistant.const import STATE_ON, STATE_OFF + SPEED_HIGH, SUPPORT_SET_SPEED) +from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, + ISYDevice) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -# Define term used for medium speed. This must be set as the fan component uses -# 'medium' which the ISY does not understand -ISY_SPEED_MEDIUM = 'med' - - VALUE_TO_STATE = { 0: SPEED_OFF, 63: SPEED_LOW, @@ -34,41 +29,28 @@ STATE_TO_VALUE = {} for key in VALUE_TO_STATE: STATE_TO_VALUE[VALUE_TO_STATE[key]] = key -STATES = [SPEED_OFF, SPEED_LOW, ISY_SPEED_MEDIUM, SPEED_HIGH] - # pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 fan 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 = [] - for node in isy.filter_nodes(isy.NODES, states=STATES): + for node in hass.data[ISY994_NODES][DOMAIN]: devices.append(ISYFanDevice(node)) - for program in isy.PROGRAMS.get(DOMAIN, []): - try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass - else: - devices.append(ISYFanProgram(program.name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + devices.append(ISYFanProgram(name, status, actions)) add_devices(devices) -class ISYFanDevice(isy.ISYDevice, FanEntity): +class ISYFanDevice(ISYDevice, FanEntity): """Representation of an ISY994 fan device.""" def __init__(self, node) -> None: """Initialize the ISY994 fan device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def speed(self) -> str: @@ -76,7 +58,7 @@ class ISYFanDevice(isy.ISYDevice, FanEntity): return VALUE_TO_STATE.get(self.value) @property - def is_on(self) -> str: + def is_on(self) -> bool: """Get if the fan is on.""" return self.value != 0 @@ -97,32 +79,32 @@ class ISYFanDevice(isy.ISYDevice, FanEntity): """Get the list of available speeds.""" return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED + class ISYFanProgram(ISYFanDevice): """Representation of an ISY994 fan program.""" def __init__(self, name: str, node, actions) -> None: """Initialize the ISY994 fan program.""" - ISYFanDevice.__init__(self, node) + super().__init__(node) self._name = name self._actions = actions - self.speed = STATE_ON if self.is_on else STATE_OFF - - @property - def state(self) -> str: - """Get the state of the ISY994 fan program.""" - return STATE_ON if bool(self.value) else STATE_OFF def turn_off(self, **kwargs) -> None: """Send the turn on command to ISY994 fan program.""" if not self._actions.runThen(): _LOGGER.error("Unable to turn off the fan") - else: - self.speed = STATE_ON if self.is_on else STATE_OFF def turn_on(self, **kwargs) -> None: """Send the turn off command to ISY994 fan program.""" if not self._actions.runElse(): _LOGGER.error("Unable to turn on the fan") - else: - self.speed = STATE_ON if self.is_on else STATE_OFF + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return 0 diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index eed6cf898c1..1ecbb12bcb4 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -15,7 +15,9 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, @@ -72,7 +74,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -111,15 +113,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, config.get(CONF_SPEED_LIST), config.get(CONF_OPTIMISTIC), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttFan(FanEntity): +class MqttFan(MqttAvailability, FanEntity): """A MQTT fan component.""" def __init__(self, name, topic, templates, qos, retain, payload, - speed_list, optimistic): + speed_list, optimistic, availability_topic, payload_available, + payload_not_available): """Initialize the MQTT fan.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._topic = topic self._qos = qos @@ -143,10 +151,9 @@ class MqttFan(FanEntity): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ templates = {} for key, tpl in list(self._templates.items()): if tpl is None: diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index e5430555910..9f21fda408d 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -7,14 +7,12 @@ https://home-assistant.io/components/fan.xiaomi_miio/ import asyncio from functools import partial import logging -import os import voluptuous as vol from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, DOMAIN) -from homeassistant.config import load_yaml_config_file from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ATTR_ENTITY_ID, ) from homeassistant.exceptions import PlatformNotReady @@ -31,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.2'] +REQUIREMENTS = ['python-miio==0.3.3'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' @@ -131,16 +129,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if update_tasks: 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__), 'xiaomi_miio_services.yaml')) - for air_purifier_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[air_purifier_service].get( 'schema', AIRPURIFIER_SERVICE_SCHEMA) hass.services.async_register( - DOMAIN, air_purifier_service, async_service_handler, - description=descriptions.get(air_purifier_service), schema=schema) + DOMAIN, air_purifier_service, async_service_handler, schema=schema) class XiaomiAirPurifier(FanEntity): diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index dc0439b8b32..e083affe92b 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -6,14 +6,12 @@ https://home-assistant.io/components/ffmpeg/ """ import asyncio import logging -import os import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from homeassistant.config import load_yaml_config_file from homeassistant.helpers.dispatcher import ( async_dispatcher_send, async_dispatcher_connect) import homeassistant.helpers.config_validation as cv @@ -89,10 +87,6 @@ def async_setup(hass, config): conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST) ) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - # Register service @asyncio.coroutine def async_service_handle(service): @@ -108,15 +102,14 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_START, async_service_handle, - descriptions[DOMAIN].get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA) + schema=SERVICE_FFMPEG_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_STOP, async_service_handle, - descriptions[DOMAIN].get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA) + schema=SERVICE_FFMPEG_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_RESTART, async_service_handle, - descriptions[DOMAIN].get(SERVICE_RESTART), schema=SERVICE_FFMPEG_SCHEMA) hass.data[DATA_FFMPEG] = manager diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index 61c5e9b1da6..2c10df327f4 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -6,14 +6,12 @@ https://home-assistant.io/components/foursquare/ """ import asyncio import logging -import os import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST -from homeassistant.config import load_yaml_config_file from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -50,9 +48,6 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Foursquare component.""" - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - config = config[DOMAIN] def checkin_user(call): @@ -72,7 +67,6 @@ def setup(hass, config): # Register our service with Home Assistant. hass.services.register(DOMAIN, 'checkin', checkin_user, - descriptions[DOMAIN][SERVICE_CHECKIN], schema=CHECKIN_SERVICE_SCHEMA) hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET])) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2bbb4dbe405..7d19ed46cd9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20171216.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180112.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -49,7 +49,7 @@ MANIFEST_JSON = { 'lang': 'en-US', 'name': 'Home Assistant', 'short_name': 'Assistant', - 'start_url': '/', + 'start_url': '/states', 'theme_color': DEFAULT_THEME_COLOR } @@ -376,12 +376,11 @@ def async_setup(hass, config): for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []): add_extra_html_url(hass, url, True) - yield from async_setup_themes(hass, conf.get(CONF_THEMES)) + async_setup_themes(hass, conf.get(CONF_THEMES)) return True -@asyncio.coroutine def async_setup_themes(hass, themes): """Set up themes data and services.""" hass.http.register_view(ThemesView) @@ -428,16 +427,9 @@ def async_setup_themes(hass, themes): hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME update_theme_and_fire_event() - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - hass.services.async_register(DOMAIN, SERVICE_SET_THEME, - set_theme, - descriptions[SERVICE_SET_THEME], - SERVICE_SET_THEME_SCHEMA) - hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes, - descriptions[SERVICE_RELOAD_THEMES]) + hass.services.async_register( + DOMAIN, SERVICE_SET_THEME, set_theme, schema=SERVICE_SET_THEME_SCHEMA) + hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) class IndexView(HomeAssistantView): @@ -579,8 +571,12 @@ def _is_latest(js_option, request): if js_option != 'auto': return js_option == 'latest' + useragent = request.headers.get('User-Agent') + if not useragent: + return False + from user_agents import parse - useragent = parse(request.headers.get('User-Agent')) + useragent = parse(useragent) # on iOS every browser is a Safari which we support from version 11. if useragent.os.family == 'iOS': diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index efb2b12bfca..f7923067270 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -190,8 +190,7 @@ def setup_services(hass, track_new_found_calendars, calendar_service): hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]) hass.services.register( - DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar, - None, schema=None) + DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) def _scan_for_calendars(service): """Scan for new calendars.""" @@ -204,9 +203,7 @@ def setup_services(hass, track_new_found_calendars, calendar_service): calendar) hass.services.register( - DOMAIN, SERVICE_SCAN_CALENDARS, - _scan_for_calendars, - None, schema=None) + DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) return True diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 2db36d8829f..aac258b4e93 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -4,7 +4,6 @@ Support for Actions on Google Assistant Smart Home Control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/google_assistant/ """ -import os import asyncio import logging @@ -15,23 +14,24 @@ import voluptuous as vol # Typing imports # pylint: disable=using-constant-test,unused-import,ungrouped-imports -# if False: from homeassistant.core import HomeAssistant # NOQA from typing import Dict, Any # NOQA -from homeassistant import config as conf_util +from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.loader import bind_hass from .const import ( DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, - CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - CONF_AGENT_USER_ID, CONF_API_KEY, - SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL + CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, + DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY, + SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, + CONF_EXPOSE, CONF_ALIASES ) from .auth import GoogleAssistantAuthView -from .http import GoogleAssistantView +from .http import async_register_http +from .smart_home import MAPPING_COMPONENT _LOGGER = logging.getLogger(__name__) @@ -39,17 +39,27 @@ DEPENDENCIES = ['http'] DEFAULT_AGENT_USER_ID = 'home-assistant' +ENTITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE): vol.In(MAPPING_COMPONENT), + vol.Optional(CONF_EXPOSE): cv.boolean, + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) +}) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { vol.Required(CONF_PROJECT_ID): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, + vol.Optional(CONF_EXPOSE_BY_DEFAULT, + default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS, + default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_AGENT_USER_ID, default=DEFAULT_AGENT_USER_ID): cv.string, - vol.Optional(CONF_API_KEY): cv.string + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} } }, extra=vol.ALLOW_EXTRA) @@ -67,13 +77,8 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): config = yaml_config.get(DOMAIN, {}) agent_user_id = config.get(CONF_AGENT_USER_ID) api_key = config.get(CONF_API_KEY) - if api_key is not None: - descriptions = yield from hass.async_add_job( - conf_util.load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml') - ) hass.http.register_view(GoogleAssistantAuthView(hass, config)) - hass.http.register_view(GoogleAssistantView(hass, config)) + async_register_http(hass, config) @asyncio.coroutine def request_sync_service_handler(call): @@ -94,10 +99,9 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not contact Google for request_sync") -# Register service only if api key is provided + # Register service only if api key is provided if api_key is not None: hass.services.async_register( - DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler, - descriptions.get(SERVICE_REQUEST_SYNC)) + DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler) return True diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index 4ef30ff53c8..1ed27403797 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -6,10 +6,10 @@ import logging # Typing imports # pylint: disable=using-constant-test,unused-import,ungrouped-imports # if False: -from homeassistant.core import HomeAssistant # NOQA from aiohttp.web import Request, Response # NOQA from typing import Dict, Any # NOQA +from homeassistant.core import HomeAssistant # NOQA from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( HTTP_BAD_REQUEST, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index c15f14bccdb..fc250c4b655 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -3,10 +3,8 @@ DOMAIN = 'google_assistant' GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' -ATTR_GOOGLE_ASSISTANT = 'google_assistant' -ATTR_GOOGLE_ASSISTANT_NAME = 'google_assistant_name' -ATTR_GOOGLE_ASSISTANT_TYPE = 'google_assistant_type' - +CONF_EXPOSE = 'expose' +CONF_ENTITY_CONFIG = 'entity_config' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_PROJECT_ID = 'project_id' diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index a9512404b1e..f376435d2ef 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -7,63 +7,51 @@ https://home-assistant.io/components/google_assistant/ import asyncio import logging -from typing import Any, Dict # NOQA - from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response # NOQA +from homeassistant.const import HTTP_UNAUTHORIZED + # Typing imports # pylint: disable=using-constant-test,unused-import,ungrouped-imports -# if False: from homeassistant.components.http import HomeAssistantView -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED -from homeassistant.core import HomeAssistant # NOQA +from homeassistant.core import HomeAssistant, callback # NOQA from homeassistant.helpers.entity import Entity # NOQA from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, CONF_ACCESS_TOKEN, - DEFAULT_EXPOSE_BY_DEFAULT, - DEFAULT_EXPOSED_DOMAINS, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - ATTR_GOOGLE_ASSISTANT, - CONF_AGENT_USER_ID + CONF_AGENT_USER_ID, + CONF_ENTITY_CONFIG, + CONF_EXPOSE, ) -from .smart_home import entity_to_device, query_device, determine_service +from .smart_home import async_handle_message, Config _LOGGER = logging.getLogger(__name__) -class GoogleAssistantView(HomeAssistantView): - """Handle Google Assistant requests.""" +@callback +def async_register_http(hass, cfg): + """Register HTTP views for Google Assistant.""" + access_token = cfg.get(CONF_ACCESS_TOKEN) + expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) + exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) + agent_user_id = cfg.get(CONF_AGENT_USER_ID) + entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} - url = GOOGLE_ASSISTANT_API_ENDPOINT - name = 'api:google_assistant' - requires_auth = False # Uses access token from oauth flow - - def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None: - """Initialize Google Assistant view.""" - super().__init__() - - self.access_token = cfg.get(CONF_ACCESS_TOKEN) - self.expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT, - DEFAULT_EXPOSE_BY_DEFAULT) - self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS) - self.agent_user_id = cfg.get(CONF_AGENT_USER_ID) - - def is_entity_exposed(self, entity) -> bool: + def is_exposed(entity) -> bool: """Determine if an entity should be exposed to Google Assistant.""" if entity.attributes.get('view') is not None: # Ignore entities that are views return False - domain = entity.domain.lower() - explicit_expose = entity.attributes.get(ATTR_GOOGLE_ASSISTANT, None) + explicit_expose = \ + entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = \ - self.expose_by_default and domain in self.exposed_domains + expose_by_default and entity.domain in exposed_domains # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being @@ -73,79 +61,22 @@ class GoogleAssistantView(HomeAssistantView): return is_default_exposed or explicit_expose - @asyncio.coroutine - def handle_sync(self, hass: HomeAssistant, request_id: str): - """Handle SYNC action.""" - devices = [] - for entity in hass.states.async_all(): - if not self.is_entity_exposed(entity): - continue + gass_config = Config(is_exposed, agent_user_id, entity_config) + hass.http.register_view( + GoogleAssistantView(access_token, gass_config)) - device = entity_to_device(entity, hass.config.units) - if device is None: - _LOGGER.warning("No mapping for %s domain", entity.domain) - continue - devices.append(device) +class GoogleAssistantView(HomeAssistantView): + """Handle Google Assistant requests.""" - return self.json( - _make_actions_response(request_id, - {'agentUserId': self.agent_user_id, - 'devices': devices})) + url = GOOGLE_ASSISTANT_API_ENDPOINT + name = 'api:google_assistant' + requires_auth = False # Uses access token from oauth flow - @asyncio.coroutine - def handle_query(self, - hass: HomeAssistant, - request_id: str, - requested_devices: list): - """Handle the QUERY action.""" - devices = {} - for device in requested_devices: - devid = device.get('id') - # In theory this should never happpen - if not devid: - _LOGGER.error('Device missing ID: %s', device) - continue - - state = hass.states.get(devid) - if not state: - # If we can't find a state, the device is offline - devices[devid] = {'online': False} - - devices[devid] = query_device(state, hass.config.units) - - return self.json( - _make_actions_response(request_id, {'devices': devices})) - - @asyncio.coroutine - def handle_execute(self, - hass: HomeAssistant, - request_id: str, - requested_commands: list): - """Handle the EXECUTE action.""" - commands = [] - for command in requested_commands: - ent_ids = [ent.get('id') for ent in command.get('devices', [])] - for execution in command.get('execution'): - for eid in ent_ids: - success = False - domain = eid.split('.')[0] - (service, service_data) = determine_service( - eid, execution.get('command'), execution.get('params'), - hass.config.units) - if domain == "group": - domain = "homeassistant" - success = yield from hass.services.async_call( - domain, service, service_data, blocking=True) - result = {"ids": [eid], "states": {}} - if success: - result['status'] = 'SUCCESS' - else: - result['status'] = 'ERROR' - commands.append(result) - - return self.json( - _make_actions_response(request_id, {'commands': commands})) + def __init__(self, access_token, gass_config): + """Initialize the Google Assistant request handler.""" + self.access_token = access_token + self.gass_config = gass_config @asyncio.coroutine def post(self, request: Request) -> Response: @@ -155,35 +86,7 @@ class GoogleAssistantView(HomeAssistantView): return self.json_message( "missing authorization", status_code=HTTP_UNAUTHORIZED) - data = yield from request.json() # type: dict - - inputs = data.get('inputs') # type: list - if len(inputs) != 1: - _LOGGER.error('Too many inputs in request %d', len(inputs)) - return self.json_message( - "too many inputs", status_code=HTTP_BAD_REQUEST) - - request_id = data.get('requestId') # type: str - intent = inputs[0].get('intent') - payload = inputs[0].get('payload') - - hass = request.app['hass'] # type: HomeAssistant - res = None - if intent == 'action.devices.SYNC': - res = yield from self.handle_sync(hass, request_id) - elif intent == 'action.devices.QUERY': - res = yield from self.handle_query(hass, request_id, - payload.get('devices', [])) - elif intent == 'action.devices.EXECUTE': - res = yield from self.handle_execute(hass, request_id, - payload.get('commands', [])) - - if res: - return res - - return self.json_message( - "invalid intent", status_code=HTTP_BAD_REQUEST) - - -def _make_actions_response(request_id: str, payload: dict) -> dict: - return {'requestId': request_id, 'payload': payload} + message = yield from request.json() # type: dict + result = yield from async_handle_message( + request.app['hass'], self.gass_config, message) + return self.json(result) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 23876a068f9..0faa9bdc484 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,4 +1,5 @@ """Support for Google Assistant Smart Home API.""" +import asyncio import logging # Typing imports @@ -10,12 +11,13 @@ from homeassistant.helpers.entity import Entity # NOQA from homeassistant.core import HomeAssistant # NOQA from homeassistant.util import color from homeassistant.util.unit_system import UnitSystem # NOQA +from homeassistant.util.decorator import Registry from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, - CONF_FRIENDLY_NAME, STATE_OFF, - SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS, + CONF_NAME, CONF_TYPE ) from homeassistant.components import ( switch, light, cover, media_player, group, fan, scene, script, climate @@ -23,8 +25,7 @@ from homeassistant.components import ( from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( - ATTR_GOOGLE_ASSISTANT_NAME, COMMAND_COLOR, - ATTR_GOOGLE_ASSISTANT_TYPE, + COMMAND_COLOR, COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE, COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE, @@ -34,6 +35,7 @@ from .const import ( CONF_ALIASES, CLIMATE_SUPPORTED_MODES ) +HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) # Mapping is [actions schema, primary trait, optional features] @@ -52,12 +54,12 @@ MAPPING_COMPONENT = { } ], cover.DOMAIN: [ - TYPE_LIGHT, TRAIT_ONOFF, { + TYPE_SWITCH, TRAIT_ONOFF, { cover.SUPPORT_SET_POSITION: TRAIT_BRIGHTNESS } ], media_player.DOMAIN: [ - TYPE_LIGHT, TRAIT_ONOFF, { + TYPE_SWITCH, TRAIT_ONOFF, { media_player.SUPPORT_VOLUME_SET: TRAIT_BRIGHTNESS } ], @@ -65,15 +67,22 @@ MAPPING_COMPONENT = { } # type: Dict[str, list] -def make_actions_response(request_id: str, payload: dict) -> dict: - """Make response message.""" - return {'requestId': request_id, 'payload': payload} +class Config: + """Hold the configuration for Google Assistant.""" + + def __init__(self, should_expose, agent_user_id, entity_config=None): + """Initialize the configuration.""" + self.should_expose = should_expose + self.agent_user_id = agent_user_id + self.entity_config = entity_config or {} -def entity_to_device(entity: Entity, units: UnitSystem): +def entity_to_device(entity: Entity, config: Config, units: UnitSystem): """Convert a hass entity into an google actions device.""" + entity_config = config.entity_config.get(entity.entity_id, {}) class_data = MAPPING_COMPONENT.get( - entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain) + entity_config.get(CONF_TYPE) or entity.domain) + if class_data is None: return None @@ -88,17 +97,12 @@ def entity_to_device(entity: Entity, units: UnitSystem): device['traits'].append(class_data[1]) # handle custom names - device['name']['name'] = \ - entity.attributes.get(ATTR_GOOGLE_ASSISTANT_NAME) or \ - entity.attributes.get(CONF_FRIENDLY_NAME) + device['name']['name'] = entity_config.get(CONF_NAME) or entity.name # use aliases - aliases = entity.attributes.get(CONF_ALIASES) + aliases = entity_config.get(CONF_ALIASES) if aliases: - if isinstance(aliases, list): - device['name']['nicknames'] = aliases - else: - _LOGGER.warning("%s must be a list", CONF_ALIASES) + device['name']['nicknames'] = aliases # add trait if entity supports feature if class_data[2]: @@ -286,3 +290,98 @@ def determine_service( return (SERVICE_TURN_OFF, service_data) return (None, service_data) + + +@asyncio.coroutine +def async_handle_message(hass, config, message): + """Handle incoming API messages.""" + request_id = message.get('requestId') # type: str + inputs = message.get('inputs') # type: list + + if len(inputs) > 1: + _LOGGER.warning('Got unexpected more than 1 input. %s', message) + + # Only use first input + intent = inputs[0].get('intent') + payload = inputs[0].get('payload') + + handler = HANDLERS.get(intent) + + if handler: + result = yield from handler(hass, config, payload) + else: + result = {'errorCode': 'protocolError'} + + return {'requestId': request_id, 'payload': result} + + +@HANDLERS.register('action.devices.SYNC') +@asyncio.coroutine +def async_devices_sync(hass, config, payload): + """Handle action.devices.SYNC request.""" + devices = [] + for entity in hass.states.async_all(): + if not config.should_expose(entity): + continue + + device = entity_to_device(entity, config, hass.config.units) + if device is None: + _LOGGER.warning("No mapping for %s domain", entity.domain) + continue + + devices.append(device) + + return { + 'agentUserId': config.agent_user_id, + 'devices': devices, + } + + +@HANDLERS.register('action.devices.QUERY') +@asyncio.coroutine +def async_devices_query(hass, config, payload): + """Handle action.devices.QUERY request.""" + devices = {} + for device in payload.get('devices', []): + devid = device.get('id') + # In theory this should never happpen + if not devid: + _LOGGER.error('Device missing ID: %s', device) + continue + + state = hass.states.get(devid) + if not state: + # If we can't find a state, the device is offline + devices[devid] = {'online': False} + + devices[devid] = query_device(state, hass.config.units) + + return {'devices': devices} + + +@HANDLERS.register('action.devices.EXECUTE') +@asyncio.coroutine +def handle_devices_execute(hass, config, payload): + """Handle action.devices.EXECUTE request.""" + commands = [] + for command in payload.get('commands', []): + ent_ids = [ent.get('id') for ent in command.get('devices', [])] + for execution in command.get('execution'): + for eid in ent_ids: + success = False + domain = eid.split('.')[0] + (service, service_data) = determine_service( + eid, execution.get('command'), execution.get('params'), + hass.config.units) + if domain == "group": + domain = "homeassistant" + success = yield from hass.services.async_call( + domain, service, service_data, blocking=True) + result = {"ids": [eid], "states": {}} + if success: + result['status'] = 'SUCCESS' + else: + result['status'] = 'ERROR' + commands.append(result) + + return {'commands': commands} diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 0bc1fa46c4c..8b1e05e3122 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -6,11 +6,10 @@ https://home-assistant.io/components/group/ """ import asyncio import logging -import os import voluptuous as vol -from homeassistant import config as conf_util, core as ha +from homeassistant import core as ha from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, @@ -43,6 +42,8 @@ ATTR_ORDER = 'order' ATTR_VIEW = 'view' ATTR_VISIBLE = 'visible' +DATA_ALL_GROUPS = 'data_all_groups' + SERVICE_SET_VISIBILITY = 'set_visibility' SERVICE_SET = 'set' SERVICE_REMOVE = 'remove' @@ -146,7 +147,7 @@ def set_visibility(hass, entity_id=None, visible=True): @bind_hass def set_group(hass, object_id, name=None, entity_ids=None, visible=None, icon=None, view=None, control=None, add=None): - """Create a new user group.""" + """Create/Update a group.""" hass.add_job( async_set_group, hass, object_id, name, entity_ids, visible, icon, view, control, add) @@ -156,7 +157,7 @@ def set_group(hass, object_id, name=None, entity_ids=None, visible=None, @bind_hass def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None, icon=None, view=None, control=None, add=None): - """Create a new user group.""" + """Create/Update a group.""" data = { key: value for key, value in [ (ATTR_OBJECT_ID, object_id), @@ -250,15 +251,10 @@ def get_entity_ids(hass, entity_id, domain_filter=None): def async_setup(hass, config): """Set up all groups found definded in the configuration.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - service_groups = {} + hass.data[DATA_ALL_GROUPS] = {} 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 def reload_service_handler(service): """Remove all groups and load new ones from config.""" @@ -269,12 +265,13 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, - descriptions[SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA) + schema=RELOAD_SERVICE_SCHEMA) @asyncio.coroutine def groups_service_handler(service): """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] + service_groups = hass.data[DATA_ALL_GROUPS] # new group if service.service == SERVICE_SET and object_id not in service_groups: @@ -285,7 +282,7 @@ def async_setup(hass, config): ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL ) if service.data.get(attr) is not None} - new_group = yield from Group.async_create_group( + yield from Group.async_create_group( hass, service.data.get(ATTR_NAME, object_id), object_id=object_id, entity_ids=entity_ids, @@ -293,7 +290,6 @@ def async_setup(hass, config): **extra_arg ) - service_groups[object_id] = new_group return # update group @@ -346,11 +342,11 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET, groups_service_handler, - descriptions[SERVICE_SET], schema=SET_SERVICE_SCHEMA) + schema=SET_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_REMOVE, groups_service_handler, - descriptions[SERVICE_REMOVE], schema=REMOVE_SERVICE_SCHEMA) + schema=REMOVE_SERVICE_SCHEMA) @asyncio.coroutine def visibility_service_handler(service): @@ -368,7 +364,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, - descriptions[SERVICE_SET_VISIBILITY], schema=SET_VISIBILITY_SERVICE_SCHEMA) return True @@ -456,6 +451,11 @@ class Group(Entity): else: yield from group.async_update_ha_state(True) + # If called before the platform async_setup is called (test cases) + if DATA_ALL_GROUPS not in hass.data: + hass.data[DATA_ALL_GROUPS] = {} + + hass.data[DATA_ALL_GROUPS][object_id] = group return group @property diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py index 048a7d531f4..cc6db5fbab3 100644 --- a/homeassistant/components/hassio.py +++ b/homeassistant/components/hassio.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/hassio/ """ import asyncio +from datetime import timedelta import logging import os import re @@ -16,28 +17,46 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.const import ( - CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, CONF_TIME_ZONE) + CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, CONF_TIME_ZONE, + SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) +from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.components.http import ( HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT, - CONF_SSL_CERTIFICATE) -from homeassistant.helpers.aiohttp_client import async_get_clientsession + CONF_SERVER_HOST, CONF_SSL_CERTIFICATE) +from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) DOMAIN = 'hassio' DEPENDENCIES = ['http'] +X_HASSIO = 'X-HASSIO-KEY' + +DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' +HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) + SERVICE_ADDON_START = 'addon_start' SERVICE_ADDON_STOP = 'addon_stop' SERVICE_ADDON_RESTART = 'addon_restart' SERVICE_ADDON_STDIN = 'addon_stdin' SERVICE_HOST_SHUTDOWN = 'host_shutdown' SERVICE_HOST_REBOOT = 'host_reboot' +SERVICE_SNAPSHOT_FULL = 'snapshot_full' +SERVICE_SNAPSHOT_PARTIAL = 'snapshot_partial' +SERVICE_RESTORE_FULL = 'restore_full' +SERVICE_RESTORE_PARTIAL = 'restore_partial' ATTR_ADDON = 'addon' ATTR_INPUT = 'input' +ATTR_SNAPSHOT = 'snapshot' +ATTR_ADDONS = 'addons' +ATTR_FOLDERS = 'folders' +ATTR_HOMEASSISTANT = 'homeassistant' +ATTR_NAME = 'name' NO_TIMEOUT = { re.compile(r'^homeassistant/update$'), @@ -45,13 +64,17 @@ NO_TIMEOUT = { re.compile(r'^supervisor/update$'), re.compile(r'^addons/[^/]*/update$'), re.compile(r'^addons/[^/]*/install$'), - re.compile(r'^addons/[^/]*/rebuild$') + re.compile(r'^addons/[^/]*/rebuild$'), + re.compile(r'^snapshots/.*/full$'), + re.compile(r'^snapshots/.*/partial$'), } NO_AUTH = { re.compile(r'^panel_(es5|latest)$'), re.compile(r'^addons/[^/]*/logo$') } +SCHEMA_NO_DATA = vol.Schema({}) + SCHEMA_ADDON = vol.Schema({ vol.Required(ATTR_ADDON): cv.slug, }) @@ -60,16 +83,80 @@ SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend({ vol.Required(ATTR_INPUT): vol.Any(dict, cv.string) }) +SCHEMA_SNAPSHOT_FULL = vol.Schema({ + vol.Optional(ATTR_NAME): cv.string, +}) + +SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({ + vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), +}) + +SCHEMA_RESTORE_FULL = vol.Schema({ + vol.Required(ATTR_SNAPSHOT): cv.slug, +}) + +SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend({ + vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, + vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), +}) + MAP_SERVICE_API = { - SERVICE_ADDON_START: ('/addons/{addon}/start', SCHEMA_ADDON), - SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON), - SERVICE_ADDON_RESTART: ('/addons/{addon}/restart', SCHEMA_ADDON), - SERVICE_ADDON_STDIN: ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN), - SERVICE_HOST_SHUTDOWN: ('/host/shutdown', None), - SERVICE_HOST_REBOOT: ('/host/reboot', None), + SERVICE_ADDON_START: ('/addons/{addon}/start', SCHEMA_ADDON, 60, False), + SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON, 60, False), + SERVICE_ADDON_RESTART: + ('/addons/{addon}/restart', SCHEMA_ADDON, 60, False), + SERVICE_ADDON_STDIN: + ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN, 60, False), + SERVICE_HOST_SHUTDOWN: ('/host/shutdown', SCHEMA_NO_DATA, 60, False), + SERVICE_HOST_REBOOT: ('/host/reboot', SCHEMA_NO_DATA, 60, False), + SERVICE_SNAPSHOT_FULL: + ('/snapshots/new/full', SCHEMA_SNAPSHOT_FULL, 300, True), + SERVICE_SNAPSHOT_PARTIAL: + ('/snapshots/new/partial', SCHEMA_SNAPSHOT_PARTIAL, 300, True), + SERVICE_RESTORE_FULL: + ('/snapshots/{snapshot}/restore/full', SCHEMA_RESTORE_FULL, 300, True), + SERVICE_RESTORE_PARTIAL: + ('/snapshots/{snapshot}/restore/partial', SCHEMA_RESTORE_PARTIAL, 300, + True), } +@callback +@bind_hass +def get_homeassistant_version(hass): + """Return latest available HomeAssistant version. + + Async friendly. + """ + return hass.data.get(DATA_HOMEASSISTANT_VERSION) + + +@callback +@bind_hass +def is_hassio(hass): + """Return True if hass.io is loaded. + + Async friendly. + """ + return DOMAIN in hass.config.components + + +@bind_hass +@asyncio.coroutine +def async_check_config(hass): + """Check config over Hass.io API.""" + result = yield from hass.data[DOMAIN].send_command( + '/homeassistant/check', timeout=300) + + if not result: + return "Hass.io config check API error" + elif result['result'] == "error": + return result['message'] + return None + + @asyncio.coroutine def async_setup(hass, config): """Set up the HASSio component.""" @@ -79,8 +166,8 @@ def async_setup(hass, config): _LOGGER.error("No HassIO supervisor detect!") return False - websession = async_get_clientsession(hass) - hassio = HassIO(hass.loop, websession, host) + websession = hass.helpers.aiohttp_client.async_get_clientsession() + hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) if not (yield from hassio.is_connected()): _LOGGER.error("Not connected with HassIO!") @@ -102,19 +189,82 @@ def async_setup(hass, config): def async_service_handler(service): """Handle service calls for HassIO.""" api_command = MAP_SERVICE_API[service.service][0] - addon = service.data.get(ATTR_ADDON) - data = service.data[ATTR_INPUT] if ATTR_INPUT in service.data else None + data = service.data.copy() + addon = data.pop(ATTR_ADDON, None) + snapshot = data.pop(ATTR_SNAPSHOT, None) + payload = None - yield from hassio.send_command( - api_command.format(addon=addon), payload=data, timeout=60) + # Pass data to hass.io API + if service.service == SERVICE_ADDON_STDIN: + payload = data[ATTR_INPUT] + elif MAP_SERVICE_API[service.service][3]: + payload = data + + # Call API + ret = yield from hassio.send_command( + api_command.format(addon=addon, snapshot=snapshot), + payload=payload, timeout=MAP_SERVICE_API[service.service][2] + ) + + if not ret or ret['result'] != "ok": + _LOGGER.error("Error on Hass.io API: %s", ret['message']) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( DOMAIN, service, async_service_handler, schema=settings[1]) + @asyncio.coroutine + def update_homeassistant_version(now): + """Update last available HomeAssistant version.""" + data = yield from hassio.get_homeassistant_info() + if data: + hass.data[DATA_HOMEASSISTANT_VERSION] = \ + data['data']['last_version'] + + hass.helpers.event.async_track_point_in_utc_time( + update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL) + + # Fetch last version + yield from update_homeassistant_version(None) + + @asyncio.coroutine + def async_handle_core_service(call): + """Service handler for handling core services.""" + if call.service == SERVICE_HOMEASSISTANT_STOP: + yield from hassio.send_command('/homeassistant/stop') + return + + error = yield from async_check_config(hass) + if error: + _LOGGER.error(error) + hass.components.persistent_notification.async_create( + "Config error. See dev-info panel for details.", + "Config validating", "{0}.check_config".format(HASS_DOMAIN)) + return + + if call.service == SERVICE_HOMEASSISTANT_RESTART: + yield from hassio.send_command('/homeassistant/restart') + + # Mock core services + for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, + SERVICE_CHECK_CONFIG): + hass.services.async_register( + HASS_DOMAIN, service, async_handle_core_service) + return True +def _api_bool(funct): + """API wrapper to return Boolean.""" + @asyncio.coroutine + def _wrapper(*argv, **kwargs): + """Wrapper function.""" + data = yield from funct(*argv, **kwargs) + return data and data['result'] == "ok" + + return _wrapper + + class HassIO(object): """Small API wrapper for HassIO.""" @@ -124,6 +274,7 @@ class HassIO(object): self.websession = websession self._ip = ip + @_api_bool def is_connected(self): """Return True if it connected to HassIO supervisor. @@ -131,6 +282,14 @@ class HassIO(object): """ return self.send_command("/supervisor/ping", method="get") + def get_homeassistant_info(self): + """Return data for HomeAssistant. + + This method return a coroutine. + """ + return self.send_command("/homeassistant/info", method="get") + + @_api_bool def update_hass_api(self, http_config): """Update Home-Assistant API data on HassIO. @@ -141,10 +300,16 @@ class HassIO(object): 'ssl': CONF_SSL_CERTIFICATE in http_config, 'port': port, 'password': http_config.get(CONF_API_PASSWORD), + 'watchdog': True, } + if CONF_SERVER_HOST in http_config: + options['watchdog'] = False + _LOGGER.warning("Don't use 'server_host' options with Hass.io!") + return self.send_command("/homeassistant/options", payload=options) + @_api_bool def update_hass_timezone(self, core_config): """Update Home-Assistant timezone data on HassIO. @@ -164,15 +329,17 @@ class HassIO(object): with async_timeout.timeout(timeout, loop=self.loop): request = yield from self.websession.request( method, "http://{}{}".format(self._ip, command), - json=payload) + json=payload, headers={ + X_HASSIO: os.environ.get('HASSIO_TOKEN') + }) - if request.status != 200: + if request.status not in (200, 400): _LOGGER.error( "%s return code %d.", command, request.status) - return False + return None answer = yield from request.json() - return answer and answer['result'] == 'ok' + return answer except asyncio.TimeoutError: _LOGGER.error("Timeout on %s request", command) @@ -180,7 +347,7 @@ class HassIO(object): except aiohttp.ClientError as err: _LOGGER.error("Client error on %s request %s", command, err) - return False + return None @asyncio.coroutine def command_proxy(self, path, request): @@ -192,11 +359,11 @@ class HassIO(object): try: data = None - headers = None + headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN')} with async_timeout.timeout(10, loop=self.loop): data = yield from request.read() if data: - headers = {CONTENT_TYPE: request.content_type} + headers[CONTENT_TYPE] = request.content_type else: data = None diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index b4233f1ac82..f94dd8816a7 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/hdmi_cec/ """ import logging import multiprocessing -import os from collections import defaultdict from functools import reduce @@ -16,7 +15,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER from homeassistant.components.switch import DOMAIN as SWITCH -from homeassistant.config import load_yaml_config_file from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, STATE_ON, STATE_OFF, CONF_DEVICES, CONF_PLATFORM, @@ -301,17 +299,12 @@ def setup(hass: HomeAssistant, base_config): def _start_cec(event): """Register services and start HDMI network to watch for devices.""" - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml'))[DOMAIN] hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx, - descriptions[SERVICE_SEND_COMMAND], SERVICE_SEND_COMMAND_SCHEMA) hass.services.register(DOMAIN, SERVICE_VOLUME, _volume, - descriptions[SERVICE_VOLUME], - SERVICE_VOLUME_SCHEMA) + schema=SERVICE_VOLUME_SCHEMA) hass.services.register(DOMAIN, SERVICE_UPDATE_DEVICES, _update, - descriptions[SERVICE_UPDATE_DEVICES], - SERVICE_UPDATE_DEVICES_SCHEMA) + schema=SERVICE_UPDATE_DEVICES_SCHEMA) hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, _select_device) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 55858dbe765..8f96d95521d 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -304,7 +304,20 @@ class HistoryPeriodView(HomeAssistantView): elapsed = time.perf_counter() - timer_start _LOGGER.debug( 'Extracted %d states in %fs', sum(map(len, result)), elapsed) - return self.json(result) + + # Reorder the result to respect the ordering given by any + # entities explicitly included in the configuration. + + sorted_result = [] + for order_entity in self.filters.included_entities: + for state_list in result: + if state_list[0].entity_id == order_entity: + sorted_result.append(state_list) + result.remove(state_list) + break + sorted_result.extend(result) + + return self.json(sorted_result) class Filters(object): diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive.py index 277800502c1..bf5196d6582 100644 --- a/homeassistant/components/hive.py +++ b/homeassistant/components/hive.py @@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ['pyhiveapi==0.2.5'] +REQUIREMENTS = ['pyhiveapi==0.2.10'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'hive' diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 46f25e4e05f..b2f6384d467 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -8,12 +8,10 @@ import asyncio from datetime import timedelta from functools import partial import logging -import os import socket import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN) @@ -22,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.36'] +REQUIREMENTS = ['pyhomematic==0.1.37'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) @@ -77,10 +75,10 @@ HM_DEVICE_TYPES = { 'ThermostatGroup'], DISCOVER_BINARY_SENSORS: [ 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', - 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', - 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', - 'PresenceIP'], - DISCOVER_COVER: ['Blind', 'KeyBlind'] + 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', + 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', + 'WiredSensor', 'PresenceIP'], + DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -90,6 +88,7 @@ HM_IGNORE_DISCOVERY_NODE = [ HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], + 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], @@ -105,6 +104,7 @@ HM_ATTRIBUTE_SUPPORT = { 'POWER': ['power', {}], 'CURRENT': ['current', {}], 'VOLTAGE': ['voltage', {}], + 'OPERATING_VOLTAGE': ['voltage', {}], 'WORKING': ['working', {0: 'No', 1: 'Yes'}], } @@ -328,10 +328,6 @@ def setup(hass, config): for hub_name in conf[CONF_HOSTS].keys(): entity_hubs.append(HMHub(hass, homematic, hub_name)) - # Register HomeMatic services - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - def _hm_service_virtualkey(service): """Service to handle virtualkey servicecalls.""" address = service.data.get(ATTR_ADDRESS) @@ -360,7 +356,7 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey, - descriptions[SERVICE_VIRTUALKEY], schema=SCHEMA_SERVICE_VIRTUALKEY) + schema=SCHEMA_SERVICE_VIRTUALKEY) def _service_handle_value(service): """Service to call setValue method for HomeMatic system variable.""" @@ -383,7 +379,6 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_SET_VARIABLE_VALUE, _service_handle_value, - descriptions[SERVICE_SET_VARIABLE_VALUE], schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE) def _service_handle_reconnect(service): @@ -392,7 +387,7 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect, - descriptions[SERVICE_RECONNECT], schema=SCHEMA_SERVICE_RECONNECT) + schema=SCHEMA_SERVICE_RECONNECT) def _service_handle_device(service): """Service to call setValue method for HomeMatic devices.""" @@ -411,7 +406,6 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_SET_DEVICE_VALUE, _service_handle_device, - descriptions[SERVICE_SET_DEVICE_VALUE], schema=SCHEMA_SERVICE_SET_DEVICE_VALUE) def _service_handle_install_mode(service): @@ -425,7 +419,6 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode, - descriptions[SERVICE_SET_INSTALL_MODE], schema=SCHEMA_SERVICE_SET_INSTALL_MODE) return True diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 17ceccfd218..33f97395945 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -36,7 +36,7 @@ from .static import ( CachingFileResponse, CachingStaticResource, staticresource_middleware) from .util import get_real_ip -REQUIREMENTS = ['aiohttp_cors==0.5.3'] +REQUIREMENTS = ['aiohttp_cors==0.6.0'] ALLOWED_CORS_HEADERS = [ ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index 6147f706658..a83b55e84e5 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -12,7 +12,6 @@ import socket import voluptuous as vol from homeassistant.components.discovery import SERVICE_HUE -from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_FILENAME, CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery @@ -153,6 +152,7 @@ class HueBridge(object): allow_in_emulated_hue=True, allow_hue_groups=True): """Initialize the system.""" self.host = host + self.bridge_id = socket.gethostbyname(host) self.hass = hass self.filename = filename self.allow_unreachable = allow_unreachable @@ -166,7 +166,7 @@ class HueBridge(object): self.configured = False self.config_request_id = None - hass.data[DOMAIN][socket.gethostbyname(host)] = self + hass.data[DOMAIN][self.bridge_id] = self def setup(self): """Set up a phue bridge based on host parameter.""" @@ -197,7 +197,7 @@ class HueBridge(object): discovery.load_platform( self.hass, 'light', DOMAIN, - {'bridge_id': socket.gethostbyname(self.host)}) + {'bridge_id': self.bridge_id}) # create a service for calling run_scene directly on the bridge, # used to simplify automation rules. @@ -207,11 +207,8 @@ class HueBridge(object): scene_name = call.data[ATTR_SCENE_NAME] self.bridge.run_scene(group_name, scene_name) - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) self.hass.services.register( DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, - descriptions.get(SERVICE_HUE_SCENE), schema=SCENE_SCHEMA) def request_configuration(self): diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index e6979087b6f..646bfcf421f 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -7,12 +7,10 @@ https://home-assistant.io/components/image_processing/ import asyncio from datetime import timedelta import logging -import os import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) from homeassistant.exceptions import HomeAssistantError @@ -74,10 +72,6 @@ def async_setup(hass, 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 def async_scan_service(service): """Service handler for scan.""" @@ -90,7 +84,7 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_SCAN, async_scan_service, - descriptions.get(SERVICE_SCAN), schema=SERVICE_SCAN_SCHEMA) + schema=SERVICE_SCAN_SCHEMA) return True diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 56a4ac50bd7..0abc449afba 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -8,16 +8,15 @@ from datetime import timedelta import logging import requests - import voluptuous as vol -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, PLATFORM_SCHEMA, + CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity) +from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.13.3'] +REQUIREMENTS = ['numpy==1.14.0'] _LOGGER = logging.getLogger(__name__) @@ -73,7 +72,7 @@ def _create_processor_from_config(hass, camera_entity, config): def _get_default_classifier(dest_path): """Download the default OpenCV classifier.""" - _LOGGER.info('Downloading default classifier') + _LOGGER.info("Downloading default classifier") req = requests.get(CASCADE_URL, stream=True) with open(dest_path, 'wb') as fil: for chunk in req.iter_content(chunk_size=1024): @@ -84,14 +83,13 @@ def _get_default_classifier(dest_path): def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the OpenCV image processing platform.""" try: - # Verify opencv python package is preinstalled + # Verify that the OpenCV python package is pre-installed # pylint: disable=unused-import,unused-variable import cv2 # noqa except ImportError: - _LOGGER.error("No opencv library found! " + - "Install or compile for your system " + - "following instructions here: " + - "http://opencv.org/releases.html") + _LOGGER.error( + "No OpenCV library found! Install or compile for your system " + "following instructions here: http://opencv.org/releases.html") return entities = [] @@ -105,8 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for camera in config[CONF_SOURCE]: entities.append(OpenCVImageProcessor( hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), - config[CONF_CLASSIFIER] - )) + config[CONF_CLASSIFIER])) add_devices(entities) @@ -121,8 +118,7 @@ class OpenCVImageProcessor(ImageProcessingEntity): if name: self._name = name else: - self._name = "OpenCV {0}".format( - split_entity_id(camera_entity)[1]) + self._name = "OpenCV {0}".format(split_entity_id(camera_entity)[1]) self._classifiers = classifiers self._matches = {} self._total_matches = 0 @@ -157,8 +153,8 @@ class OpenCVImageProcessor(ImageProcessingEntity): import numpy # pylint: disable=no-member - cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), - cv2.IMREAD_UNCHANGED) + cv_image = cv2.imdecode( + numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) for name, classifier in self._classifiers.items(): scale = DEFAULT_SCALE diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index e60f44e8ea0..43feeb8c4f4 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -6,7 +6,6 @@ at https://home-assistant.io/components/input_boolean/ """ import asyncio import logging -import os import voluptuous as vol @@ -15,7 +14,6 @@ from homeassistant.const import ( SERVICE_TOGGLE, STATE_ON) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -27,7 +25,6 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' _LOGGER = logging.getLogger(__name__) CONF_INITIAL = 'initial' -DEFAULT_INITIAL = False SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -104,22 +101,14 @@ def async_setup(hass, config): if tasks: 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( DOMAIN, SERVICE_TURN_OFF, async_handler_service, - descriptions[DOMAIN][SERVICE_TURN_OFF], schema=SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TURN_ON, async_handler_service, - descriptions[DOMAIN][SERVICE_TURN_ON], schema=SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TOGGLE, async_handler_service, - descriptions[DOMAIN][SERVICE_TOGGLE], schema=SERVICE_SCHEMA) yield from component.async_add_entities(entities) diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index 856cdac1e4b..e18169fca73 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -4,14 +4,12 @@ Component to offer a way to set a numeric value from a slider or text box. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_number/ """ -import os import asyncio import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) from homeassistant.helpers.entity import Entity @@ -165,14 +163,9 @@ def async_setup(hass, config): if update_tasks: 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, data in SERVICE_TO_METHOD.items(): hass.services.async_register( - DOMAIN, service, async_handle_service, - description=descriptions[DOMAIN][service], schema=data['schema']) + DOMAIN, service, async_handle_service, schema=data['schema']) yield from component.async_add_entities(entities) return True diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py old mode 100755 new mode 100644 index a9df7c15ea3..583181fe453 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -4,14 +4,12 @@ Component to offer a way to enter a value into a text box. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_text/ """ -import os import asyncio import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME) from homeassistant.loader import bind_hass @@ -112,13 +110,8 @@ def async_setup(hass, config): if tasks: 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( DOMAIN, SERVICE_SET_VALUE, async_set_value_service, - description=descriptions[DOMAIN][SERVICE_SET_VALUE], schema=SERVICE_SET_VALUE_SCHEMA) yield from component.async_add_entities(entities) diff --git a/homeassistant/components/insteon_local.py b/homeassistant/components/insteon_local.py index 711dafb6b73..dbe8597be3d 100644 --- a/homeassistant/components/insteon_local.py +++ b/homeassistant/components/insteon_local.py @@ -13,8 +13,9 @@ import voluptuous as vol from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ['insteonlocal==0.52'] +REQUIREMENTS = ['insteonlocal==0.53'] _LOGGER = logging.getLogger(__name__) @@ -22,6 +23,14 @@ DEFAULT_PORT = 25105 DEFAULT_TIMEOUT = 10 DOMAIN = 'insteon_local' +INSTEON_CACHE = '.insteon_local_cache' + +INSTEON_PLATFORMS = [ + 'light', + 'switch', + 'fan', +] + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, @@ -34,12 +43,8 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Set up the Insteon Hub component. - - This will automatically import associated lights. - """ + """Setup insteon hub.""" from insteonlocal.Hub import Hub - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) @@ -48,21 +53,23 @@ def setup(hass, config): timeout = conf.get(CONF_TIMEOUT) try: - if not os.path.exists(hass.config.path('.insteon_cache')): - os.makedirs(hass.config.path('.insteon_cache')) + if not os.path.exists(hass.config.path(INSTEON_CACHE)): + os.makedirs(hass.config.path(INSTEON_CACHE)) insteonhub = Hub(host, username, password, port, timeout, _LOGGER, - hass.config.path('.insteon_cache')) + hass.config.path(INSTEON_CACHE)) # Check for successful connection insteonhub.get_buffer_status() except requests.exceptions.ConnectTimeout: - _LOGGER.error("Error on insteon_local." - "Could not connect. Check config", exc_info=True) + _LOGGER.error( + "Could not connect. Check config", + exc_info=True) return False except requests.exceptions.ConnectionError: - _LOGGER.error("Error on insteon_local. Could not connect." - "Check config", exc_info=True) + _LOGGER.error( + "Could not connect. Check config", + exc_info=True) return False except requests.exceptions.RequestException: if insteonhub.http_code == 401: @@ -71,6 +78,12 @@ def setup(hass, config): _LOGGER.error("Error on insteon_local hub check", exc_info=True) return False + linked = insteonhub.get_linked() + hass.data['insteon_local'] = insteonhub + for insteon_platform in INSTEON_PLATFORMS: + load_platform(hass, insteon_platform, DOMAIN, {'linked': linked}, + config) + return True diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index af1846c7bf8..28cfac39154 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -24,15 +24,14 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'isy994' -CONF_HIDDEN_STRING = 'hidden_string' +CONF_IGNORE_STRING = 'ignore_string' CONF_SENSOR_STRING = 'sensor_string' +CONF_ENABLE_CLIMATE = 'enable_climate' CONF_TLS_VER = 'tls' -DEFAULT_HIDDEN_STRING = '{HIDE ME}' +DEFAULT_IGNORE_STRING = '{IGNORE ME}' DEFAULT_SENSOR_STRING = 'sensor' -ISY = None - KEY_ACTIONS = 'actions' KEY_FOLDER = 'folder' KEY_MY_PROGRAMS = 'My Programs' @@ -44,190 +43,344 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_TLS_VER): vol.Coerce(float), - vol.Optional(CONF_HIDDEN_STRING, - default=DEFAULT_HIDDEN_STRING): cv.string, + vol.Optional(CONF_IGNORE_STRING, + default=DEFAULT_IGNORE_STRING): cv.string, vol.Optional(CONF_SENSOR_STRING, - default=DEFAULT_SENSOR_STRING): cv.string + default=DEFAULT_SENSOR_STRING): cv.string, + vol.Optional(CONF_ENABLE_CLIMATE, + default=True): cv.boolean }) }, extra=vol.ALLOW_EXTRA) -SENSOR_NODES = [] -WEATHER_NODES = [] -NODES = [] -GROUPS = [] -PROGRAMS = {} +# Do not use the Hass consts for the states here - we're matching exact API +# responses, not using them for Hass states +NODE_FILTERS = { + 'binary_sensor': { + 'uom': [], + 'states': [], + 'node_def_id': ['BinaryAlarm'], + 'insteon_type': ['16.'] # Does a startswith() match; include the dot + }, + 'sensor': { + # This is just a more-readable way of including MOST uoms between 1-100 + # (Remember that range() is non-inclusive of the stop value) + 'uom': (['1'] + + list(map(str, range(3, 11))) + + list(map(str, range(12, 51))) + + list(map(str, range(52, 66))) + + list(map(str, range(69, 78))) + + ['79'] + + list(map(str, range(82, 97)))), + 'states': [], + 'node_def_id': ['IMETER_SOLO'], + 'insteon_type': ['9.0.', '9.7.'] + }, + 'lock': { + 'uom': ['11'], + 'states': ['locked', 'unlocked'], + 'node_def_id': ['DoorLock'], + 'insteon_type': ['15.'] + }, + 'fan': { + 'uom': [], + 'states': ['on', 'off', 'low', 'medium', 'high'], + 'node_def_id': ['FanLincMotor'], + 'insteon_type': ['1.46.'] + }, + 'cover': { + 'uom': ['97'], + 'states': ['open', 'closed', 'closing', 'opening', 'stopped'], + 'node_def_id': [], + 'insteon_type': [] + }, + 'light': { + 'uom': ['51'], + 'states': ['on', 'off', '%'], + 'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV', + 'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV', + 'DimmerLampOnly', 'BallastRelayLampSwitch', + 'BallastRelayLampSwitch_ADV', 'RelayLampSwitch', + 'RemoteLinc2', 'RemoteLinc2_ADV'], + 'insteon_type': ['1.'] + }, + 'switch': { + 'uom': ['2', '78'], + 'states': ['on', 'off'], + 'node_def_id': ['OnOffControl', 'RelayLampSwitch', + 'RelayLampSwitch_ADV', 'RelaySwitchOnlyPlusQuery', + 'RelaySwitchOnlyPlusQuery_ADV', 'RelayLampOnly', + 'RelayLampOnly_ADV', 'KeypadButton', + 'KeypadButton_ADV', 'EZRAIN_Input', 'EZRAIN_Output', + 'EZIO2x4_Input', 'EZIO2x4_Input_ADV', 'BinaryControl', + 'BinaryControl_ADV', 'AlertModuleSiren', + 'AlertModuleSiren_ADV', 'AlertModuleArmed', 'Siren', + 'Siren_ADV'], + 'insteon_type': ['2.', '9.10.', '9.11.'] + } +} -PYISY = None +SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', + 'light', 'switch'] +SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch'] -HIDDEN_STRING = DEFAULT_HIDDEN_STRING - -SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock', - 'sensor', 'switch'] +# ISY Scenes are more like Swithes than Hass Scenes +# (they can turn off, and report their state) +SCENE_DOMAIN = 'switch' +ISY994_NODES = "isy994_nodes" +ISY994_WEATHER = "isy994_weather" +ISY994_PROGRAMS = "isy994_programs" WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom')) -def filter_nodes(nodes: list, units: list=None, states: list=None) -> list: - """Filter a list of ISY nodes based on the units and states provided.""" - filtered_nodes = [] - units = units if units else [] - states = states if states else [] - for node in nodes: - match_unit = False - match_state = True - for uom in node.uom: - if uom in units: - match_unit = True - continue - elif uom not in states: - match_state = False +def _check_for_node_def(hass: HomeAssistant, node, + single_domain: str=None) -> bool: + """Check if the node matches the node_def_id for any domains. - if match_unit: - continue - - if match_unit or match_state: - filtered_nodes.append(node) - - return filtered_nodes - - -def _is_node_a_sensor(node, path: str, sensor_identifier: str) -> bool: - """Determine if the given node is a sensor.""" - if not isinstance(node, PYISY.Nodes.Node): + This is only present on the 5.0 ISY firmware, and is the most reliable + way to determine a device's type. + """ + if not hasattr(node, 'node_def_id') or node.node_def_id is None: + # Node doesn't have a node_def (pre 5.0 firmware most likely) return False - if sensor_identifier in path or sensor_identifier in node.name: - return True + node_def_id = node.node_def_id - # This method is most reliable but only works on 5.x firmware - try: - if node.node_def_id == 'BinaryAlarm': + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if node_def_id in NODE_FILTERS[domain]['node_def_id']: + hass.data[ISY994_NODES][domain].append(node) return True - except AttributeError: - pass - - # This method works on all firmwares, but only for Insteon devices - try: - device_type = node.type - except AttributeError: - # Node has no type; most likely not an Insteon device - pass - else: - split_type = device_type.split('.') - return split_type[0] == '16' # 16 represents Insteon binary sensors return False -def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None: - """Categorize the ISY994 nodes.""" - global SENSOR_NODES - global NODES - global GROUPS +def _check_for_insteon_type(hass: HomeAssistant, node, + single_domain: str=None) -> bool: + """Check if the node matches the Insteon type for any domains. - SENSOR_NODES = [] - NODES = [] - GROUPS = [] + This is for (presumably) every version of the ISY firmware, but only + works for Insteon device. "Node Server" (v5+) and Z-Wave and others will + not have a type. + """ + if not hasattr(node, 'type') or node.type is None: + # Node doesn't have a type (non-Insteon device most likely) + return False + device_type = node.type + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if any([device_type.startswith(t) for t in + set(NODE_FILTERS[domain]['insteon_type'])]): + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _check_for_uom_id(hass: HomeAssistant, node, + single_domain: str=None, uom_list: list=None) -> bool: + """Check if a node's uom matches any of the domains uom filter. + + This is used for versions of the ISY firmware that report uoms as a single + ID. We can often infer what type of device it is by that ID. + """ + if not hasattr(node, 'uom') or node.uom is None: + # Node doesn't have a uom (Scenes for example) + return False + + node_uom = set(map(str.lower, node.uom)) + + if uom_list: + if node_uom.intersection(NODE_FILTERS[single_domain]['uom']): + hass.data[ISY994_NODES][single_domain].append(node) + return True + else: + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if node_uom.intersection(NODE_FILTERS[domain]['uom']): + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _check_for_states_in_uom(hass: HomeAssistant, node, + single_domain: str=None, + states_list: list=None) -> bool: + """Check if a list of uoms matches two possible filters. + + This is for versions of the ISY firmware that report uoms as a list of all + possible "human readable" states. This filter passes if all of the possible + states fit inside the given filter. + """ + if not hasattr(node, 'uom') or node.uom is None: + # Node doesn't have a uom (Scenes for example) + return False + + node_uom = set(map(str.lower, node.uom)) + + if states_list: + if node_uom == set(states_list): + hass.data[ISY994_NODES][single_domain].append(node) + return True + else: + domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] + for domain in domains: + if node_uom == set(NODE_FILTERS[domain]['states']): + hass.data[ISY994_NODES][domain].append(node) + return True + + return False + + +def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: + """Determine if the given sensor node should be a binary_sensor.""" + if _check_for_node_def(hass, node, single_domain='binary_sensor'): + return True + if _check_for_insteon_type(hass, node, single_domain='binary_sensor'): + return True + + # For the next two checks, we're providing our own set of uoms that + # represent on/off devices. This is because we can only depend on these + # checks in the context of already knowing that this is definitely a + # sensor device. + if _check_for_uom_id(hass, node, single_domain='binary_sensor', + uom_list=['2', '78']): + return True + if _check_for_states_in_uom(hass, node, single_domain='binary_sensor', + states_list=['on', 'off']): + return True + + return False + + +def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, + sensor_identifier: str)-> None: + """Sort the nodes to their proper domains.""" # pylint: disable=no-member - for (path, node) in ISY.nodes: - hidden = hidden_identifier in path or hidden_identifier in node.name - if hidden: - node.name += hidden_identifier - if _is_node_a_sensor(node, path, sensor_identifier): - SENSOR_NODES.append(node) - elif isinstance(node, PYISY.Nodes.Node): - NODES.append(node) - elif isinstance(node, PYISY.Nodes.Group): - GROUPS.append(node) + for (path, node) in nodes: + ignored = ignore_identifier in path or ignore_identifier in node.name + if ignored: + # Don't import this node as a device at all + continue + + from PyISY.Nodes import Group + if isinstance(node, Group): + hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) + continue + + if sensor_identifier in path or sensor_identifier in node.name: + # User has specified to treat this as a sensor. First we need to + # determine if it should be a binary_sensor. + if _is_sensor_a_binary_sensor(hass, node): + continue + else: + hass.data[ISY994_NODES]['sensor'].append(node) + continue + + # We have a bunch of different methods for determining the device type, + # each of which works with different ISY firmware versions or device + # family. The order here is important, from most reliable to least. + if _check_for_node_def(hass, node): + continue + if _check_for_insteon_type(hass, node): + continue + if _check_for_uom_id(hass, node): + continue + if _check_for_states_in_uom(hass, node): + continue -def _categorize_programs() -> None: +def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" - global PROGRAMS - - PROGRAMS = {} - - for component in SUPPORTED_DOMAINS: + for domain in SUPPORTED_PROGRAM_DOMAINS: try: - folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component] + folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)] except KeyError: pass else: for dtype, _, node_id in folder.children: - if dtype is KEY_FOLDER: - program = folder[node_id] + if dtype == KEY_FOLDER: + entity_folder = folder[node_id] try: - node = program[KEY_STATUS].leaf - assert node.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass - else: - if component not in PROGRAMS: - PROGRAMS[component] = [] - PROGRAMS[component].append(program) + status = entity_folder[KEY_STATUS] + assert status.dtype == 'program', 'Not a program' + if domain != 'binary_sensor': + actions = entity_folder[KEY_ACTIONS] + assert actions.dtype == 'program', 'Not a program' + else: + actions = None + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program entity '%s' not loaded due " + "to invalid folder structure.", + entity_folder.name) + continue + + entity = (entity_folder.name, status, actions) + hass.data[ISY994_PROGRAMS][domain].append(entity) -def _categorize_weather() -> None: +def _categorize_weather(hass: HomeAssistant, climate) -> None: """Categorize the ISY994 weather data.""" - global WEATHER_NODES - - climate_attrs = dir(ISY.climate) - WEATHER_NODES = [WeatherNode(getattr(ISY.climate, attr), attr, - getattr(ISY.climate, attr + '_units')) + climate_attrs = dir(climate) + weather_nodes = [WeatherNode(getattr(climate, attr), + attr.replace('_', ' '), + getattr(climate, '{}_units'.format(attr))) for attr in climate_attrs - if attr + '_units' in climate_attrs] + if '{}_units'.format(attr) in climate_attrs] + hass.data[ISY994_WEATHER].extend(weather_nodes) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ISY 994 platform.""" + hass.data[ISY994_NODES] = {} + for domain in SUPPORTED_DOMAINS: + hass.data[ISY994_NODES][domain] = [] + + hass.data[ISY994_WEATHER] = [] + + hass.data[ISY994_PROGRAMS] = {} + for domain in SUPPORTED_DOMAINS: + hass.data[ISY994_PROGRAMS][domain] = [] + isy_config = config.get(DOMAIN) user = isy_config.get(CONF_USERNAME) password = isy_config.get(CONF_PASSWORD) tls_version = isy_config.get(CONF_TLS_VER) host = urlparse(isy_config.get(CONF_HOST)) - port = host.port - addr = host.geturl() - hidden_identifier = isy_config.get( - CONF_HIDDEN_STRING, DEFAULT_HIDDEN_STRING) - sensor_identifier = isy_config.get( - CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING) - - global HIDDEN_STRING - HIDDEN_STRING = hidden_identifier + ignore_identifier = isy_config.get(CONF_IGNORE_STRING) + sensor_identifier = isy_config.get(CONF_SENSOR_STRING) + enable_climate = isy_config.get(CONF_ENABLE_CLIMATE) if host.scheme == 'http': - addr = addr.replace('http://', '') https = False + port = host.port or 80 elif host.scheme == 'https': - addr = addr.replace('https://', '') https = True + port = host.port or 443 else: _LOGGER.error("isy994 host value in configuration is invalid") return False - addr = addr.replace(':{}'.format(port), '') - import PyISY - - global PYISY - PYISY = PyISY - # Connect to ISY controller. - global ISY - ISY = PyISY.ISY(addr, port, username=user, password=password, + isy = PyISY.ISY(host.hostname, port, username=user, password=password, use_https=https, tls_ver=tls_version, log=_LOGGER) - if not ISY.connected: + if not isy.connected: return False - _categorize_nodes(hidden_identifier, sensor_identifier) + _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) + _categorize_programs(hass, isy.programs) - _categorize_programs() + if enable_climate and isy.configuration.get('Weather Information'): + _categorize_weather(hass, isy.climate) - if ISY.configuration.get('Weather Information'): - _categorize_weather() + def stop(event: object) -> None: + """Stop ISY auto updates.""" + isy.auto_update = False # Listen for HA stop to disconnect. hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) @@ -236,21 +389,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: for component in SUPPORTED_DOMAINS: discovery.load_platform(hass, component, DOMAIN, {}, config) - ISY.auto_update = True + isy.auto_update = True return True -# pylint: disable=unused-argument -def stop(event: object) -> None: - """Stop ISY auto updates.""" - ISY.auto_update = False - - class ISYDevice(Entity): """Representation of an ISY994 device.""" _attrs = {} - _domain = None # type: str _name = None # type: str def __init__(self, node) -> None: @@ -281,28 +427,16 @@ class ISYDevice(Entity): 'control': event }) - @property - def domain(self) -> str: - """Get the domain of the device.""" - return self._domain - @property def unique_id(self) -> str: """Get the unique identifier of the device.""" # pylint: disable=protected-access return self._node._id - @property - def raw_name(self) -> str: - """Get the raw name of the device.""" - return str(self._name) \ - if self._name is not None else str(self._node.name) - @property def name(self) -> str: """Get the name of the device.""" - return self.raw_name.replace(HIDDEN_STRING, '').strip() \ - .replace('_', ' ') + return self._name or str(self._node.name) @property def should_poll(self) -> bool: @@ -310,7 +444,7 @@ class ISYDevice(Entity): return False @property - def value(self) -> object: + def value(self) -> int: """Get the current value of the device.""" # pylint: disable=protected-access return self._node.status._val @@ -338,22 +472,3 @@ class ISYDevice(Entity): for name, val in self._node.aux_properties.items(): attr[name] = '{} {}'.format(val.get('value'), val.get('uom')) return attr - - @property - def hidden(self) -> bool: - """Get whether the device should be hidden from the UI.""" - return HIDDEN_STRING in self.raw_name - - @property - def unit_of_measurement(self) -> str: - """Get the device unit of measure.""" - return None - - def _attr_filter(self, attr: str) -> str: - """Filter the attribute.""" - # pylint: disable=no-self-use - return attr - - def update(self) -> None: - """Perform an update for the device.""" - pass diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index 3966b490f52..f9747351bdd 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -80,8 +80,11 @@ def async_setup(hass, config): yield from hass.data[DATA_KNX].start() except XKNXException as ex: - _LOGGER.exception("Can't connect to KNX interface: %s", ex) - return False + _LOGGER.warning("Can't connect to KNX interface: %s", ex) + hass.components.persistent_notification.async_create( + "Can't connect to KNX interface:
" + "{0}".format(ex), + title="KNX") for component, discovery_type in ( ('switch', 'Switch'), @@ -120,7 +123,8 @@ class KNXModule(object): """Initialization of KNXModule.""" self.hass = hass self.config = config - self.initialized = False + self.connected = False + self.initialized = True self.init_xknx() self.register_callbacks() @@ -139,7 +143,7 @@ class KNXModule(object): state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER], connection_config=connection_config) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) - self.initialized = True + self.connected = True @asyncio.coroutine def stop(self, event): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e4fb4542205..3d333e229fa 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -15,7 +15,6 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) @@ -282,21 +281,17 @@ def async_setup(hass, config): yield from asyncio.wait(update_tasks, loop=hass.loop) # Listen for light on and light off service calls. - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml')) - hass.services.async_register( DOMAIN, SERVICE_TURN_ON, async_handle_light_service, - descriptions.get(SERVICE_TURN_ON), schema=LIGHT_TURN_ON_SCHEMA) + schema=LIGHT_TURN_ON_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handle_light_service, - descriptions.get(SERVICE_TURN_OFF), schema=LIGHT_TURN_OFF_SCHEMA) + schema=LIGHT_TURN_OFF_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TOGGLE, async_handle_light_service, - descriptions.get(SERVICE_TOGGLE), schema=LIGHT_TOGGLE_SCHEMA) + schema=LIGHT_TOGGLE_SCHEMA) return True diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py new file mode 100644 index 00000000000..a1c43ad4cbc --- /dev/null +++ b/homeassistant/components/light/deconz.py @@ -0,0 +1,172 @@ +""" +Support for deCONZ light. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.deconz/ +""" + +import asyncio + +from homeassistant.components.deconz import DOMAIN as DECONZ_DATA +from homeassistant.components.light import ( + Light, ATTR_BRIGHTNESS, ATTR_FLASH, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_RGB_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, + SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR) +from homeassistant.core import callback +from homeassistant.util.color import color_RGB_to_xy + +DEPENDENCIES = ['deconz'] + +ATTR_LIGHT_GROUP = 'LightGroup' + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup light for deCONZ component.""" + if discovery_info is None: + return + + lights = hass.data[DECONZ_DATA].lights + groups = hass.data[DECONZ_DATA].groups + entities = [] + + for light in lights.values(): + entities.append(DeconzLight(light)) + + for group in groups.values(): + if group.lights: # Don't create entity for group not containing light + entities.append(DeconzLight(group)) + async_add_devices(entities, True) + + +class DeconzLight(Light): + """Representation of a deCONZ light.""" + + def __init__(self, light): + """Setup light and add update callback to get data from websocket.""" + self._light = light + + self._features = SUPPORT_BRIGHTNESS + self._features |= SUPPORT_FLASH + self._features |= SUPPORT_TRANSITION + + if self._light.ct is not None: + self._features |= SUPPORT_COLOR_TEMP + + if self._light.xy is not None: + self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_XY_COLOR + + if self._light.effect is not None: + self._features |= SUPPORT_EFFECT + + @asyncio.coroutine + def async_added_to_hass(self): + """Subscribe to lights events.""" + self._light.register_async_callback(self.async_update_callback) + + @callback + def async_update_callback(self, reason): + """Update the light's state.""" + self.async_schedule_update_ha_state() + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._light.brightness + + @property + def effect_list(self): + """Return the list of supported effects.""" + return [EFFECT_COLORLOOP] + + @property + def color_temp(self): + """Return the CT color value.""" + return self._light.ct + + @property + def xy_color(self): + """Return the XY color value.""" + return self._light.xy + + @property + def is_on(self): + """Return true if light is on.""" + return self._light.state + + @property + def name(self): + """Return the name of the light.""" + return self._light.name + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + @property + def available(self): + """Return True if light is available.""" + return self._light.reachable + + @property + def should_poll(self): + """No polling needed.""" + return False + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn on light.""" + data = {'on': True} + + if ATTR_COLOR_TEMP in kwargs: + data['ct'] = kwargs[ATTR_COLOR_TEMP] + + if ATTR_RGB_COLOR in kwargs: + xyb = color_RGB_to_xy( + *(int(val) for val in kwargs[ATTR_RGB_COLOR])) + data['xy'] = xyb[0], xyb[1] + data['bri'] = xyb[2] + + if ATTR_BRIGHTNESS in kwargs: + data['bri'] = kwargs[ATTR_BRIGHTNESS] + + if ATTR_TRANSITION in kwargs: + data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10 + + if ATTR_FLASH in kwargs: + if kwargs[ATTR_FLASH] == FLASH_SHORT: + data['alert'] = 'select' + del data['on'] + elif kwargs[ATTR_FLASH] == FLASH_LONG: + data['alert'] = 'lselect' + del data['on'] + + if ATTR_EFFECT in kwargs: + if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: + data['effect'] = 'colorloop' + else: + data['effect'] = 'none' + + yield from self._light.async_set_state(data) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn off light.""" + data = {'on': False} + + if ATTR_TRANSITION in kwargs: + data = {'bri': 0} + data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10 + + if ATTR_FLASH in kwargs: + if kwargs[ATTR_FLASH] == FLASH_SHORT: + data['alert'] = 'select' + del data['on'] + elif kwargs[ATTR_FLASH] == FLASH_LONG: + data['alert'] = 'lselect' + del data['on'] + + yield from self._light.async_set_state(data) diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py new file mode 100644 index 00000000000..0e99a49eaa9 --- /dev/null +++ b/homeassistant/components/light/greenwave.py @@ -0,0 +1,112 @@ +""" +Support for Greenwave Reality (TCP Connected) lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.greenwave/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS) +from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv + +SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS) + +REQUIREMENTS = ['greenwavereality==0.2.9'] +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required("version"): cv.positive_int, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Greenwave Reality Platform.""" + import greenwavereality as greenwave + import os + host = config.get(CONF_HOST) + tokenfile = hass.config.path('.greenwave') + if config.get("version") == 3: + if os.path.exists(tokenfile): + tokenfile = open(tokenfile) + token = tokenfile.read() + tokenfile.close() + else: + token = greenwave.grab_token(host, 'hass', 'homeassistant') + tokenfile = open(tokenfile, "w+") + tokenfile.write(token) + tokenfile.close() + else: + token = None + doc = greenwave.grab_xml(host, token) + add_devices(GreenwaveLight(device, host, token) for device in doc) + + +class GreenwaveLight(Light): + """Representation of an Greenwave Reality Light.""" + + def __init__(self, light, host, token): + """Initialize a Greenwave Reality Light.""" + import greenwavereality as greenwave + self._did = light['did'] + self._name = light['name'] + self._state = int(light['state']) + self._brightness = greenwave.hass_brightness(light) + self._host = host + self._online = greenwave.check_online(light) + self.token = token + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES + + @property + def available(self): + """Return True if entity is available.""" + return self._online + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + import greenwavereality as greenwave + temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) + / 255) * 100) + greenwave.set_brightness(self._host, self._did, + temp_brightness, self.token) + greenwave.turn_on(self._host, self._did, self.token) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + import greenwavereality as greenwave + greenwave.turn_off(self._host, self._did, self.token) + + def update(self): + """Fetch new state data for this light.""" + import greenwavereality as greenwave + doc = greenwave.grab_xml(self._host, self.token) + + for device in doc: + if device['did'] == self._did: + self._state = int(device['state']) + self._brightness = greenwave.hass_brightness(device) + self._online = greenwave.check_online(device) + self._name = device['name'] diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index 95bd0b6988d..3356d637be8 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -4,8 +4,10 @@ Support for the Hive devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.hive/ """ +import colorsys from homeassistant.components.hive import DATA_HIVE from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) @@ -46,19 +48,24 @@ class HiveDeviceLight(Light): """Return the display name of this light.""" return self.node_name + @property + def brightness(self): + """Brightness of the light (an integer in the range 1-255).""" + return self.session.light.get_brightness(self.node_id) + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" if self.light_device_type == "tuneablelight" \ or self.light_device_type == "colourtuneablelight": - return self.session.light.get_min_colour_temp(self.node_id) + return self.session.light.get_min_color_temp(self.node_id) @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" if self.light_device_type == "tuneablelight" \ or self.light_device_type == "colourtuneablelight": - return self.session.light.get_max_colour_temp(self.node_id) + return self.session.light.get_max_color_temp(self.node_id) @property def color_temp(self): @@ -68,9 +75,10 @@ class HiveDeviceLight(Light): return self.session.light.get_color_temp(self.node_id) @property - def brightness(self): - """Brightness of the light (an integer in the range 1-255).""" - return self.session.light.get_brightness(self.node_id) + def rgb_color(self) -> tuple: + """Return the RBG color value.""" + if self.light_device_type == "colourtuneablelight": + return self.session.light.get_color(self.node_id) @property def is_on(self): @@ -81,6 +89,7 @@ class HiveDeviceLight(Light): """Instruct the light to turn on.""" new_brightness = None new_color_temp = None + new_color = None if ATTR_BRIGHTNESS in kwargs: tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) percentage_brightness = ((tmp_new_brightness / 255) * 100) @@ -90,13 +99,19 @@ class HiveDeviceLight(Light): if ATTR_COLOR_TEMP in kwargs: tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) new_color_temp = round(1000000 / tmp_new_color_temp) + if ATTR_RGB_COLOR in kwargs: + get_new_color = kwargs.get(ATTR_RGB_COLOR) + tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0], + get_new_color[1], + get_new_color[2]) + hue = int(round(tmp_new_color[0] * 360)) + saturation = int(round(tmp_new_color[1] * 100)) + value = int(round((tmp_new_color[2] / 255) * 100)) + new_color = (hue, saturation, value) - if new_brightness is not None: - self.session.light.set_brightness(self.node_id, new_brightness) - elif new_color_temp is not None: - self.session.light.set_colour_temp(self.node_id, new_color_temp) - else: - self.session.light.turn_on(self.node_id) + self.session.light.turn_on(self.node_id, self.light_device_type, + new_brightness, new_color_temp, + new_color) for entity in self.session.entities: entity.handle_update(self.data_updatesource) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index f5c910ea116..f4ea04240f1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -4,6 +4,7 @@ This component provides light support for the Philips Hue system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.hue/ """ +import asyncio from datetime import timedelta import logging import random @@ -14,9 +15,6 @@ import voluptuous as vol import homeassistant.components.hue as hue -import homeassistant.util as util -from homeassistant.util import yaml -import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, @@ -24,8 +22,10 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME -from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE_HIDDEN import homeassistant.helpers.config_validation as cv +import homeassistant.util as util +from homeassistant.util import yaml +import homeassistant.util.color as color_util DEPENDENCIES = ['hue'] @@ -49,6 +49,7 @@ SUPPORT_HUE = { 'Color temperature light': SUPPORT_HUE_COLOR_TEMP } +ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' ATTR_IS_HUE_GROUP = 'is_hue_group' # Legacy configuration, will be removed in 0.60 @@ -83,6 +84,8 @@ This configuration is deprecated, please check the information. """ +SIGNAL_CALLBACK = 'hue_light_callback_{}_{}' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Hue lights.""" @@ -130,14 +133,12 @@ def unthrottled_update_lights(hass, bridge, add_devices): _LOGGER.exception('Cannot reach the bridge') return - bridge_type = get_bridge_type(api) - new_lights = process_lights( - hass, api, bridge, bridge_type, + hass, api, bridge, lambda **kw: update_lights(hass, bridge, add_devices, **kw)) if bridge.allow_hue_groups: new_lightgroups = process_groups( - hass, api, bridge, bridge_type, + hass, api, bridge, lambda **kw: update_lights(hass, bridge, add_devices, **kw)) new_lights.extend(new_lightgroups) @@ -145,16 +146,7 @@ def unthrottled_update_lights(hass, bridge, add_devices): add_devices(new_lights) -def get_bridge_type(api): - """Return the bridge type.""" - api_name = api.get('config').get('name') - if api_name in ('RaspBee-GW', 'deCONZ-GW'): - return 'deconz' - else: - return 'hue' - - -def process_lights(hass, api, bridge, bridge_type, update_lights_cb): +def process_lights(hass, api, bridge, update_lights_cb): """Set up HueLight objects for all lights.""" api_lights = api.get('lights') @@ -169,17 +161,20 @@ def process_lights(hass, api, bridge, bridge_type, update_lights_cb): bridge.lights[light_id] = HueLight( int(light_id), info, bridge, update_lights_cb, - bridge_type, bridge.allow_unreachable, + bridge.allow_unreachable, bridge.allow_in_emulated_hue) new_lights.append(bridge.lights[light_id]) else: bridge.lights[light_id].info = info - bridge.lights[light_id].schedule_update_ha_state() + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_CALLBACK.format( + bridge.bridge_id, + bridge.lights[light_id].light_id)) return new_lights -def process_groups(hass, api, bridge, bridge_type, update_lights_cb): +def process_groups(hass, api, bridge, update_lights_cb): """Set up HueLight objects for all groups.""" api_groups = api.get('groups') @@ -199,12 +194,15 @@ def process_groups(hass, api, bridge, bridge_type, update_lights_cb): bridge.lightgroups[lightgroup_id] = HueLight( int(lightgroup_id), info, bridge, update_lights_cb, - bridge_type, bridge.allow_unreachable, + bridge.allow_unreachable, bridge.allow_in_emulated_hue, True) new_lights.append(bridge.lightgroups[lightgroup_id]) else: bridge.lightgroups[lightgroup_id].info = info - bridge.lightgroups[lightgroup_id].schedule_update_ha_state() + hass.helpers.dispatcher.dispatcher_send( + SIGNAL_CALLBACK.format( + bridge.bridge_id, + bridge.lightgroups[lightgroup_id].light_id)) return new_lights @@ -213,14 +211,12 @@ class HueLight(Light): """Representation of a Hue light.""" def __init__(self, light_id, info, bridge, update_lights_cb, - bridge_type, allow_unreachable, allow_in_emulated_hue, - is_group=False): + allow_unreachable, allow_in_emulated_hue, is_group=False): """Initialize the light.""" self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights_cb - self.bridge_type = bridge_type self.allow_unreachable = allow_unreachable self.is_group = is_group self.allow_in_emulated_hue = allow_in_emulated_hue @@ -330,7 +326,7 @@ class HueLight(Light): elif flash == FLASH_SHORT: command['alert'] = 'select' del command['on'] - elif self.bridge_type == 'hue': + else: command['alert'] = 'none' effect = kwargs.get(ATTR_EFFECT) @@ -340,8 +336,7 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - elif (self.bridge_type == 'hue' and - self.info.get('manufacturername') == 'Philips'): + elif self.info.get('manufacturername') == 'Philips': command['effect'] = 'none' self._command_func(self.light_id, command) @@ -361,7 +356,7 @@ class HueLight(Light): elif flash == FLASH_SHORT: command['alert'] = 'select' del command['on'] - elif self.bridge_type == 'hue': + else: command['alert'] = 'none' self._command_func(self.light_id, command) @@ -380,3 +375,11 @@ class HueLight(Light): if self.is_group: attributes[ATTR_IS_HUE_GROUP] = self.is_group return attributes + + @asyncio.coroutine + def async_added_to_hass(self): + """Register update callback.""" + dev_id = self.bridge.bridge_id, self.light_id + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CALLBACK.format(*dev_id), + self.async_schedule_update_ha_state) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py new file mode 100644 index 00000000000..11366ffc45c --- /dev/null +++ b/homeassistant/components/light/iglo.py @@ -0,0 +1,129 @@ +""" +Support for lights under the iGlo brand. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.iglo/ +""" +import logging +import math + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PORT) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, + Light, PLATFORM_SCHEMA +) + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + +REQUIREMENTS = ['iglo==1.1.3'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'iGlo Light' +DEFAULT_PORT = 8080 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the iGlo lighs.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + port = config.get(CONF_PORT) + add_devices([IGloLamp(name, host, port)], True) + + +class IGloLamp(Light): + """Representation of an iGlo light.""" + + def __init__(self, name, host, port): + """Initialize the light.""" + from iglo import Lamp + self._name = name + self._lamp = Lamp(0, host, port) + self._on = True + self._brightness = 255 + self._rgb = (0, 0, 0) + self._color_temp = 0 + + @property + def name(self): + """Return the name of the light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int((self._brightness / 200.0) * 255) + + @property + def color_temp(self): + """Return the color temperature.""" + return color_util.color_temperature_kelvin_to_mired(self._color_temp) + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return math.ceil(color_util.color_temperature_kelvin_to_mired( + self._lamp.max_kelvin)) + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return math.ceil(color_util.color_temperature_kelvin_to_mired( + self._lamp.min_kelvin)) + + @property + def rgb_color(self): + """Return the RGB value.""" + return self._rgb + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR + + @property + def is_on(self): + """Return true if light is on.""" + return self._on + + def turn_on(self, **kwargs): + """Turn the light on.""" + if not self._on: + self._lamp.switch(True) + if ATTR_BRIGHTNESS in kwargs: + brightness = int((kwargs[ATTR_BRIGHTNESS] / 255.0) * 200.0) + self._lamp.brightness(brightness) + return + + if ATTR_RGB_COLOR in kwargs: + rgb = kwargs[ATTR_RGB_COLOR] + self._lamp.rgb(*rgb) + return + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int(color_util.color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP])) + self._lamp.white(kelvin) + return + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._lamp.switch(False) + + def update(self): + """Update light status.""" + state = self._lamp.state() + self._on = state['on'] + self._brightness = state['brightness'] + self._rgb = state['rgb'] + self._color_temp = state['white'] diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index 9d704327a1d..88d621d4060 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -10,8 +10,6 @@ from datetime import timedelta from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) import homeassistant.util as util -from homeassistant.util.json import load_json, save_json - _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -19,8 +17,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['insteon_local'] DOMAIN = 'light' -INSTEON_LOCAL_LIGHTS_CONF = 'insteon_local_lights.conf' - MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) @@ -30,84 +26,33 @@ SUPPORT_INSTEON_LOCAL = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Insteon local light platform.""" insteonhub = hass.data['insteon_local'] - - conf_lights = load_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) - if conf_lights: - for device_id in conf_lights: - setup_light(device_id, conf_lights[device_id], insteonhub, hass, - add_devices) - - else: - linked = insteonhub.get_linked() - - for device_id in linked: - if (linked[device_id]['cat_type'] == 'dimmer' and - device_id not in conf_lights): - request_configuration(device_id, - insteonhub, - linked[device_id]['model_name'] + ' ' + - linked[device_id]['sku'], - hass, add_devices) - - -def request_configuration(device_id, insteonhub, model, hass, - add_devices_callback): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - # We got an error if this method is called while we are configuring - if device_id in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[device_id], 'Failed to register, please try again.') - + if discovery_info is None: return - def insteon_light_config_callback(data): - """Set up actions to do when our configuration callback is called.""" - setup_light(device_id, data.get('name'), insteonhub, hass, - add_devices_callback) + linked = discovery_info['linked'] + device_list = [] + for device_id in linked: + if linked[device_id]['cat_type'] == 'dimmer': + device = insteonhub.dimmer(device_id) + device_list.append( + InsteonLocalDimmerDevice(device) + ) - _CONFIGURING[device_id] = configurator.request_config( - 'Insteon ' + model + ' addr: ' + device_id, - insteon_light_config_callback, - description=('Enter a name for ' + model + ' addr: ' + device_id), - entity_picture='/static/images/config_insteon.png', - submit_caption='Confirm', - fields=[{'id': 'name', 'name': 'Name', 'type': ''}] - ) - - -def setup_light(device_id, name, insteonhub, hass, add_devices_callback): - """Set up the light.""" - if device_id in _CONFIGURING: - request_id = _CONFIGURING.pop(device_id) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.debug("Device configuration done") - - conf_lights = load_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) - if device_id not in conf_lights: - conf_lights[device_id] = name - - save_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF), conf_lights) - - device = insteonhub.dimmer(device_id) - add_devices_callback([InsteonLocalDimmerDevice(device, name)]) + add_devices(device_list) class InsteonLocalDimmerDevice(Light): """An abstract Class for an Insteon node.""" - def __init__(self, node, name): + def __init__(self, node): """Initialize the device.""" self.node = node - self.node.deviceName = name self._value = 0 @property def name(self): """Return the name of the node.""" - return self.node.deviceName + return self.node.device_id @property def unique_id(self): diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 78b92fbd145..a6191b05c7c 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -8,40 +8,30 @@ import logging from typing import Callable from homeassistant.components.light import ( - Light, SUPPORT_BRIGHTNESS) -import homeassistant.components.isy994 as isy -from homeassistant.const import STATE_ON, STATE_OFF + Light, SUPPORT_BRIGHTNESS, DOMAIN) +from homeassistant.components.isy994 import ISY994_NODES, ISYDevice from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -UOM = ['2', '51', '78'] -STATES = [STATE_OFF, STATE_ON, 'true', 'false', '%'] - # pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 light 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 = [] - - for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES): - if node.dimmable or '51' in node.uom: - devices.append(ISYLightDevice(node)) + for node in hass.data[ISY994_NODES][DOMAIN]: + devices.append(ISYLightDevice(node)) add_devices(devices) -class ISYLightDevice(isy.ISYDevice, Light): +class ISYLightDevice(ISYDevice, Light): """Representation of an ISY994 light devie.""" def __init__(self, node: object) -> None: """Initialize the ISY994 light device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def is_on(self) -> bool: diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 3688cafdd25..c1caf91db45 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -97,6 +97,11 @@ class KNXLight(Light): """Return the name of the KNX device.""" return self.device.name + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + @property def should_poll(self): """No polling needed within KNX.""" diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 06a00954d3b..22ec58f65cd 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -8,7 +8,6 @@ import logging import asyncio import sys import math -from os import path from functools import partial from datetime import timedelta @@ -22,7 +21,6 @@ from homeassistant.components.light import ( SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, preprocess_turn_on_alternatives) -from homeassistant.config import load_yaml_config_file from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant import util from homeassistant.core import callback @@ -210,13 +208,10 @@ class LIFXManager(object): self.async_add_devices = async_add_devices self.effects_conductor = aiolifx_effects().Conductor(loop=hass.loop) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) + self.register_set_state() + self.register_effects() - self.register_set_state(descriptions) - self.register_effects(descriptions) - - def register_set_state(self, descriptions): + def register_set_state(self): """Register the LIFX set_state service call.""" @asyncio.coroutine def async_service_handle(service): @@ -231,10 +226,9 @@ class LIFXManager(object): self.hass.services.async_register( DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle, - descriptions.get(SERVICE_LIFX_SET_STATE), schema=LIFX_SET_STATE_SCHEMA) - def register_effects(self, descriptions): + def register_effects(self): """Register the LIFX effects as hass service calls.""" @asyncio.coroutine def async_service_handle(service): @@ -246,17 +240,14 @@ class LIFXManager(object): self.hass.services.async_register( DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle, - descriptions.get(SERVICE_EFFECT_PULSE), schema=LIFX_EFFECT_PULSE_SCHEMA) self.hass.services.async_register( DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle, - descriptions.get(SERVICE_EFFECT_COLORLOOP), schema=LIFX_EFFECT_COLORLOOP_SCHEMA) self.hass.services.async_register( DOMAIN, SERVICE_EFFECT_STOP, async_service_handle, - descriptions.get(SERVICE_EFFECT_STOP), schema=LIFX_EFFECT_STOP_SCHEMA) @asyncio.coroutine diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 1e5c0f743bb..f97e37127b1 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -21,7 +21,9 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC) + CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -95,7 +97,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In(VALUES_ON_COMMAND_TYPE), -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -148,16 +150,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_BRIGHTNESS_SCALE), config.get(CONF_WHITE_VALUE_SCALE), config.get(CONF_ON_COMMAND_TYPE), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttLight(Light): +class MqttLight(MqttAvailability, Light): """Representation of a MQTT light.""" def __init__(self, name, effect_list, topic, templates, qos, retain, payload, optimistic, brightness_scale, - white_value_scale, on_command_type): + white_value_scale, on_command_type, availability_topic, + payload_available, payload_not_available): """Initialize MQTT light.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._effect_list = effect_list self._topic = topic @@ -208,10 +216,9 @@ class MqttLight(Light): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ templates = {} for key, tpl in list(self._templates.items()): if tpl is None: @@ -424,7 +431,7 @@ class MqttLight(Light): tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] if tpl: - colors = {'red', 'green', 'blue'} + colors = ('red', 'green', 'blue') variables = {key: val for key, val in zip(colors, kwargs[ATTR_RGB_COLOR])} rgb_color_str = tpl.async_render(variables) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py old mode 100755 new mode 100644 index e3e3f7dafde..3646de977cf --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -21,7 +21,9 @@ from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -66,7 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -97,17 +99,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_LONG ) - } + }, + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE) )]) -class MqttJson(Light): +class MqttJson(MqttAvailability, Light): """Representation of a MQTT JSON light.""" def __init__(self, name, effect_list, topic, qos, retain, optimistic, brightness, color_temp, effect, rgb, white_value, xy, - flash_times): + flash_times, availability_topic, payload_available, + payload_not_available): """Initialize MQTT JSON light.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._effect_list = effect_list self._topic = topic @@ -157,10 +165,9 @@ class MqttJson(Light): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py old mode 100755 new mode 100644 index 6dabedbd444..de0f6d934c6 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -17,7 +17,9 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -60,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -95,16 +97,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, config.get(CONF_OPTIMISTIC), config.get(CONF_QOS), - config.get(CONF_RETAIN) + config.get(CONF_RETAIN), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttTemplate(Light): +class MqttTemplate(MqttAvailability, Light): """Representation of a MQTT Template light.""" def __init__(self, hass, name, effect_list, topics, templates, optimistic, - qos, retain): + qos, retain, availability_topic, payload_available, + payload_not_available): """Initialize a MQTT Template light.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._name = name self._effect_list = effect_list self._topics = topics @@ -145,10 +153,9 @@ class MqttTemplate(Light): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index b5dbe7ebb4c..5785f0f1fc7 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -4,34 +4,36 @@ Support for Osram Lightify. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.osramlightify/ """ -import logging -import socket -import random from datetime import timedelta +import logging +import random +import socket import voluptuous as vol from homeassistant import util -from homeassistant.const import CONF_HOST from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_XY_COLOR, ATTR_TRANSITION, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, SUPPORT_XY_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, PLATFORM_SCHEMA) -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired, - color_xy_brightness_to_RGB) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, + ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_RANDOM, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, + SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) +from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, + color_xy_brightness_to_RGB) -REQUIREMENTS = ['lightify==1.0.6'] +REQUIREMENTS = ['lightify==1.0.6.1'] _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) -CONF_ALLOW_LIGHTIFY_GROUPS = "allow_lightify_groups" +CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups' + DEFAULT_ALLOW_LIGHTIFY_GROUPS = True +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION | SUPPORT_XY_COLOR) @@ -46,20 +48,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Osram Lightify lights.""" import lightify + host = config.get(CONF_HOST) add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS) - if host: - try: - bridge = lightify.Lightify(host) - except socket.error as err: - msg = "Error connecting to bridge: {} due to: {}".format( - host, str(err)) - _LOGGER.exception(msg) - return False - setup_bridge(bridge, add_devices, add_groups) - else: - _LOGGER.error("No host found in configuration") - return False + + try: + bridge = lightify.Lightify(host) + except socket.error as err: + msg = "Error connecting to bridge: {} due to: {}".format( + host, str(err)) + _LOGGER.exception(msg) + return + + setup_bridge(bridge, add_devices, add_groups) def setup_bridge(bridge, add_devices_callback, add_groups): @@ -73,17 +74,16 @@ def setup_bridge(bridge, add_devices_callback, add_groups): bridge.update_all_light_status() bridge.update_group_list() except TimeoutError: - _LOGGER.error('Timeout during updating of lights.') + _LOGGER.error("Timeout during updating of lights") except OSError: - _LOGGER.error('OSError during updating of lights.') + _LOGGER.error("OSError during updating of lights") new_lights = [] for (light_id, light) in bridge.lights().items(): if light_id not in lights: - osram_light = OsramLightifyLight(light_id, light, - update_lights) - + osram_light = OsramLightifyLight( + light_id, light, update_lights) lights[light_id] = osram_light new_lights.append(osram_light) else: @@ -92,8 +92,8 @@ def setup_bridge(bridge, add_devices_callback, add_groups): if add_groups: for (group_name, group) in bridge.groups().items(): if group_name not in lights: - osram_group = OsramLightifyGroup(group, bridge, - update_lights) + osram_group = OsramLightifyGroup( + group, bridge, update_lights) lights[group_name] = osram_group new_lights.append(osram_group) else: @@ -106,10 +106,10 @@ def setup_bridge(bridge, add_devices_callback, add_groups): class Luminary(Light): - """ABS for Lightify Lights and Groups.""" + """Representation of Luminary Lights and Groups.""" def __init__(self, luminary, update_lights): - """Init Luminary object.""" + """Initize a Luminary light.""" self.update_lights = update_lights self._luminary = luminary self._brightness = None @@ -141,7 +141,7 @@ class Luminary(Light): @property def is_on(self): - """Update Status to True if device is on.""" + """Update status to True if device is on.""" return self._state @property @@ -170,8 +170,7 @@ class Luminary(Light): _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", self._name, self._brightness) self._luminary.set_luminance( - int(self._brightness / 2.55), - transition) + int(self._brightness / 2.55), transition) else: self._luminary.set_onoff(1) @@ -187,8 +186,7 @@ class Luminary(Light): _LOGGER.debug("turn_on requested ATTR_XY_COLOR for light:" " %s is: %s,%s", self._name, x_mired, y_mired) red, green, blue = color_xy_brightness_to_RGB( - x_mired, y_mired, self._brightness - ) + x_mired, y_mired, self._brightness) self._luminary.set_rgb(red, green, blue, transition) if ATTR_COLOR_TEMP in kwargs: @@ -201,10 +199,9 @@ class Luminary(Light): if ATTR_EFFECT in kwargs: effect = kwargs.get(ATTR_EFFECT) if effect == EFFECT_RANDOM: - self._luminary.set_rgb(random.randrange(0, 255), - random.randrange(0, 255), - random.randrange(0, 255), - transition) + self._luminary.set_rgb( + random.randrange(0, 255), random.randrange(0, 255), + random.randrange(0, 255), transition) _LOGGER.debug("turn_on requested random effect for light: " "%s with transition %s", self._name, transition) @@ -212,19 +209,16 @@ class Luminary(Light): def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGER.debug("turn_off Attempting to turn off light: %s ", - self._name) + _LOGGER.debug("Attempting to turn off light: %s", self._name) if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", - self._name, transition) + " %s is: %s ", self._name, transition) self._luminary.set_luminance(0, transition) else: transition = 0 _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", - self._name, transition) + " %s is: %s ", self._name, transition) self._luminary.set_onoff(0) self.schedule_update_ha_state() @@ -238,12 +232,12 @@ class OsramLightifyLight(Luminary): """Representation of an Osram Lightify Light.""" def __init__(self, light_id, light, update_lights): - """Initialize the light.""" + """Initialize the Lightify light.""" self._light_id = light_id super().__init__(light, update_lights) def update(self): - """Update status of a Light.""" + """Update status of a light.""" super().update() self._state = self._luminary.on() self._rgb = self._luminary.rgb() @@ -252,8 +246,7 @@ class OsramLightifyLight(Luminary): self._temperature = None else: self._temperature = color_temperature_kelvin_to_mired( - self._luminary.temp() - ) + self._luminary.temp()) self._brightness = int(self._luminary.lum() * 2.55) @@ -261,16 +254,13 @@ class OsramLightifyGroup(Luminary): """Representation of an Osram Lightify Group.""" def __init__(self, group, bridge, update_lights): - """Init light group.""" + """Initialize the Lightify light group.""" self._bridge = bridge self._light_ids = [] super().__init__(group, update_lights) def _get_state(self): - """Get state of group. - - The group is on, if any of the lights is on. - """ + """Get state of group.""" lights = self._bridge.lights() return any(lights[light_id].on() for light_id in self._light_ids) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 9248b0131f1..cdfe2fe5671 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -6,15 +6,32 @@ https://home-assistant.io/components/light.rfxtrx/ """ import logging +import voluptuous as vol + import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.light import (ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, 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'] _LOGGER = logging.getLogger(__name__) -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), +}) SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 465e84fae90..ed7ba1978cc 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -40,10 +40,15 @@ LIGHT_SCHEMA = vol.Schema({ vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE, default=None): cv.template, vol.Optional(CONF_LEVEL_ACTION, default=None): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL_TEMPLATE, default=None): cv.template, - vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_ENTITY_ID): cv.entity_ids }) +LIGHT_SCHEMA = vol.All( + cv.deprecated(CONF_ENTITY_ID), + LIGHT_SCHEMA, +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}), }) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 692a5fb86ec..87004f45ea0 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -8,10 +8,13 @@ import logging import colorsys import time +import voluptuous as vol + from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR) + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin from homeassistant.util.color import ( @@ -23,9 +26,16 @@ REQUIREMENTS = ['pyHS100==0.3.0'] _LOGGER = logging.getLogger(__name__) -ATTR_CURRENT_CONSUMPTION = 'current_consumption' -ATTR_DAILY_CONSUMPTION = 'daily_consumption' -ATTR_MONTHLY_CONSUMPTION = 'monthly_consumption' +ATTR_CURRENT_POWER_W = 'current_power_w' +ATTR_DAILY_ENERGY_KWH = 'daily_energy_kwh' +ATTR_MONTHLY_ENERGY_KWH = 'monthly_energy_kwh' + +DEFAULT_NAME = 'TP-Link Light' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -156,17 +166,17 @@ class TPLinkSmartBulb(Light): if self._supported_features & SUPPORT_RGB_COLOR: self._rgb = hsv_to_rgb(self.smartbulb.hsv) if self.smartbulb.has_emeter: - self._emeter_params[ATTR_CURRENT_CONSUMPTION] \ - = "%.1f W" % self.smartbulb.current_consumption() + self._emeter_params[ATTR_CURRENT_POWER_W] = '{:.1f}'.format( + self.smartbulb.current_consumption()) daily_statistics = self.smartbulb.get_emeter_daily() monthly_statistics = self.smartbulb.get_emeter_monthly() try: - self._emeter_params[ATTR_DAILY_CONSUMPTION] \ - = "%.2f kW" % daily_statistics[int( - time.strftime("%d"))] - self._emeter_params[ATTR_MONTHLY_CONSUMPTION] \ - = "%.2f kW" % monthly_statistics[int( - time.strftime("%m"))] + self._emeter_params[ATTR_DAILY_ENERGY_KWH] \ + = "{:.3f}".format( + daily_statistics[int(time.strftime("%d"))]) + self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] \ + = "{:.3f}".format( + monthly_statistics[int(time.strftime("%m"))]) except KeyError: # device returned no daily/monthly history pass diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index ddffed52271..b35b5a3740e 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.2'] +REQUIREMENTS = ['python-miio==0.3.3'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index a18fdc9dec6..2aad486a894 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -17,6 +17,11 @@ DEPENDENCIES = ['zha'] DEFAULT_DURATION = 0.5 +CAPABILITIES_COLOR_XY = 0x08 +CAPABILITIES_COLOR_TEMP = 0x10 + +UNSUPPORTED_ATTRIBUTE = 0x86 + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -32,9 +37,36 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): except (AttributeError, KeyError): pass + if discovery_info.get('color_capabilities') is None: + # ZCL Version 4 devices don't support the color_capabilities attribute. + # In this version XY support is mandatory, but we need to probe to + # determine if the device supports color temperature. + discovery_info['color_capabilities'] = CAPABILITIES_COLOR_XY + result = yield from safe_read( + endpoint.light_color, ['color_temperature']) + if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE: + discovery_info['color_capabilities'] |= CAPABILITIES_COLOR_TEMP + async_add_devices([Light(**discovery_info)], update_before_add=True) +@asyncio.coroutine +def safe_read(cluster, attributes): + """Swallow all exceptions from network read. + + If we throw during initialization, setup fails. Rather have an + entity that exists, but is in a maybe wrong state, than no entity. + """ + try: + result, _ = yield from cluster.read_attributes( + attributes, + allow_cache=False, + ) + return result + except Exception: # pylint: disable=broad-except + return {} + + class Light(zha.Entity, light.Light): """Representation of a ZHA or ZLL light.""" @@ -54,11 +86,11 @@ class Light(zha.Entity, light.Light): self._supported_features |= light.SUPPORT_TRANSITION self._brightness = 0 if zcl_clusters.lighting.Color.cluster_id in self._in_clusters: - color_capabilities = kwargs.get('color_capabilities', 0x10) - if color_capabilities & 0x10: + color_capabilities = kwargs['color_capabilities'] + if color_capabilities & CAPABILITIES_COLOR_TEMP: self._supported_features |= light.SUPPORT_COLOR_TEMP - if color_capabilities & 0x08: + if color_capabilities & CAPABILITIES_COLOR_XY: self._supported_features |= light.SUPPORT_XY_COLOR self._supported_features |= light.SUPPORT_RGB_COLOR self._xy_color = (1.0, 1.0) @@ -142,24 +174,6 @@ class Light(zha.Entity, light.Light): @asyncio.coroutine def async_update(self): """Retrieve latest state.""" - _LOGGER.debug("%s async_update", self.entity_id) - - @asyncio.coroutine - def safe_read(cluster, attributes): - """Swallow all exceptions from network read. - - If we throw during initialization, setup fails. Rather have an - entity that exists, but is in a maybe wrong state, than no entity. - """ - try: - result, _ = yield from cluster.read_attributes( - attributes, - allow_cache=False, - ) - return result - except Exception: # pylint: disable=broad-except - return {} - result = yield from safe_read(self._endpoint.on_off, ['on_off']) self._state = result.get('on_off', self._state) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index a1ad3a83b50..80abce4ec3e 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -8,11 +8,9 @@ import asyncio from datetime import timedelta import functools as ft import logging -import os import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity @@ -104,16 +102,12 @@ def async_setup(hass, config): if update_tasks: 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')) - hass.services.async_register( DOMAIN, SERVICE_UNLOCK, async_handle_lock_service, - descriptions.get(SERVICE_UNLOCK), schema=LOCK_SERVICE_SCHEMA) + schema=LOCK_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_LOCK, async_handle_lock_service, - descriptions.get(SERVICE_LOCK), schema=LOCK_SERVICE_SCHEMA) + schema=LOCK_SERVICE_SCHEMA) return True diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 63272b90b1f..33e2a0bea25 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -8,7 +8,8 @@ import logging from typing import Callable # noqa from homeassistant.components.lock import LockDevice, DOMAIN -import homeassistant.components.isy994 as isy +from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, + ISYDevice) from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN from homeassistant.helpers.typing import ConfigType @@ -19,43 +20,27 @@ VALUE_TO_STATE = { 100: STATE_LOCKED } -UOM = ['11'] -STATES = [STATE_LOCKED, STATE_UNLOCKED] - # pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 lock 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 = [] - - for node in isy.filter_nodes(isy.NODES, units=UOM, - states=STATES): + for node in hass.data[ISY994_NODES][DOMAIN]: devices.append(ISYLockDevice(node)) - for program in isy.PROGRAMS.get(DOMAIN, []): - try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass - else: - devices.append(ISYLockProgram(program.name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + devices.append(ISYLockProgram(name, status, actions)) add_devices(devices) -class ISYLockDevice(isy.ISYDevice, LockDevice): +class ISYLockDevice(ISYDevice, LockDevice): """Representation of an ISY994 lock device.""" def __init__(self, node) -> None: """Initialize the ISY994 lock device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) self._conn = node.parent.parent.conn @property @@ -101,7 +86,7 @@ class ISYLockProgram(ISYLockDevice): def __init__(self, name: str, node, actions) -> None: """Initialize the lock.""" - ISYLockDevice.__init__(self, node) + super().__init__(node) self._name = name self._actions = actions diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index b2533145a20..e73e35a9900 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) import homeassistant.components.mqtt as mqtt @@ -36,7 +38,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -56,15 +58,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_UNLOCK), config.get(CONF_OPTIMISTIC), value_template, + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE) )]) -class MqttLock(LockDevice): +class MqttLock(MqttAvailability, LockDevice): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, name, state_topic, command_topic, qos, retain, - payload_lock, payload_unlock, optimistic, value_template): + payload_lock, payload_unlock, optimistic, value_template, + availability_topic, payload_available, payload_not_available): """Initialize the lock.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = False self._name = name self._state_topic = state_topic @@ -78,10 +86,9 @@ class MqttLock(LockDevice): @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py index b47305fa227..6efa3dcb80c 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -7,13 +7,11 @@ https://home-assistant.io/components/lock.nuki/ import asyncio from datetime import timedelta import logging -from os import path import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.lock import (DOMAIN, LockDevice, PLATFORM_SCHEMA) -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN) from homeassistant.helpers.service import extract_entity_ids @@ -75,15 +73,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif service.service == SERVICE_UNLATCH: lock.unlatch() - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - hass.services.register( DOMAIN, SERVICE_LOCK_N_GO, service_handler, - descriptions.get(SERVICE_LOCK_N_GO), schema=LOCK_N_GO_SERVICE_SCHEMA) + schema=LOCK_N_GO_SERVICE_SCHEMA) hass.services.register( DOMAIN, SERVICE_UNLATCH, service_handler, - descriptions.get(SERVICE_UNLATCH), schema=UNLATCH_SERVICE_SCHEMA) + schema=UNLATCH_SERVICE_SCHEMA) class NukiLock(LockDevice): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 502592ac6f3..118a8d8f664 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/lock.wink/ """ import asyncio import logging -from os import path import voluptuous as vol @@ -14,7 +13,6 @@ from homeassistant.components.lock import LockDevice from homeassistant.components.wink import WinkDevice, DOMAIN import homeassistant.helpers.config_validation as cv from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, ATTR_CODE -from homeassistant.config import load_yaml_config_file DEPENDENCIES = ['wink'] @@ -99,37 +97,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): code = service.data.get(ATTR_CODE) lock.add_new_key(code, name) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_SET_VACATION_MODE, service_handle, - descriptions.get(SERVICE_SET_VACATION_MODE), schema=SET_ENABLED_SCHEMA) hass.services.register(DOMAIN, SERVICE_SET_ALARM_STATE, service_handle, - descriptions.get(SERVICE_SET_ALARM_STATE), schema=SET_ENABLED_SCHEMA) hass.services.register(DOMAIN, SERVICE_SET_BEEPER_STATE, service_handle, - descriptions.get(SERVICE_SET_BEEPER_STATE), schema=SET_ENABLED_SCHEMA) hass.services.register(DOMAIN, SERVICE_SET_ALARM_MODE, service_handle, - descriptions.get(SERVICE_SET_ALARM_MODE), schema=SET_ALARM_MODES_SCHEMA) hass.services.register(DOMAIN, SERVICE_SET_ALARM_SENSITIVITY, service_handle, - descriptions.get(SERVICE_SET_ALARM_SENSITIVITY), schema=SET_SENSITIVITY_SCHEMA) hass.services.register(DOMAIN, SERVICE_ADD_KEY, service_handle, - descriptions.get(SERVICE_ADD_KEY), schema=ADD_KEY_SCHEMA) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 009d4cf1069..c0560722966 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -8,13 +8,11 @@ https://home-assistant.io/components/lock.zwave/ # pylint: disable=import-error import asyncio import logging -from os import path import voluptuous as vol from homeassistant.components.lock import DOMAIN, LockDevice from homeassistant.components import zwave -from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -126,8 +124,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): yield from zwave.async_setup_platform( hass, config, async_add_devices, discovery_info) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) network = hass.data[zwave.const.DATA_NETWORK] def set_usercode(service): @@ -184,13 +180,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass.services.async_register( DOMAIN, SERVICE_SET_USERCODE, set_usercode, - descriptions.get(SERVICE_SET_USERCODE), schema=SET_USERCODE_SCHEMA) + schema=SET_USERCODE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_GET_USERCODE, get_usercode, - descriptions.get(SERVICE_GET_USERCODE), schema=GET_USERCODE_SCHEMA) + schema=GET_USERCODE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_CLEAR_USERCODE, clear_usercode, - descriptions.get(SERVICE_CLEAR_USERCODE), schema=CLEAR_USERCODE_SCHEMA) + schema=CLEAR_USERCODE_SCHEMA) def get_device(node, values, **kwargs): diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 6b79bd40987..21898f7b16d 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -6,12 +6,10 @@ https://home-assistant.io/components/logger/ """ import asyncio import logging -import os from collections import OrderedDict import voluptuous as vol -from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv DOMAIN = 'logger' @@ -123,13 +121,8 @@ def async_setup(hass, config): """Handle logger services.""" set_log_levels(service.data) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml')) - hass.services.async_register( DOMAIN, SERVICE_SET_LEVEL, async_service_handler, - descriptions[DOMAIN].get(SERVICE_SET_LEVEL), schema=SERVICE_SET_LEVEL_SCHEMA) return True diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 669390b3b90..645a418cf8c 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_extractor/ """ import logging -import os import voluptuous as vol @@ -13,10 +12,9 @@ from homeassistant.components.media_player import ( ATTR_ENTITY_ID, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, SERVICE_PLAY_MEDIA) -from homeassistant.config import load_yaml_config_file from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2017.12.10'] +REQUIREMENTS = ['youtube_dl==2017.12.28'] _LOGGER = logging.getLogger(__name__) @@ -38,18 +36,11 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the media extractor service.""" - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), - 'media_player', 'services.yaml')) - def play_media(call): """Get stream URL and send it to the play_media service.""" MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() - hass.services.register(DOMAIN, - SERVICE_PLAY_MEDIA, - play_media, - description=descriptions[SERVICE_PLAY_MEDIA], + hass.services.register(DOMAIN, SERVICE_PLAY_MEDIA, play_media, schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA) return True diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 89686c312bd..44e6810fd5d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -10,7 +10,6 @@ import functools as ft import collections import hashlib import logging -import os from random import SystemRandom from aiohttp import web @@ -19,7 +18,6 @@ import async_timeout import voluptuous as vol from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, @@ -372,10 +370,6 @@ def async_setup(hass, 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 def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" @@ -418,7 +412,7 @@ def async_setup(hass, config): 'schema', MEDIA_PLAYER_SCHEMA) hass.services.async_register( DOMAIN, service, async_service_handler, - descriptions.get(service), schema=schema) + schema=schema) return True diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 6ae44495e3e..2aaff646885 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -20,9 +20,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -# Do not upgrade to 1.0.2, it breaks a bunch of stuff -# https://github.com/home-assistant/home-assistant/issues/10926 -REQUIREMENTS = ['pychromecast==0.8.2'] +REQUIREMENTS = ['pychromecast==1.0.3'] _LOGGER = logging.getLogger(__name__) @@ -188,7 +186,7 @@ class CastDevice(MediaPlayerDevice): images = self.media_status.images - return images[0].url if images else None + return images[0].url if images and images[0].url else None @property def media_title(self): diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 00dd90938c8..2c428c6b833 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -10,19 +10,17 @@ from functools import wraps import logging import urllib import re -import os import aiohttp import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, - SUPPORT_TURN_ON) + MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST, + MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, @@ -73,6 +71,8 @@ MEDIA_TYPES = { 'tvshow': MEDIA_TYPE_TVSHOW, 'season': MEDIA_TYPE_TVSHOW, 'episode': MEDIA_TYPE_TVSHOW, + # Type 'channel' is used for radio or tv streams from pvr + 'channel': MEDIA_TYPE_CHANNEL, } SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -207,15 +207,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): return - 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: schema = SERVICE_TO_METHOD[service]['schema'] hass.services.async_register( DOMAIN, service, async_service_handler, - description=descriptions.get(service), schema=schema) + schema=schema) def cmd(func): diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 10b4b8414d8..c95ddcab97e 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -8,15 +8,16 @@ import logging import voluptuous as vol -from homeassistant.const import (CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) +from homeassistant.const import (ATTR_ENTITY_ID, CONF_NAME, CONF_PORT, + STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_VOLUME_MUTE, - SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + DOMAIN, MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, + SUPPORT_VOLUME_MUTE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) -REQUIREMENTS = ['pymonoprice==0.2'] +REQUIREMENTS = ['pymonoprice==0.3'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,11 @@ SOURCE_SCHEMA = vol.Schema({ CONF_ZONES = 'zones' CONF_SOURCES = 'sources' +DATA_MONOPRICE = 'monoprice' + +SERVICE_SNAPSHOT = 'snapshot' +SERVICE_RESTORE = 'restore' + # Valid zone ids: 11-16 or 21-26 or 31-36 ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16), vol.Range(min=21, max=26), @@ -56,9 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get(CONF_PORT) from serial import SerialException - from pymonoprice import Monoprice + from pymonoprice import get_monoprice try: - monoprice = Monoprice(port) + monoprice = get_monoprice(port) except SerialException: _LOGGER.error('Error connecting to Monoprice controller.') return @@ -66,10 +72,38 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sources = {source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} + hass.data[DATA_MONOPRICE] = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - add_devices([MonopriceZone(monoprice, sources, - zone_id, extra[CONF_NAME])], True) + hass.data[DATA_MONOPRICE].append(MonopriceZone(monoprice, sources, + zone_id, + extra[CONF_NAME])) + + add_devices(hass.data[DATA_MONOPRICE], True) + + def service_handle(service): + """Handle for services.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + if entity_ids: + devices = [device for device in hass.data[DATA_MONOPRICE] + if device.entity_id in entity_ids] + else: + devices = hass.data[DATA_MONOPRICE] + + for device in devices: + if service.service == SERVICE_SNAPSHOT: + device.snapshot() + elif service.service == SERVICE_RESTORE: + device.restore() + + hass.services.register( + DOMAIN, SERVICE_SNAPSHOT, service_handle, + schema=MEDIA_PLAYER_SCHEMA) + + hass.services.register( + DOMAIN, SERVICE_RESTORE, service_handle, + schema=MEDIA_PLAYER_SCHEMA) class MonopriceZone(MediaPlayerDevice): @@ -90,6 +124,7 @@ class MonopriceZone(MediaPlayerDevice): self._zone_id = zone_id self._name = zone_name + self._snapshot = None self._state = None self._volume = None self._source = None @@ -152,6 +187,16 @@ class MonopriceZone(MediaPlayerDevice): """List of available input sources.""" return self._source_names + def snapshot(self): + """Save zone's current state.""" + self._snapshot = self._monoprice.zone_status(self._zone_id) + + def restore(self): + """Restore saved state.""" + if self._snapshot: + self._monoprice.restore_zone(self._snapshot) + self.schedule_update_ha_state(True) + def select_source(self, source): """Set input source.""" if source not in self._source_name_id: diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 9b984813ff6..c96b0f3c2ae 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -23,7 +23,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['plexapi==3.0.3'] +REQUIREMENTS = ['plexapi==3.0.5'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -156,7 +156,7 @@ def setup_plexserver( if device.machineIdentifier not in plex_clients: new_client = PlexClient(config, device, None, plex_sessions, update_devices, - update_sessions, plexserver) + update_sessions) plex_clients[device.machineIdentifier] = new_client new_plex_clients.append(new_client) else: @@ -169,7 +169,7 @@ def setup_plexserver( and machine_identifier is not None): new_client = PlexClient(config, None, session, plex_sessions, update_devices, - update_sessions, plexserver) + update_sessions) plex_clients[machine_identifier] = new_client new_plex_clients.append(new_client) else: @@ -227,7 +227,7 @@ def request_configuration(host, hass, config, add_devices_callback): _CONFIGURING[host] = configurator.request_config( 'Plex Media Server', plex_configuration_callback, - description=('Enter the X-Plex-Token'), + description='Enter the X-Plex-Token', entity_picture='/static/images/logo_plex_mediaserver.png', submit_caption='Confirm', fields=[{ @@ -249,10 +249,9 @@ class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" def __init__(self, config, device, session, plex_sessions, - update_devices, update_sessions, plex_server): + update_devices, update_sessions): """Initialize the Plex device.""" self._app_name = '' - self._server = plex_server self._device = None self._device_protocol_capabilities = None self._is_player_active = False @@ -273,8 +272,23 @@ class PlexClient(MediaPlayerDevice): self.plex_sessions = plex_sessions self.update_devices = update_devices self.update_sessions = update_sessions - - self._clear_media() + # General + self._media_content_id = None + self._media_content_rating = None + self._media_content_type = None + self._media_duration = None + self._media_image_url = None + self._media_title = None + self._media_position = None + # Music + self._media_album_artist = None + self._media_album_name = None + self._media_artist = None + self._media_track = None + # TV Show + self._media_episode = None + self._media_season = None + self._media_series_title = None self.refresh(device, session) @@ -296,7 +310,7 @@ class PlexClient(MediaPlayerDevice): 'media_player', prefix, self.name.lower().replace('-', '_')) - def _clear_media(self): + def _clear_media_details(self): """Set all Media Items to None.""" # General self._media_content_id = None @@ -316,10 +330,13 @@ class PlexClient(MediaPlayerDevice): self._media_season = None self._media_series_title = None + # Clear library Name + self._app_name = '' + def refresh(self, device, session): """Refresh key device data.""" # new data refresh - self._clear_media() + self._clear_media_details() if session: # Not being triggered by Chrome or FireTablet Plex App self._session = session @@ -355,6 +372,35 @@ class PlexClient(MediaPlayerDevice): self._media_content_id = self._session.ratingKey self._media_content_rating = self._session.contentRating + self._set_player_state() + + if self._is_player_active and self._session is not None: + self._session_type = self._session.type + self._media_duration = self._session.duration + # title (movie name, tv episode name, music song name) + self._media_title = self._session.title + # media type + self._set_media_type() + self._app_name = self._session.section().title \ + if self._session.section() is not None else '' + self._set_media_image() + else: + self._session_type = None + + def _set_media_image(self): + thumb_url = self._session.thumbUrl + if (self.media_content_type is MEDIA_TYPE_TVSHOW + and not self.config.get(CONF_USE_EPISODE_ART)): + thumb_url = self._session.url(self._session.grandparentThumb) + + if thumb_url is None: + _LOGGER.debug("Using media art because media thumb " + "was not found: %s", self.entity_id) + thumb_url = self.session.url(self._session.art) + + self._media_image_url = thumb_url + + def _set_player_state(self): if self._player_state == 'playing': self._is_player_active = True self._state = STATE_PLAYING @@ -368,38 +414,14 @@ class PlexClient(MediaPlayerDevice): self._is_player_active = False self._state = STATE_OFF - if self._is_player_active and self._session is not None: - self._session_type = self._session.type - self._media_duration = self._session.duration - else: - self._session_type = None - - # media type - if self._session_type == 'clip': - _LOGGER.debug("Clip content type detected, compatibility may " - "vary: %s", self.entity_id) + def _set_media_type(self): + if self._session_type in ['clip', 'episode']: self._media_content_type = MEDIA_TYPE_TVSHOW - elif self._session_type == 'episode': - self._media_content_type = MEDIA_TYPE_TVSHOW - elif self._session_type == 'movie': - self._media_content_type = MEDIA_TYPE_VIDEO - elif self._session_type == 'track': - self._media_content_type = MEDIA_TYPE_MUSIC - # title (movie name, tv episode name, music song name) - if self._session and self._is_player_active: - self._media_title = self._session.title - - # Movies - if (self.media_content_type == MEDIA_TYPE_VIDEO and - self._session.year is not None): - self._media_title += ' (' + str(self._session.year) + ')' - - # TV Show - if self._media_content_type is MEDIA_TYPE_TVSHOW: # season number (00) - if callable(self._session.seasons): - self._media_season = self._session.seasons()[0].index.zfill(2) + if callable(self._session.season): + self._media_season = str( + (self._session.season()).index).zfill(2) elif self._session.parentIndex is not None: self._media_season = self._session.parentIndex.zfill(2) else: @@ -410,8 +432,14 @@ class PlexClient(MediaPlayerDevice): if self._session.index is not None: self._media_episode = str(self._session.index).zfill(2) - # Music - if self._media_content_type == MEDIA_TYPE_MUSIC: + elif self._session_type == 'movie': + self._media_content_type = MEDIA_TYPE_VIDEO + if self._session.year is not None and \ + self._media_title is not None: + self._media_title += ' (' + str(self._session.year) + ')' + + elif self._session_type == 'track': + self._media_content_type = MEDIA_TYPE_MUSIC self._media_album_name = self._session.parentTitle self._media_album_artist = self._session.grandparentTitle self._media_track = self._session.index @@ -422,33 +450,11 @@ class PlexClient(MediaPlayerDevice): "was not found: %s", self.entity_id) self._media_artist = self._media_album_artist - # set app name to library name - if (self._session is not None - and self._session.section() is not None): - self._app_name = self._session.section().title - else: - self._app_name = '' - - # media image url - if self._session is not None: - thumb_url = self._session.thumbUrl - if (self.media_content_type is MEDIA_TYPE_TVSHOW - and not self.config.get(CONF_USE_EPISODE_ART)): - thumb_url = self._server.url( - self._session.grandparentThumb) - - if thumb_url is None: - _LOGGER.debug("Using media art because media thumb " - "was not found: %s", self.entity_id) - thumb_url = self._server.url(self._session.art) - - self._media_image_url = thumb_url - def force_idle(self): """Force client to idle.""" self._state = STATE_IDLE self._session = None - self._clear_media() + self._clear_media_details() @property def unique_id(self): @@ -792,9 +798,10 @@ class PlexClient(MediaPlayerDevice): @property def device_state_attributes(self): """Return the scene state attributes.""" - attr = {} - attr['media_content_rating'] = self._media_content_rating - attr['session_username'] = self._session_username - attr['media_library_name'] = self._app_name + attr = { + 'media_content_rating': self._media_content_rating, + 'session_username': self._session_username, + 'media_library_name': self._app_name + } return attr diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b2f98d378cf..fe8280fb2ab 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -107,6 +107,20 @@ media_seek: description: Position to seek to. The format is platform dependent. example: 100 +monoprice_snapshot: + description: Take a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be snapshot. Platform dependent. + example: 'media_player.living_room' + +monoprice_restore: + description: Restore a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be restored. Platform dependent. + example: 'media_player.living_room' + play_media: description: Send the media player the command for playing media. fields: @@ -293,3 +307,16 @@ kodi_call_method: method: description: Name of the Kodi JSONRPC API method to be called. example: 'VideoLibrary.GetRecentlyAddedEpisodes' + +squeezebox_call_method: + description: 'Call a Squeezebox JSON/RPC API method.' + fields: + entity_id: + description: Name(s) of the Squeexebox entities where to run the API method. + example: 'media_player.squeezebox_radio' + command: + description: Name of the Squeezebox command. + example: 'playlist' + parameters: + description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. + example: '["loadtracks", "track.titlesearch=highway to hell"]' diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 54015bec277..220f1691c52 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.snapcast/ """ import asyncio import logging -from os import path import socket import voluptuous as vol @@ -18,7 +17,6 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID) import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file REQUIREMENTS = ['snapcast==2.0.8'] @@ -69,14 +67,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): elif service.service == SERVICE_RESTORE: yield from device.async_restore() - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) hass.services.async_register( DOMAIN, SERVICE_SNAPSHOT, _handle_service, - descriptions.get(SERVICE_SNAPSHOT), schema=SERVICE_SCHEMA) + schema=SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_RESTORE, _handle_service, - descriptions.get(SERVICE_RESTORE), schema=SERVICE_SCHEMA) + schema=SERVICE_SCHEMA) try: server = yield from snapcast.control.create_server( diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 3bd3a722b46..0c6d380e81e 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -8,7 +8,6 @@ import asyncio import datetime import functools as ft import logging -from os import path import socket import urllib @@ -23,7 +22,6 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID, CONF_HOSTS, ATTR_TIME) -from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -171,9 +169,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(slaves, True) _LOGGER.info("Added %s Sonos speakers", len(players)) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - def service_handle(service): """Handle for services.""" entity_ids = service.data.get('entity_id') @@ -207,36 +202,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register( DOMAIN, SERVICE_JOIN, service_handle, - descriptions.get(SERVICE_JOIN), schema=SONOS_JOIN_SCHEMA) + schema=SONOS_JOIN_SCHEMA) hass.services.register( DOMAIN, SERVICE_UNJOIN, service_handle, - descriptions.get(SERVICE_UNJOIN), schema=SONOS_SCHEMA) + schema=SONOS_SCHEMA) hass.services.register( DOMAIN, SERVICE_SNAPSHOT, service_handle, - descriptions.get(SERVICE_SNAPSHOT), schema=SONOS_STATES_SCHEMA) + schema=SONOS_STATES_SCHEMA) hass.services.register( DOMAIN, SERVICE_RESTORE, service_handle, - descriptions.get(SERVICE_RESTORE), schema=SONOS_STATES_SCHEMA) + schema=SONOS_STATES_SCHEMA) hass.services.register( DOMAIN, SERVICE_SET_TIMER, service_handle, - descriptions.get(SERVICE_SET_TIMER), schema=SONOS_SET_TIMER_SCHEMA) + schema=SONOS_SET_TIMER_SCHEMA) hass.services.register( DOMAIN, SERVICE_CLEAR_TIMER, service_handle, - descriptions.get(SERVICE_CLEAR_TIMER), schema=SONOS_SCHEMA) + schema=SONOS_SCHEMA) hass.services.register( DOMAIN, SERVICE_UPDATE_ALARM, service_handle, - descriptions.get(SERVICE_UPDATE_ALARM), schema=SONOS_UPDATE_ALARM_SCHEMA) hass.services.register( DOMAIN, SERVICE_SET_OPTION, service_handle, - descriptions.get(SERVICE_SET_OPTION), schema=SONOS_SET_OPTION_SCHEMA) diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index c04d3b4d77f..790ad8b8e29 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.soundtouch/ """ import logging -from os import path import re import voluptuous as vol @@ -16,7 +15,6 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) -from homeassistant.config import load_yaml_config_file from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) @@ -107,9 +105,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) add_devices([soundtouch_device]) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - def service_handle(service): """Handle the applying of a service.""" master_device_id = service.data.get('master') @@ -140,19 +135,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE, service_handle, - descriptions.get(SERVICE_PLAY_EVERYWHERE), schema=SOUNDTOUCH_PLAY_EVERYWHERE) hass.services.register(DOMAIN, SERVICE_CREATE_ZONE, service_handle, - descriptions.get(SERVICE_CREATE_ZONE), schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, service_handle, - descriptions.get(SERVICE_REMOVE_ZONE_SLAVE), schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE, service_handle, - descriptions.get(SERVICE_ADD_ZONE_SLAVE), schema=SOUNDTOUCH_ADD_ZONE_SCHEMA) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index a4a15fbce24..13f05cc59f7 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -17,10 +17,11 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_ENQUEUE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice, + MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_SHUFFLE_SET, SUPPORT_CLEAR_PLAYLIST) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, CONF_PORT) + STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, CONF_PORT, ATTR_COMMAND) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.dt import utcnow @@ -33,7 +34,7 @@ TIMEOUT = 10 SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ - SUPPORT_PLAY + SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -42,12 +43,33 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_USERNAME): cv.string, }) +SERVICE_CALL_METHOD = 'squeezebox_call_method' + +DATA_SQUEEZEBOX = 'squeexebox' + +ATTR_PARAMETERS = 'parameters' + +SQUEEZEBOX_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMETERS): + vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), +}) + +SERVICE_TO_METHOD = { + SERVICE_CALL_METHOD: { + 'method': 'async_call_method', + 'schema': SQUEEZEBOX_CALL_METHOD_SCHEMA}, +} + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the squeezebox platform.""" import socket + if DATA_SQUEEZEBOX not in hass.data: + hass.data[DATA_SQUEEZEBOX] = [] + username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -74,8 +96,40 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): lms = LogitechMediaServer(hass, host, port, username, password) players = yield from lms.create_players() + + hass.data[DATA_SQUEEZEBOX].extend(players) async_add_devices(players) + @asyncio.coroutine + def async_service_handler(service): + """Map services to methods on MediaPlayerDevice.""" + method = SERVICE_TO_METHOD.get(service.service) + if not method: + return + + params = {key: value for key, value in service.data.items() + if key != 'entity_id'} + entity_ids = service.data.get('entity_id') + if entity_ids: + target_players = [player for player in hass.data[DATA_SQUEEZEBOX] + if player.entity_id in entity_ids] + else: + target_players = hass.data[DATA_SQUEEZEBOX] + + update_tasks = [] + for player in target_players: + yield from getattr(player, method['method'])(**params) + update_tasks.append(player.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]['schema'] + hass.services.async_register( + DOMAIN, service, async_service_handler, + schema=schema) + return True @@ -305,6 +359,12 @@ class SqueezeBoxDevice(MediaPlayerDevice): if 'album' in self._status: return self._status['album'] + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + if 'playlist_shuffle' in self._status: + return self._status['playlist_shuffle'] == 1 + @property def supported_features(self): """Flag media player features that are supported.""" @@ -415,3 +475,24 @@ class SqueezeBoxDevice(MediaPlayerDevice): def _add_uri_to_playlist(self, media_id): """Add a items to the existing playlist.""" return self.async_query('playlist', 'add', media_id) + + def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + return self.async_query('playlist', 'shuffle', int(shuffle)) + + def async_clear_playlist(self): + """Send the media player the command for clear playlist.""" + return self.async_query('playlist', 'clear') + + def async_call_method(self, command, parameters=None): + """ + Call Squeezebox JSON/RPC method. + + Escaped optional parameters are added to the command to form the list + of positional parameters (p0, p1..., pN) passed to JSON/RPC server. + """ + all_params = [command] + if parameters: + for parameter in parameters: + all_params.append(urllib.parse.quote(parameter, safe=':=')) + return self.async_query(*all_params) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 861e75ac144..10f7adccae0 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -27,6 +27,7 @@ SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ CONF_SOURCE_NAMES = 'source_names' CONF_SOURCE_IGNORE = 'source_ignore' +CONF_ZONE_NAMES = 'zone_names' CONF_ZONE_IGNORE = 'zone_ignore' DEFAULT_NAME = 'Yamaha Receiver' @@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ZONE_IGNORE, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SOURCE_NAMES, default={}): {cv.string: cv.string}, + vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string}, }) @@ -57,6 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): source_ignore = config.get(CONF_SOURCE_IGNORE) source_names = config.get(CONF_SOURCE_NAMES) zone_ignore = config.get(CONF_ZONE_IGNORE) + zone_names = config.get(CONF_ZONE_NAMES) if discovery_info is not None: name = discovery_info.get('name') @@ -84,14 +87,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if receiver.zone not in zone_ignore: hass.data[KNOWN].add(receiver.ctrl_url) add_devices([ - YamahaDevice(name, receiver, source_ignore, source_names) + YamahaDevice(name, receiver, source_ignore, + source_names, zone_names) ], True) class YamahaDevice(MediaPlayerDevice): """Representation of a Yamaha device.""" - def __init__(self, name, receiver, source_ignore, source_names): + def __init__(self, name, receiver, source_ignore, + source_names, zone_names): """Initialize the Yamaha Receiver.""" self._receiver = receiver self._muted = False @@ -101,6 +106,7 @@ class YamahaDevice(MediaPlayerDevice): self._source_list = None self._source_ignore = source_ignore or [] self._source_names = source_names or {} + self._zone_names = zone_names or {} self._reverse_mapping = None self._playback_support = None self._is_playback_supported = False @@ -148,9 +154,10 @@ class YamahaDevice(MediaPlayerDevice): def name(self): """Return the name of the device.""" name = self._name - if self._zone != "Main_Zone": + zone_name = self._zone_names.get(self._zone, self._zone) + if zone_name != "Main_Zone": # Zone will be one of Main_Zone, Zone_2, Zone_3 - name += " " + self._zone.replace('_', ' ') + name += " " + zone_name.replace('_', ' ') return name @property diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 49d79ccaea0..829c1124363 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/microsoft_face/ import asyncio import json import logging -import os import aiohttp from aiohttp.hdrs import CONTENT_TYPE @@ -15,7 +14,6 @@ import async_timeout import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT -from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -133,10 +131,6 @@ def async_setup(hass, config): hass.data[DATA_MICROSOFT_FACE] = face - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - @asyncio.coroutine def async_create_group(service): """Create a new person group.""" @@ -155,7 +149,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_CREATE_GROUP, async_create_group, - descriptions[DOMAIN].get(SERVICE_CREATE_GROUP), schema=SCHEMA_GROUP_SERVICE) @asyncio.coroutine @@ -174,7 +167,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, - descriptions[DOMAIN].get(SERVICE_DELETE_GROUP), schema=SCHEMA_GROUP_SERVICE) @asyncio.coroutine @@ -190,7 +182,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, - descriptions[DOMAIN].get(SERVICE_TRAIN_GROUP), schema=SCHEMA_TRAIN_SERVICE) @asyncio.coroutine @@ -211,7 +202,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_CREATE_PERSON, async_create_person, - descriptions[DOMAIN].get(SERVICE_CREATE_PERSON), schema=SCHEMA_PERSON_SERVICE) @asyncio.coroutine @@ -232,7 +222,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, - descriptions[DOMAIN].get(SERVICE_DELETE_PERSON), schema=SCHEMA_PERSON_SERVICE) @asyncio.coroutine @@ -259,7 +248,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_FACE_PERSON, async_face_person, - descriptions[DOMAIN].get(SERVICE_FACE_PERSON), schema=SCHEMA_FACE_SERVICE) return True diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 001c8d1188a..a928c0d3aca 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -6,12 +6,10 @@ https://home-assistant.io/components/modbus/ """ import logging import threading -import os import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_METHOD, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, ATTR_STATE) @@ -40,7 +38,7 @@ SERIAL_SCHEMA = { ETHERNET_SCHEMA = { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.positive_int, - vol.Required(CONF_TYPE): vol.Any('tcp', 'udp'), + vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'), vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, } @@ -92,6 +90,13 @@ def setup(hass, config): bytesize=config[DOMAIN][CONF_BYTESIZE], parity=config[DOMAIN][CONF_PARITY], timeout=config[DOMAIN][CONF_TIMEOUT]) + elif client_type == 'rtuovertcp': + from pymodbus.client.sync import ModbusTcpClient as ModbusClient + from pymodbus.transaction import ModbusRtuFramer as ModbusFramer + client = ModbusClient(host=config[DOMAIN][CONF_HOST], + port=config[DOMAIN][CONF_PORT], + framer=ModbusFramer, + timeout=config[DOMAIN][CONF_TIMEOUT]) elif client_type == 'tcp': from pymodbus.client.sync import ModbusTcpClient as ModbusClient client = ModbusClient(host=config[DOMAIN][CONF_HOST], @@ -117,17 +122,12 @@ def setup(hass, config): HUB.connect() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - descriptions = load_yaml_config_file(os.path.join( - os.path.dirname(__file__), 'services.yaml')).get(DOMAIN) - # Register services for modbus hass.services.register( DOMAIN, SERVICE_WRITE_REGISTER, write_register, - descriptions.get(SERVICE_WRITE_REGISTER), schema=SERVICE_WRITE_REGISTER_SCHEMA) hass.services.register( DOMAIN, SERVICE_WRITE_COIL, write_coil, - descriptions.get(SERVICE_WRITE_COIL), schema=SERVICE_WRITE_COIL_SCHEMA) def write_register(service): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3a6abec0ddf..cdf59b92606 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -17,12 +17,12 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.setup import async_prepare_setup_platform -from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity from homeassistant.util.async import ( run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.const import ( @@ -59,6 +59,8 @@ CONF_WILL_MESSAGE = 'will_message' CONF_STATE_TOPIC = 'state_topic' CONF_COMMAND_TOPIC = 'command_topic' CONF_AVAILABILITY_TOPIC = 'availability_topic' +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_QOS = 'qos' CONF_RETAIN = 'retain' @@ -73,6 +75,8 @@ DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_DISCOVERY = False DEFAULT_DISCOVERY_PREFIX = 'homeassistant' DEFAULT_TLS_PROTOCOL = 'auto' +DEFAULT_PAYLOAD_AVAILABLE = 'online' +DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' @@ -145,6 +149,14 @@ SCHEMA_BASE = { vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, } +MQTT_AVAILABILITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD_AVAILABLE, + default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, +}) + MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events @@ -410,13 +422,9 @@ def async_setup(hass, config): yield from hass.data[DATA_MQTT].async_publish( msg_topic, payload, qos, retain) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml')) - hass.services.async_register( DOMAIN, SERVICE_PUBLISH, async_publish_service, - descriptions.get(SERVICE_PUBLISH), schema=MQTT_PUBLISH_SCHEMA) + schema=MQTT_PUBLISH_SCHEMA) if conf.get(CONF_DISCOVERY): yield from _async_setup_discovery(hass, config) @@ -653,3 +661,41 @@ def _match_topic(subscription, topic): reg = re.compile(reg_ex) return reg.match(topic) is not None + + +class MqttAvailability(Entity): + """Mixin used for platforms that report availability.""" + + def __init__(self, availability_topic, qos, payload_available, + payload_not_available): + """Initialize the availability mixin.""" + self._availability_topic = availability_topic + self._availability_qos = qos + self._available = availability_topic is None + self._payload_available = payload_available + self._payload_not_available = payload_not_available + + def async_added_to_hass(self): + """Subscribe mqtt events. + + This method must be run in the event loop and returns a coroutine. + """ + @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 async_subscribe( + self.hass, self._availability_topic, + availability_message_received, self. _availability_qos) + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 9496ff1d596..41198d1f296 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/notify/ """ import asyncio import logging -import os from functools import partial import voluptuous as vol @@ -15,7 +14,6 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_per_platform, discovery from homeassistant.util import slugify @@ -71,10 +69,6 @@ def send_message(hass, message, title=None, data=None): @asyncio.coroutine def async_setup(hass, config): """Set up the notify services.""" - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - targets = {} @asyncio.coroutine @@ -151,7 +145,6 @@ def async_setup(hass, config): targets[target_name] = target hass.services.async_register( DOMAIN, target_name, async_notify_message, - descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA) platform_name = ( @@ -161,7 +154,7 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, platform_name_slug, async_notify_message, - descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA) + schema=NOTIFY_SERVICE_SCHEMA) return True diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index f6f7cc71f14..6ef758b7bb5 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -45,9 +45,6 @@ REGISTER_SERVICE_SCHEMA = vol.Schema({ def get_service(hass, config, discovery_info=None): """Return push service.""" - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - name = config.get(CONF_NAME) cert_file = config.get(CONF_CERTFILE) topic = config.get(CONF_TOPIC) @@ -56,7 +53,7 @@ def get_service(hass, config, discovery_info=None): service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) hass.services.register( DOMAIN, 'apns_{}'.format(name), service.register, - descriptions.get(SERVICE_REGISTER), schema=REGISTER_SERVICE_SCHEMA) + schema=REGISTER_SERVICE_SCHEMA) return service diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 2314722a2ab..f2611cf65d3 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -27,7 +27,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string -REQUIREMENTS = ['pywebpush==1.3.0', 'PyJWT==1.5.3'] +REQUIREMENTS = ['pywebpush==1.5.0', 'PyJWT==1.5.3'] DEPENDENCIES = ['frontend'] @@ -169,15 +169,35 @@ class HTML5PushRegistrationView(HomeAssistantView): return self.json_message( humanize_error(data, ex), HTTP_BAD_REQUEST) - name = ensure_unique_string('unnamed device', self.registrations) + name = self.find_registration_name(data) + previous_registration = self.registrations.get(name) self.registrations[name] = data - if not save_json(self.json_path, self.registrations): + try: + hass = request.app['hass'] + + yield from hass.async_add_job(save_json, self.json_path, + self.registrations) + return self.json_message( + 'Push notification subscriber registered.') + except HomeAssistantError: + if previous_registration is not None: + self.registrations[name] = previous_registration + else: + self.registrations.pop(name) + return self.json_message( 'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR) - return self.json_message('Push notification subscriber registered.') + def find_registration_name(self, data): + """Find a registration name matching data or generate a unique one.""" + endpoint = data.get(ATTR_SUBSCRIPTION).get(ATTR_ENDPOINT) + for key, registration in self.registrations.items(): + subscription = registration.get(ATTR_SUBSCRIPTION) + if subscription.get(ATTR_ENDPOINT) == endpoint: + return key + return ensure_unique_string('unnamed device', self.registrations) @asyncio.coroutine def delete(self, request): @@ -202,7 +222,12 @@ class HTML5PushRegistrationView(HomeAssistantView): reg = self.registrations.pop(found) - if not save_json(self.json_path, self.registrations): + try: + hass = request.app['hass'] + + yield from hass.async_add_job(save_json, self.json_path, + self.registrations) + except HomeAssistantError: self.registrations[found] = reg return self.json_message( 'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 0e846ebaf84..359810bb6bc 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_URL = 'url' ATTR_FILE = 'file' ATTR_FILE_URL = 'file_url' +ATTR_LIST = 'list' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -99,7 +100,7 @@ class PushBulletNotificationService(BaseNotificationService): continue # Target is email, send directly, don't use a target object. - # This also seems works to send to all devices in own account. + # This also seems to work to send to all devices in own account. if ttype == 'email': self._push_data(message, title, data, self.pushbullet, tname) _LOGGER.info("Sent notification to email %s", tname) @@ -127,20 +128,18 @@ class PushBulletNotificationService(BaseNotificationService): _LOGGER.error("No such target: %s/%s", ttype, tname) continue - def _push_data(self, message, title, data, pusher, tname=None): + def _push_data(self, message, title, data, pusher, email=None): """Helper for creating the message content.""" from pushbullet import PushError if data is None: data = {} + data_list = data.get(ATTR_LIST) url = data.get(ATTR_URL) filepath = data.get(ATTR_FILE) file_url = data.get(ATTR_FILE_URL) try: if url: - if tname: - pusher.push_link(title, url, body=message, email=tname) - else: - pusher.push_link(title, url, body=message) + pusher.push_link(title, url, body=message, email=email) elif filepath: if not self.hass.config.is_allowed_path(filepath): _LOGGER.error("Filepath is not valid or allowed") @@ -150,18 +149,20 @@ class PushBulletNotificationService(BaseNotificationService): if filedata.get('file_type') == 'application/x-empty': _LOGGER.error("Can not send an empty file") return - pusher.push_file(title=title, body=message, **filedata) + + pusher.push_file(title=title, body=message, + email=email, **filedata) elif file_url: if not file_url.startswith('http'): _LOGGER.error("URL should start with http or https") return - pusher.push_file(title=title, body=message, file_name=file_url, - file_url=file_url, - file_type=mimetypes.guess_type(file_url)[0]) + pusher.push_file(title=title, body=message, email=email, + file_name=file_url, file_url=file_url, + file_type=(mimetypes + .guess_type(file_url)[0])) + elif data_list: + pusher.push_note(title, data_list, email=email) else: - if tname: - pusher.push_note(title, message, email=tname) - else: - pusher.push_note(title, message) + pusher.push_note(title, message, email=email) except PushError as err: _LOGGER.error("Notify failed: %s", err) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index c70b198a333..78c43c5f0ad 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.webostv/ """ import logging -import os import voluptuous as vol @@ -19,14 +18,11 @@ REQUIREMENTS = ['pylgtv==0.1.7'] _LOGGER = logging.getLogger(__name__) WEBOSTV_CONFIG_FILE = 'webostv.conf' -HOME_ASSISTANT_ICON_PATH = os.path.join(os.path.dirname(__file__), '..', - 'frontend', 'www_static', 'icons', - 'favicon-1024x1024.png') PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_ICON, default=HOME_ASSISTANT_ICON_PATH): cv.string + vol.Optional(CONF_ICON): cv.string }) @@ -36,7 +32,8 @@ def get_service(hass, config, discovery_info=None): from pylgtv import PyLGTVPairException path = hass.config.path(config.get(CONF_FILENAME)) - client = WebOsClient(config.get(CONF_HOST), key_file_path=path) + client = WebOsClient(config.get(CONF_HOST), key_file_path=path, + timeout_connect=8) if not client.is_registered(): try: diff --git a/homeassistant/components/nuheat.py b/homeassistant/components/nuheat.py new file mode 100644 index 00000000000..fb14f119dbd --- /dev/null +++ b/homeassistant/components/nuheat.py @@ -0,0 +1,45 @@ +""" +Support for NuHeat thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/nuheat/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery + +REQUIREMENTS = ["nuheat==0.3.0"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "nuheat" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the NuHeat thermostat component.""" + import nuheat + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + devices = conf.get(CONF_DEVICES) + + api = nuheat.NuHeat(username, password) + api.authenticate() + hass.data[DOMAIN] = (api, devices) + + discovery.load_platform(hass, "climate", DOMAIN, {}, config) + return True diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 086242ab070..5caaa1b372d 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -69,7 +69,6 @@ class OctoPrintAPI(object): self.job_error_logged = False self.bed = bed self.number_of_tools = number_of_tools - _LOGGER.error(str(bed) + " " + str(number_of_tools)) def get_tools(self): """Get the list of tools that temperature is monitored on.""" @@ -118,9 +117,7 @@ class OctoPrintAPI(object): self.job_error_logged = False self.printer_error_logged = False return response.json() - except (requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - requests.exceptions.ReadTimeout) as conn_exc: + except Exception as conn_exc: # pylint: disable=broad-except log_string = "Failed to update OctoPrint status. " + \ " Error: %s" % (conn_exc) # Only log the first failure diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index aaba6e42de3..cce3550d35c 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/persistent_notification/ """ import asyncio -import os import logging import voluptuous as vol @@ -16,7 +15,6 @@ from homeassistant.loader import bind_hass from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util import slugify -from homeassistant.config import load_yaml_config_file ATTR_MESSAGE = 'message' ATTR_NOTIFICATION_ID = 'notification_id' @@ -127,17 +125,10 @@ def async_setup(hass, config): hass.states.async_remove(entity_id) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml') - ) - hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service, - descriptions[SERVICE_CREATE], SCHEMA_SERVICE_CREATE) hass.services.async_register(DOMAIN, SERVICE_DISMISS, dismiss_service, - descriptions[SERVICE_DISMISS], SCHEMA_SERVICE_DISMISS) return True diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 0ecfa50ee63..f9629ca726a 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -13,14 +13,14 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.components import recorder from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, TEMP_CELSIUS, + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT) from homeassistant import core as hacore from homeassistant.helpers import state as state_helper from homeassistant.util.temperature import fahrenheit_to_celsius -REQUIREMENTS = ['prometheus_client==0.0.21'] +REQUIREMENTS = ['prometheus_client==0.1.0'] _LOGGER = logging.getLogger(__name__) @@ -181,57 +181,26 @@ class Metrics(object): pass def _handle_sensor(self, state): - _sensor_types = { - TEMP_CELSIUS: ( - 'temperature_c', self.prometheus_client.Gauge, - 'Temperature in degrees Celsius', - ), - TEMP_FAHRENHEIT: ( - 'temperature_c', self.prometheus_client.Gauge, - 'Temperature in degrees Celsius', - ), - '%': ( - 'relative_humidity', self.prometheus_client.Gauge, - 'Relative humidity (0..100)', - ), - 'lux': ( - 'light_lux', self.prometheus_client.Gauge, - 'Light level in lux', - ), - 'kWh': ( - 'electricity_used_kwh', self.prometheus_client.Gauge, - 'Electricity used by this device in KWh', - ), - 'V': ( - 'voltage', self.prometheus_client.Gauge, - 'Currently reported voltage in Volts', - ), - 'W': ( - 'electricity_usage_w', self.prometheus_client.Gauge, - 'Currently reported electricity draw in Watts', - ), - 'min': ( - 'sensor_min', self.prometheus_client.Gauge, - 'Time in minutes reported by a sensor' - ), - 'Events': ( - 'sensor_event_count', self.prometheus_client.Gauge, - 'Number of events for a sensor' - ), - } unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - metric = _sensor_types.get(unit) + metric = state.entity_id.split(".")[1] - if metric is not None: - metric = self._metric(*metric) - try: - value = state_helper.state_as_number(state) - if unit == TEMP_FAHRENHEIT: - value = fahrenheit_to_celsius(value) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass + try: + int(metric.split("_")[-1]) + metric = "_".join(metric.split("_")[:-1]) + except ValueError: + pass + + _metric = self._metric(metric, self.prometheus_client.Gauge, + state.entity_id) + + try: + value = state_helper.state_as_number(state) + if unit == TEMP_FAHRENHEIT: + value = fahrenheit_to_celsius(value) + _metric.labels(**self._labels(state)).set(value) + except ValueError: + pass self._battery(state) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index 85f12a18afd..a56b40f3064 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -202,4 +202,11 @@ class TimeWrapper: def __getattr__(self, attr): """Fetch an attribute from Time module.""" - return getattr(time, attr) + attribute = getattr(time, attr) + if callable(attribute): + def wrapper(*args, **kw): + """Wrapper to return callable method if callable.""" + return attribute(*args, **kw) + return wrapper + else: + return attribute diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py new file mode 100644 index 00000000000..882731d4f2c --- /dev/null +++ b/homeassistant/components/rainbird.py @@ -0,0 +1,47 @@ +""" +Support for Rain Bird Irrigation system LNK WiFi Module. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainbird/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_HOST, CONF_PASSWORD) + +REQUIREMENTS = ['pyrainbird==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINBIRD = 'rainbird' +DOMAIN = 'rainbird' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Rain Bird componenent.""" + conf = config[DOMAIN] + server = conf.get(CONF_HOST) + password = conf.get(CONF_PASSWORD) + + from pyrainbird import RainbirdController + controller = RainbirdController() + controller.setConfig(server, password) + + _LOGGER.debug("Rain Bird Controller set to: %s", server) + + initialstatus = controller.currentIrrigation() + if initialstatus == -1: + _LOGGER.error("Error getting state. Possible configuration issues") + return False + + hass.data[DATA_RAINBIRD] = controller + return True diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f8ae9e9d0be..51da2d470ea 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -10,7 +10,6 @@ https://home-assistant.io/components/recorder/ import asyncio import concurrent.futures import logging -from os import path import queue import threading import time @@ -30,13 +29,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -from homeassistant import config as conf_util from . import purge, migration from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.1.15'] +REQUIREMENTS = ['sqlalchemy==1.2.0'] _LOGGER = logging.getLogger(__name__) @@ -142,13 +140,8 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle calls to the purge service.""" instance.do_adhoc_purge(service.data[ATTR_KEEP_DAYS]) - descriptions = yield from hass.async_add_job( - conf_util.load_yaml_config_file, path.join( - path.dirname(__file__), 'services.yaml')) - hass.services.async_register(DOMAIN, SERVICE_PURGE, async_handle_purge_service, - descriptions.get(SERVICE_PURGE), schema=SERVICE_PURGE_SCHEMA) return (yield from instance.async_db_ready) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 719f65abb47..328bbe68dcb 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -12,17 +12,44 @@ _LOGGER = logging.getLogger(__name__) def purge_old_data(instance, purge_days): """Purge events and states older than purge_days ago.""" from .models import States, Events + from sqlalchemy import func + purge_before = dt_util.utcnow() - timedelta(days=purge_days) with session_scope(session=instance.get_session()) as session: + # For each entity, the most recent state is protected from deletion + # s.t. we can properly restore state even if the entity has not been + # updated in a long time + protected_states = session.query(States.state_id, States.event_id, + func.max(States.last_updated)) \ + .group_by(States.entity_id).subquery() + + protected_state_ids = session.query(States.state_id).join( + protected_states, States.state_id == protected_states.c.state_id)\ + .subquery() + deleted_rows = session.query(States) \ .filter((States.last_updated < purge_before)) \ + .filter(~States.state_id.in_( + protected_state_ids)) \ .delete(synchronize_session=False) _LOGGER.debug("Deleted %s states", deleted_rows) + # We also need to protect the events belonging to the protected states. + # Otherwise, if the SQL server has "ON DELETE CASCADE" as default, it + # will delete the protected state when deleting its associated + # event. Also, we would be producing NULLed foreign keys otherwise. + + protected_event_ids = session.query(States.event_id).join( + protected_states, States.state_id == protected_states.c.state_id)\ + .filter(~States.event_id is not None).subquery() + deleted_rows = session.query(Events) \ - .filter((Events.time_fired < purge_before)) \ - .delete(synchronize_session=False) + .filter((Events.time_fired < purge_before)) \ + .filter(~Events.event_id.in_( + protected_event_ids + )) \ + .delete(synchronize_session=False) _LOGGER.debug("Deleted %s events", deleted_rows) # Execute sqlite vacuum command to free up space on disk diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 4a788297c60..08c371fcf0a 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -14,8 +14,8 @@ import os import json import voluptuous as vol -from homeassistant.config import load_yaml_config_file -from homeassistant.const import (CONF_API_KEY, STATE_OK, CONF_TOKEN, CONF_NAME) +from homeassistant.const import (CONF_API_KEY, STATE_OK, CONF_TOKEN, + CONF_NAME, CONF_ID) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -31,6 +31,10 @@ DEFAULT_NAME = DOMAIN GROUP_NAME_RTM = 'remember the milk accounts' CONF_SHARED_SECRET = 'shared_secret' +CONF_ID_MAP = 'id_map' +CONF_LIST_ID = 'list_id' +CONF_TIMESERIES_ID = 'timeseries_id' +CONF_TASK_ID = 'task_id' RTM_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, @@ -44,9 +48,15 @@ CONFIG_SCHEMA = vol.Schema({ CONFIG_FILE_NAME = '.remember_the_milk.conf' SERVICE_CREATE_TASK = 'create_task' +SERVICE_COMPLETE_TASK = 'complete_task' SERVICE_SCHEMA_CREATE_TASK = vol.Schema({ vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ID): cv.string, +}) + +SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({ + vol.Required(CONF_ID): cv.string, }) @@ -55,9 +65,6 @@ def setup(hass, config): component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_RTM) - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - stored_rtm_config = RememberTheMilkConfiguration(hass) for rtm_config in config[DOMAIN]: account_name = rtm_config[CONF_NAME] @@ -69,29 +76,31 @@ def setup(hass, config): _LOGGER.debug("found token for account %s", account_name) _create_instance( hass, account_name, api_key, shared_secret, token, - stored_rtm_config, component, descriptions) + stored_rtm_config, component) else: _register_new_account( hass, account_name, api_key, shared_secret, - stored_rtm_config, component, descriptions) + stored_rtm_config, component) _LOGGER.debug("Finished adding all Remember the milk accounts") return True def _create_instance(hass, account_name, api_key, shared_secret, - token, stored_rtm_config, component, descriptions): + token, stored_rtm_config, component): entity = RememberTheMilk(account_name, api_key, shared_secret, token, stored_rtm_config) component.add_entity(entity) - hass.services.async_register( + hass.services.register( DOMAIN, '{}_create_task'.format(account_name), entity.create_task, - description=descriptions.get(SERVICE_CREATE_TASK), schema=SERVICE_SCHEMA_CREATE_TASK) + hass.services.register( + DOMAIN, '{}_complete_task'.format(account_name), entity.complete_task, + schema=SERVICE_SCHEMA_COMPLETE_TASK) def _register_new_account(hass, account_name, api_key, shared_secret, - stored_rtm_config, component, descriptions): + stored_rtm_config, component): from rtmapi import Rtm request_id = None @@ -116,7 +125,7 @@ def _register_new_account(hass, account_name, api_key, shared_secret, _create_instance( hass, account_name, api_key, shared_secret, token, - stored_rtm_config, component, descriptions) + stored_rtm_config, component) configurator.request_done(request_id) @@ -168,8 +177,7 @@ class RememberTheMilkConfiguration(object): def set_token(self, profile_name, token): """Store a new server token for a profile.""" - if profile_name not in self._config: - self._config[profile_name] = dict() + self._initialize_profile(profile_name) self._config[profile_name][CONF_TOKEN] = token self.save_config() @@ -181,6 +189,44 @@ class RememberTheMilkConfiguration(object): self._config.pop(profile_name, None) self.save_config() + def _initialize_profile(self, profile_name): + """Initialize the data structures for a profile.""" + if profile_name not in self._config: + self._config[profile_name] = dict() + if CONF_ID_MAP not in self._config[profile_name]: + self._config[profile_name][CONF_ID_MAP] = dict() + + def get_rtm_id(self, profile_name, hass_id): + """Get the rtm ids for a home assistant task id. + + The id of a rtm tasks consists of the tuple: + list id, timeseries id and the task id. + """ + self._initialize_profile(profile_name) + ids = self._config[profile_name][CONF_ID_MAP].get(hass_id) + if ids is None: + return None + return ids[CONF_LIST_ID], ids[CONF_TIMESERIES_ID], ids[CONF_TASK_ID] + + def set_rtm_id(self, profile_name, hass_id, list_id, time_series_id, + rtm_task_id): + """Add/Update the rtm task id for a home assistant task id.""" + self._initialize_profile(profile_name) + id_tuple = { + CONF_LIST_ID: list_id, + CONF_TIMESERIES_ID: time_series_id, + CONF_TASK_ID: rtm_task_id, + } + self._config[profile_name][CONF_ID_MAP][hass_id] = id_tuple + self.save_config() + + def delete_rtm_id(self, profile_name, hass_id): + """Delete a key mapping.""" + self._initialize_profile(profile_name) + if hass_id in self._config[profile_name][CONF_ID_MAP]: + del self._config[profile_name][CONF_ID_MAP][hass_id] + self.save_config() + class RememberTheMilk(Entity): """MVP implementation of an interface to Remember The Milk.""" @@ -225,19 +271,65 @@ class RememberTheMilk(Entity): import rtmapi try: - task_name = call.data.get('name') + task_name = call.data.get(CONF_NAME) + hass_id = call.data.get(CONF_ID) + rtm_id = None + if hass_id is not None: + rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) result = self._rtm_api.rtm.timelines.create() timeline = result.timeline.value - self._rtm_api.rtm.tasks.add( - timeline=timeline, name=task_name, parse='1') - _LOGGER.debug('created new task "%s" in account %s', - task_name, self.name) + + if hass_id is None or rtm_id is None: + result = self._rtm_api.rtm.tasks.add( + timeline=timeline, name=task_name, parse='1') + _LOGGER.debug('created new task "%s" in account %s', + task_name, self.name) + self._rtm_config.set_rtm_id(self._name, + hass_id, + result.list.id, + result.list.taskseries.id, + result.list.taskseries.task.id) + else: + self._rtm_api.rtm.tasks.setName(name=task_name, + list_id=rtm_id[0], + taskseries_id=rtm_id[1], + task_id=rtm_id[2], + timeline=timeline) + _LOGGER.debug('updated task with id "%s" in account ' + '%s to name %s', + hass_id, self.name, task_name) except rtmapi.RtmRequestFailedException as rtm_exception: _LOGGER.error('Error creating new Remember The Milk task for ' 'account %s: %s', self._name, rtm_exception) return False return True + def complete_task(self, call): + """Complete a task that was previously created by this component.""" + import rtmapi + + hass_id = call.data.get(CONF_ID) + rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) + if rtm_id is None: + _LOGGER.error('Could not find task with id %s in account %s. ' + 'So task could not be closed.', + hass_id, self._name) + return False + try: + result = self._rtm_api.rtm.timelines.create() + timeline = result.timeline.value + self._rtm_api.rtm.tasks.complete(list_id=rtm_id[0], + taskseries_id=rtm_id[1], + task_id=rtm_id[2], + timeline=timeline) + self._rtm_config.delete_rtm_id(self._name, hass_id) + _LOGGER.debug('Completed task with id %s in account %s', + hass_id, self._name) + except rtmapi.RtmRequestFailedException as rtm_exception: + _LOGGER.error('Error creating new Remember The Milk task for ' + 'account %s: %s', self._name, rtm_exception) + return True + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/remember_the_milk/services.yaml b/homeassistant/components/remember_the_milk/services.yaml index ebf242013f1..74a2c3a4d4f 100644 --- a/homeassistant/components/remember_the_milk/services.yaml +++ b/homeassistant/components/remember_the_milk/services.yaml @@ -1,9 +1,24 @@ # Describes the format for available Remember The Milk services create_task: - description: Create a new task in your Remember The Milk account + description: > + Create (or update) a new task in your Remember The Milk account. If you want to update a task + later on, you have to set an "id" when creating the task. + Note: Updating a tasks does not support the smart syntax. fields: name: description: name of the new task, you can use the smart syntax here - example: 'do this ^today #from_hass' \ No newline at end of file + example: 'do this ^today #from_hass' + + id: + description: (optional) identifier for the task you're creating, can be used to update or complete the task later on + example: myid + +complete_task: + description: Complete a tasks that was privously created. + + fields: + id: + description: identifier that was defined when creating the task + example: myid \ No newline at end of file diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py old mode 100755 new mode 100644 index 3f1086c46c7..ddae36b92a7 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -8,11 +8,9 @@ import asyncio from datetime import timedelta import functools as ft import logging -import os import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -160,24 +158,17 @@ def async_setup(hass, config): if update_tasks: 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')) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handle_remote_service, - descriptions.get(SERVICE_TURN_OFF), schema=REMOTE_SERVICE_ACTIVITY_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TURN_ON, async_handle_remote_service, - descriptions.get(SERVICE_TURN_ON), schema=REMOTE_SERVICE_ACTIVITY_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TOGGLE, async_handle_remote_service, - descriptions.get(SERVICE_TOGGLE), schema=REMOTE_SERVICE_ACTIVITY_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_SEND_COMMAND, async_handle_remote_service, - descriptions.get(SERVICE_SEND_COMMAND), schema=REMOTE_SERVICE_SEND_COMMAND_SCHEMA) return True diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py old mode 100755 new mode 100644 index 40536a83602..4d241ed5913 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/remote.harmony/ """ import logging import asyncio -from os import path import time import voluptuous as vol @@ -19,7 +18,6 @@ from homeassistant.components.remote import ( PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_ACTIVITY, ATTR_NUM_REPEATS, ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) from homeassistant.util import slugify -from homeassistant.config import load_yaml_config_file REQUIREMENTS = ['pyharmony==1.0.18'] @@ -105,11 +103,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def register_services(hass): """Register all services for harmony devices.""" - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - hass.services.register( - DOMAIN, SERVICE_SYNC, _sync_service, descriptions.get(SERVICE_SYNC), + DOMAIN, SERVICE_SYNC, _sync_service, schema=HARMONY_SYNC_SCHEMA) diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 5045017790e..73922d56040 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -8,11 +8,9 @@ import asyncio from collections import defaultdict import functools as ft import logging -import os import async_timeout -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) @@ -132,14 +130,9 @@ def async_setup(hass, config): call.data.get(CONF_COMMAND))): _LOGGER.error('Failed Rflink command for %s', str(call.data)) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml') - ) - hass.services.async_register( DOMAIN, SERVICE_SEND_COMMAND, async_send_command, - descriptions[DOMAIN][SERVICE_SEND_COMMAND], SEND_COMMAND_SCHEMA) + schema=SEND_COMMAND_SCHEMA) @callback def event_callback(event): diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 8b730bf97f2..f28a9aafb19 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -16,11 +16,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ATTR_ENTITY_ID, TEMP_CELSIUS, - CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF + CONF_DEVICES ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.20.1'] +REQUIREMENTS = ['pyRFXtrx==0.21.1'] DOMAIN = 'rfxtrx' @@ -31,13 +31,19 @@ ATTR_DEVICE = 'device' ATTR_DEBUG = 'debug' ATTR_STATE = 'state' ATTR_NAME = 'name' -ATTR_FIREEVENT = 'fire_event' +ATTR_FIRE_EVENT = 'fire_event' ATTR_DATA_TYPE = 'data_type' ATTR_DATA_BITS = 'data_bits' ATTR_DUMMY = 'dummy' ATTR_OFF_DELAY = 'off_delay' +CONF_AUTOMATIC_ADD = 'automatic_add' +CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' -CONF_DEVICES = 'devices' +CONF_FIRE_EVENT = 'fire_event' +CONF_DATA_BITS = 'data_bits' +CONF_DUMMY = 'dummy' +CONF_DEVICE = 'device' +CONF_DEBUG = 'debug' EVENT_BUTTON_PRESSED = 'button_pressed' DATA_TYPES = OrderedDict([ @@ -57,93 +63,13 @@ DATA_TYPES = OrderedDict([ RECEIVED_EVT_SUBSCRIBERS = [] RFX_DEVICES = {} _LOGGER = logging.getLogger(__name__) -RFXOBJECT = 'rfxobject' - - -def _valid_device(value, device_type): - """Validate a dictionary of devices definitions.""" - config = OrderedDict() - for key, device in value.items(): - - # Still accept old configuration - if 'packetid' in device.keys(): - msg = 'You are using an outdated configuration of the rfxtrx ' +\ - 'device, {}.'.format(key) +\ - ' Your new config should be:\n {}: \n name: {}'\ - .format(device.get('packetid'), - device.get(ATTR_NAME, 'deivce_name')) - _LOGGER.warning(msg) - key = device.get('packetid') - device.pop('packetid') - - key = str(key) - if not len(key) % 2 == 0: - key = '0' + key - - if device_type == 'sensor': - config[key] = DEVICE_SCHEMA_SENSOR(device) - elif device_type == 'binary_sensor': - config[key] = DEVICE_SCHEMA_BINARYSENSOR(device) - elif device_type == 'light_switch': - config[key] = DEVICE_SCHEMA(device) - else: - raise vol.Invalid('Rfxtrx device is invalid') - - if not config[key][ATTR_NAME]: - config[key][ATTR_NAME] = key - return config - - -def valid_sensor(value): - """Validate sensor configuration.""" - return _valid_device(value, "sensor") - - -def valid_binary_sensor(value): - """Validate binary sensor configuration.""" - return _valid_device(value, "binary_sensor") - - -def _valid_light_switch(value): - return _valid_device(value, "light_switch") - - -DEVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, - vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, -}) - -DEVICE_SCHEMA_SENSOR = vol.Schema({ - vol.Optional(ATTR_NAME, default=None): cv.string, - vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, - vol.Optional(ATTR_DATA_TYPE, default=[]): - vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), -}) - -DEVICE_SCHEMA_BINARYSENSOR = vol.Schema({ - vol.Optional(ATTR_NAME, default=None): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=None): cv.string, - vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, - vol.Optional(ATTR_OFF_DELAY, default=None): - vol.Any(cv.time_period, cv.positive_timedelta), - vol.Optional(ATTR_DATA_BITS, default=None): cv.positive_int, - vol.Optional(CONF_COMMAND_ON, default=None): cv.byte, - vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte -}) - -DEFAULT_SCHEMA = vol.Schema({ - vol.Required("platform"): DOMAIN, - vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch), - vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): - vol.Coerce(int), -}) +DATA_RFXOBJECT = 'rfxobject' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_DEBUG, default=False): cv.boolean, - vol.Optional(ATTR_DUMMY, default=False): cv.boolean, + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DUMMY, default=False): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -152,7 +78,7 @@ def setup(hass, config): """Set up the RFXtrx component.""" # Declare the Handle event def handle_receive(event): - """Handle revieved messgaes from RFXtrx gateway.""" + """Handle revieved messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: return @@ -175,21 +101,22 @@ def setup(hass, config): dummy_connection = config[DOMAIN][ATTR_DUMMY] if dummy_connection: - hass.data[RFXOBJECT] =\ - rfxtrxmod.Connect(device, None, debug=debug, - transport_protocol=rfxtrxmod.DummyTransport2) + rfx_object = rfxtrxmod.Connect( + device, None, debug=debug, + transport_protocol=rfxtrxmod.DummyTransport2) else: - hass.data[RFXOBJECT] = rfxtrxmod.Connect(device, None, debug=debug) + rfx_object = rfxtrxmod.Connect(device, None, debug=debug) def _start_rfxtrx(event): - hass.data[RFXOBJECT].event_callback = handle_receive + rfx_object.event_callback = handle_receive hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx) def _shutdown_rfxtrx(event): """Close connection with RFXtrx.""" - hass.data[RFXOBJECT].close_connection() + rfx_object.close_connection() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) + hass.data[DATA_RFXOBJECT] = rfx_object return True @@ -248,9 +175,9 @@ def get_pt2262_device(device_id): if (hasattr(device, 'is_lighting4') and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits)): - _LOGGER.info("rfxtrx: found matching device %s for %s", - device_id, - device.masked_id) + _LOGGER.debug("rfxtrx: found matching device %s for %s", + device_id, + device.masked_id) return device return None @@ -295,11 +222,11 @@ def get_devices_from_config(config, device): device_id = slugify(event.device.id_string.lower()) if device_id in RFX_DEVICES: continue - _LOGGER.info("Add %s rfxtrx", entity_info[ATTR_NAME]) + _LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME]) # Check if i must fire event - fire_event = entity_info[ATTR_FIREEVENT] - datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event} + fire_event = entity_info[ATTR_FIRE_EVENT] + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event} new_device = device(entity_info[ATTR_NAME], event, datas, signal_repetitions) @@ -318,14 +245,14 @@ def get_new_device(event, config, device): return pkt_id = "".join("{0:02x}".format(x) for x in event.data) - _LOGGER.info( + _LOGGER.debug( "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", device_id, event.device.__class__.__name__, event.device.subtype, pkt_id ) - datas = {ATTR_STATE: False, ATTR_FIREEVENT: False} + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} signal_repetitions = config[CONF_SIGNAL_REPETITIONS] new_device = device(pkt_id, event, datas, signal_repetitions) @@ -370,7 +297,7 @@ def apply_received_command(event): ATTR_STATE: event.values['Command'].lower() } ) - _LOGGER.info( + _LOGGER.debug( "Rfxtrx fired event: (event_type: %s, %s: %s, %s: %s)", EVENT_BUTTON_PRESSED, ATTR_ENTITY_ID, @@ -392,7 +319,7 @@ class RfxtrxDevice(Entity): self._name = name self._event = event self._state = datas[ATTR_STATE] - self._should_fire_event = datas[ATTR_FIREEVENT] + self._should_fire_event = datas[ATTR_FIRE_EVENT] self._brightness = 0 self.added_to_hass = False @@ -440,40 +367,35 @@ class RfxtrxDevice(Entity): def _send_command(self, command, brightness=0): if not self._event: return + rfx_object = self.hass.data[DATA_RFXOBJECT] if command == "turn_on": for _ in range(self.signal_repetitions): - self._event.device.send_on(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_on(rfx_object.transport) self._state = True elif command == "dim": for _ in range(self.signal_repetitions): - self._event.device.send_dim(self.hass.data[RFXOBJECT] - .transport, brightness) + self._event.device.send_dim(rfx_object.transport, brightness) self._state = True elif command == 'turn_off': for _ in range(self.signal_repetitions): - self._event.device.send_off(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_off(rfx_object.transport) self._state = False self._brightness = 0 elif command == "roll_up": for _ in range(self.signal_repetitions): - self._event.device.send_open(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_open(rfx_object.transport) elif command == "roll_down": for _ in range(self.signal_repetitions): - self._event.device.send_close(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_close(rfx_object.transport) elif command == "stop_roll": for _ in range(self.signal_repetitions): - self._event.device.send_stop(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_stop(rfx_object.transport) if self.added_to_hass: self.schedule_update_ha_state() diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py new file mode 100644 index 00000000000..f035ae3128e --- /dev/null +++ b/homeassistant/components/scene/deconz.py @@ -0,0 +1,45 @@ +""" +Support for deCONZ scenes. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/scene.deconz/ +""" + +import asyncio + +from homeassistant.components.deconz import DOMAIN as DECONZ_DATA +from homeassistant.components.scene import Scene + +DEPENDENCIES = ['deconz'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up scenes for deCONZ component.""" + if discovery_info is None: + return + + scenes = hass.data[DECONZ_DATA].scenes + entities = [] + + for scene in scenes.values(): + entities.append(DeconzScene(scene)) + async_add_devices(entities) + + +class DeconzScene(Scene): + """Representation of a deCONZ scene.""" + + def __init__(self, scene): + """Setup scene.""" + self._scene = scene + + @asyncio.coroutine + def async_activate(self, **kwargs): + """Activate the scene.""" + yield from self._scene.async_set_state({}) + + @property + def name(self): + """Return the name of the scene.""" + return self._scene.full_name diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml new file mode 100644 index 00000000000..ee255affe44 --- /dev/null +++ b/homeassistant/components/scene/services.yaml @@ -0,0 +1,8 @@ +# Describes the format for available scene services + +turn_on: + description: Activate a scene. + fields: + entity_id: + description: Name(s) of scenes to turn on + example: 'scene.romantic' diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 88ead3301b6..7987de7caf3 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -10,11 +10,12 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['alpha_vantage==1.3.6'] +REQUIREMENTS = ['alpha_vantage==1.8.0'] _LOGGER = logging.getLogger(__name__) @@ -23,25 +24,62 @@ ATTR_HIGH = 'high' ATTR_LOW = 'low' ATTR_VOLUME = 'volume' -CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage." +CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage" +CONF_FOREIGN_EXCHANGE = 'foreign_exchange' +CONF_FROM = 'from' +CONF_SYMBOL = 'symbol' CONF_SYMBOLS = 'symbols' +CONF_TO = 'to' -DEFAULT_SYMBOL = 'GOOGL' +DEFAULT_SYMBOL = { + CONF_CURRENCY: 'USD', + CONF_NAME: 'Google', + CONF_SYMBOL: 'GOOGL', +} -ICON = 'mdi:currency-usd' +DEFAULT_CURRENCY = { + CONF_FROM: 'BTC', + CONF_NAME: 'Bitcon', + CONF_TO: 'USD', +} + +ICONS = { + 'BTC': 'mdi:currency-btc', + 'EUR': 'mdi:currency-eur', + 'GBP': 'mdi:currency-gbp', + 'INR': 'mdi:currency-inr', + 'RUB': 'mdi:currency-rub', + 'TRY': 'mdi: currency-try', + 'USD': 'mdi:currency-usd', +} SCAN_INTERVAL = timedelta(minutes=5) +SYMBOL_SCHEMA = vol.Schema({ + vol.Required(CONF_SYMBOL): cv.string, + vol.Optional(CONF_CURRENCY): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + +CURRENCY_SCHEMA = vol.Schema({ + vol.Required(CONF_FROM): cv.string, + vol.Required(CONF_TO): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_FOREIGN_EXCHANGE, default=[DEFAULT_CURRENCY]): + vol.All(cv.ensure_list, [CURRENCY_SCHEMA]), vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]): - vol.All(cv.ensure_list, [cv.string]), + vol.All(cv.ensure_list, [SYMBOL_SCHEMA]), }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Alpha Vantage sensor.""" from alpha_vantage.timeseries import TimeSeries + from alpha_vantage.foreignexchange import ForeignExchange api_key = config.get(CONF_API_KEY) symbols = config.get(CONF_SYMBOLS) @@ -51,13 +89,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] for symbol in symbols: try: - timeseries.get_intraday(symbol) + timeseries.get_intraday(symbol[CONF_SYMBOL]) except ValueError: _LOGGER.error( "API Key is not valid or symbol '%s' not known", symbol) - return dev.append(AlphaVantageSensor(timeseries, symbol)) + forex = ForeignExchange(key=api_key) + for conversion in config.get(CONF_FOREIGN_EXCHANGE): + from_cur = conversion.get(CONF_FROM) + to_cur = conversion.get(CONF_TO) + try: + forex.get_currency_exchange_rate( + from_currency=from_cur, to_currency=to_cur) + except ValueError as error: + _LOGGER.error( + "API Key is not valid or currencies '%s'/'%s' not known", + from_cur, to_cur) + _LOGGER.debug(str(error)) + dev.append(AlphaVantageForeignExchange(forex, conversion)) + add_devices(dev, True) @@ -66,11 +117,12 @@ class AlphaVantageSensor(Entity): def __init__(self, timeseries, symbol): """Initialize the sensor.""" - self._name = symbol + self._symbol = symbol[CONF_SYMBOL] + self._name = symbol.get(CONF_NAME, self._symbol) self._timeseries = timeseries - self._symbol = symbol self.values = None - self._unit_of_measurement = None + self._unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) + self._icon = ICONS.get(symbol.get(CONF_CURRENCY, 'USD')) @property def name(self): @@ -80,7 +132,7 @@ class AlphaVantageSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return self._symbol + return self._unit_of_measurement @property def state(self): @@ -102,9 +154,61 @@ class AlphaVantageSensor(Entity): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return ICON + return self._icon def update(self): """Get the latest data and updates the states.""" all_values, _ = self._timeseries.get_intraday(self._symbol) self.values = next(iter(all_values.values())) + + +class AlphaVantageForeignExchange(Entity): + """Sensor for foreign exchange rates.""" + + def __init__(self, foreign_exchange, config): + """Initialize the sensor.""" + self._foreign_exchange = foreign_exchange + self._from_currency = config.get(CONF_FROM) + self._to_currency = config.get(CONF_TO) + if CONF_NAME in config: + self._name = config.get(CONF_NAME) + else: + self._name = '{}/{}'.format(self._to_currency, self._from_currency) + self._unit_of_measurement = self._to_currency + self._icon = ICONS.get(self._from_currency, 'USD') + self.values = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + return self.values['5. Exchange Rate'] + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.values is not None: + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + CONF_FROM: self._from_currency, + CONF_TO: self._to_currency, + } + + def update(self): + """Get the latest data and updates the states.""" + self.values, _ = self._foreign_exchange.get_currency_exchange_rate( + from_currency=self._from_currency, to_currency=self._to_currency) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 31c6c1809b3..8bed72a67c2 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -38,7 +38,7 @@ OPTION_TYPES = { 'number_of_transactions': ['No. of Transactions', None], 'hash_rate': ['Hash rate', 'PH/s'], 'timestamp': ['Timestamp', None], - 'mined_blocks': ['Minded Blocks', None], + 'mined_blocks': ['Mined Blocks', None], 'blocks_size': ['Block size', None], 'total_fees_btc': ['Total fees', 'BTC'], 'total_btc_sent': ['Total sent', 'BTC'], diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/sensor/coinbase.py b/homeassistant/components/sensor/coinbase.py new file mode 100644 index 00000000000..d66c7d4e4b6 --- /dev/null +++ b/homeassistant/components/sensor/coinbase.py @@ -0,0 +1,141 @@ +""" +Support for Coinbase sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.coinbase/ +""" +from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_ATTRIBUTION + + +DEPENDENCIES = ['coinbase'] + +DATA_COINBASE = 'coinbase_cache' + +CONF_ATTRIBUTION = "Data provided by coinbase.com" +ATTR_NATIVE_BALANCE = "Balance in native currency" + +BTC_ICON = 'mdi:currency-btc' +ETH_ICON = 'mdi:currency-eth' +COIN_ICON = 'mdi:coin' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Coinbase sensors.""" + if discovery_info is None: + return + if 'account' in discovery_info: + account = discovery_info['account'] + sensor = AccountSensor(hass.data[DATA_COINBASE], + account['name'], + account['balance']['currency']) + if 'exchange_currency' in discovery_info: + sensor = ExchangeRateSensor(hass.data[DATA_COINBASE], + discovery_info['exchange_currency'], + discovery_info['native_currency']) + + add_devices([sensor], True) + + +class AccountSensor(Entity): + """Representation of a Coinbase.com sensor.""" + + def __init__(self, coinbase_data, name, currency): + """Initialize the sensor.""" + self._coinbase_data = coinbase_data + self._name = "Coinbase {}".format(name) + self._state = None + self._unit_of_measurement = currency + self._native_balance = None + self._native_currency = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if self._name == "Coinbase BTC Wallet": + return BTC_ICON + if self._name == "Coinbase ETH Wallet": + return ETH_ICON + return COIN_ICON + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_NATIVE_BALANCE: "{} {}".format(self._native_balance, + self._native_currency) + } + + def update(self): + """Get the latest state of the sensor.""" + self._coinbase_data.update() + for account in self._coinbase_data.accounts['data']: + if self._name == "Coinbase {}".format(account['name']): + self._state = account['balance']['amount'] + self._native_balance = account['native_balance']['amount'] + self._native_currency = account['native_balance']['currency'] + + +class ExchangeRateSensor(Entity): + """Representation of a Coinbase.com sensor.""" + + def __init__(self, coinbase_data, exchange_currency, native_currency): + """Initialize the sensor.""" + self._coinbase_data = coinbase_data + self.currency = exchange_currency + self._name = "{} Exchange Rate".format(exchange_currency) + self._state = None + self._unit_of_measurement = native_currency + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if self._name == "BTC Exchange Rate": + return BTC_ICON + if self._name == "ETH Exchange Rate": + return ETH_ICON + return COIN_ICON + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION + } + + def update(self): + """Get the latest state of the sensor.""" + self._coinbase_data.update() + rate = self._coinbase_data.exchange_rates.rates[self.currency] + self._state = round(1 / float(rate), 2) diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py new file mode 100644 index 00000000000..ad571110e88 --- /dev/null +++ b/homeassistant/components/sensor/daikin.py @@ -0,0 +1,124 @@ +""" +Support for Daikin AC Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.daikin/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.daikin import ( + SENSOR_TYPES, SENSOR_TYPE_TEMPERATURE, + ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, + daikin_api_setup +) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_ICON, CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_TYPE +) +from homeassistant.helpers.entity import Entity +from homeassistant.util.unit_system import UnitSystem + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=None): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Daikin sensors.""" + if discovery_info is not None: + host = discovery_info.get('ip') + name = None + monitored_conditions = discovery_info.get( + CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES.keys()) + ) + else: + host = config[CONF_HOST] + name = config.get(CONF_NAME) + monitored_conditions = config[CONF_MONITORED_CONDITIONS] + _LOGGER.info("Added Daikin AC sensor on %s", host) + + api = daikin_api_setup(hass, host, name) + units = hass.config.units + sensors = [] + for monitored_state in monitored_conditions: + sensors.append(DaikinClimateSensor(api, monitored_state, units, name)) + + add_devices(sensors, True) + + +class DaikinClimateSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, api, monitored_state, units: UnitSystem, name=None): + """Initialize the sensor.""" + self._api = api + self._sensor = SENSOR_TYPES.get(monitored_state) + if name is None: + name = "{} {}".format(self._sensor[CONF_NAME], api.name) + + self._name = "{} {}".format(name, monitored_state.replace("_", " ")) + self._device_attribute = monitored_state + + if self._sensor[CONF_TYPE] == SENSOR_TYPE_TEMPERATURE: + self._unit_of_measurement = units.temperature_unit + + def get(self, key): + """Retrieve device settings from API library cache.""" + value = None + cast_to_float = False + + if key == ATTR_INSIDE_TEMPERATURE: + value = self._api.device.values.get('htemp') + cast_to_float = True + elif key == ATTR_OUTSIDE_TEMPERATURE: + value = self._api.device.values.get('otemp') + + 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 + + @property + def unique_id(self): + """Return the ID of this AC.""" + return "{}.{}".format(self.__class__, self._api.ip_address) + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._sensor[CONF_ICON] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self.get(self._device_attribute) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + def update(self): + """Retrieve latest state.""" + self._api.update() diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py new file mode 100644 index 00000000000..c01483169cb --- /dev/null +++ b/homeassistant/components/sensor/deconz.py @@ -0,0 +1,192 @@ +""" +Support for deCONZ sensor. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.deconz/ +""" + +import asyncio + +from homeassistant.components.deconz import DOMAIN as DECONZ_DATA +from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID +from homeassistant.core import callback, EventOrigin +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util import slugify + +DEPENDENCIES = ['deconz'] + +ATTR_EVENT_ID = 'event_id' +ATTR_ZHASWITCH = 'ZHASwitch' + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup sensor for deCONZ component.""" + if discovery_info is None: + return + + from pydeconz.sensor import DECONZ_SENSOR + sensors = hass.data[DECONZ_DATA].sensors + entities = [] + + for sensor in sensors.values(): + if sensor.type in DECONZ_SENSOR: + if sensor.type == ATTR_ZHASWITCH: + DeconzEvent(hass, sensor) + if sensor.battery: + entities.append(DeconzBattery(sensor)) + else: + entities.append(DeconzSensor(sensor)) + async_add_devices(entities, True) + + +class DeconzSensor(Entity): + """Representation of a 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 to 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 state(self): + """Return the state of the sensor.""" + return self._sensor.state + + @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 unit_of_measurement(self): + """Unit of measurement of this sensor.""" + return self._sensor.sensor_unit + + @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.""" + attr = { + ATTR_BATTERY_LEVEL: self._sensor.battery, + } + return attr + + +class DeconzBattery(Entity): + """Battery class for when a device is only represented as an event.""" + + def __init__(self, device): + """Register dispatcher callback for update of battery state.""" + self._device = device + self._name = self._device.name + ' Battery Level' + self._device_class = 'battery' + self._unit_of_measurement = "%" + + @asyncio.coroutine + def async_added_to_hass(self): + """Subscribe to sensors events.""" + self._device.register_async_callback(self.async_update_callback) + + @callback + def async_update_callback(self, reason): + """Update the battery's state, if needed.""" + if 'battery' in reason['attr']: + self.async_schedule_update_ha_state() + + @property + def state(self): + """Return the state of the battery.""" + return self._device.battery + + @property + def name(self): + """Return the name of the battery.""" + return self._name + + @property + def device_class(self): + """Class of the sensor.""" + return self._device_class + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return icon_for_battery_level(int(self.state)) + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes of the battery.""" + attr = { + ATTR_EVENT_ID: slugify(self._device.name), + } + return attr + + +class DeconzEvent(object): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, hass, device): + """Register callback that will be used for signals.""" + self._hass = hass + self._device = device + self._device.register_async_callback(self.async_update_callback) + self._event = 'deconz_{}'.format(CONF_EVENT) + self._id = slugify(self._device.name) + + @callback + def async_update_callback(self, reason): + """Fire the event if reason is that state is updated.""" + if reason['state']: + data = {CONF_ID: self._id, CONF_EVENT: self._device.state} + self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index e07730b53e8..c13fc930ed1 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['schiene==0.19'] +REQUIREMENTS = ['schiene==0.20'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py new file mode 100644 index 00000000000..2920dc025d7 --- /dev/null +++ b/homeassistant/components/sensor/discogs.py @@ -0,0 +1,97 @@ +""" +Show the amount of records in a user's Discogs collection. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.discogs/ +""" +import asyncio +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TOKEN +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['discogs_client==2.2.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_IDENTITY = 'identity' + +CONF_ATTRIBUTION = "Data provided by Discogs" + +DEFAULT_NAME = 'Discogs' + +ICON = 'mdi:album' + +SCAN_INTERVAL = timedelta(hours=2) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Discogs sensor.""" + import discogs_client + + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + try: + discogs = discogs_client.Client(SERVER_SOFTWARE, user_token=token) + identity = discogs.identity() + except discogs_client.exceptions.HTTPError: + _LOGGER.error("API token is not valid") + return + + async_add_devices([DiscogsSensor(identity, name)], True) + + +class DiscogsSensor(Entity): + """Get a user's number of records in collection.""" + + def __init__(self, identity, name): + """Initialize the Discogs sensor.""" + self._identity = identity + self._name = name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'records' + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_IDENTITY: self._identity.name, + } + + @asyncio.coroutine + def async_update(self): + """Set state to the amount of records in user's collection.""" + self._state = self._identity.num_collection diff --git a/homeassistant/components/sensor/etherscan.py b/homeassistant/components/sensor/etherscan.py index 5c9a8839dc9..36513805882 100644 --- a/homeassistant/components/sensor/etherscan.py +++ b/homeassistant/components/sensor/etherscan.py @@ -8,12 +8,12 @@ from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-etherscan-api==0.0.1'] +REQUIREMENTS = ['python-etherscan-api==0.0.2'] CONF_ADDRESS = 'address' CONF_ATTRIBUTION = "Data provided by etherscan.io" diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index c4f4217616f..07c085cd18d 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -7,10 +7,10 @@ https://www.fido.ca/pages/#/my-account/wireless For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fido/ """ +import asyncio import logging from datetime import timedelta -import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyfido==1.0.1'] +REQUIREMENTS = ['pyfido==2.1.0'] _LOGGER = logging.getLogger(__name__) @@ -70,17 +70,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Fido sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - try: - fido_data = FidoData(username, password) - fido_data.update() - except requests.exceptions.HTTPError as error: - _LOGGER.error("Failt login: %s", error) - return False + httpsession = hass.helpers.aiohttp_client.async_get_clientsession() + fido_data = FidoData(username, password, httpsession) + ret = yield from fido_data.async_update() + if ret is False: + return name = config.get(CONF_NAME) @@ -89,7 +89,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for variable in config[CONF_MONITORED_VARIABLES]: sensors.append(FidoSensor(fido_data, variable, name, number)) - add_devices(sensors, True) + async_add_devices(sensors, True) class FidoSensor(Entity): @@ -133,9 +133,10 @@ class FidoSensor(Entity): 'number': self._number, } - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data from Fido and update the state.""" - self.fido_data.update() + yield from self.fido_data.async_update() if self.type == 'balance': if self.fido_data.data.get(self.type) is not None: self._state = round(self.fido_data.data[self.type], 2) @@ -149,20 +150,23 @@ class FidoSensor(Entity): class FidoData(object): """Get data from Fido.""" - def __init__(self, username, password): + def __init__(self, username, password, httpsession): """Initialize the data object.""" from pyfido import FidoClient - self.client = FidoClient(username, password, REQUESTS_TIMEOUT) + self.client = FidoClient(username, password, + REQUESTS_TIMEOUT, httpsession) self.data = {} + @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def async_update(self): """Get the latest data from Fido.""" from pyfido.client import PyFidoError try: - self.client.fetch_data() - except PyFidoError as err: - _LOGGER.error("Error on receive last Fido data: %s", err) - return + yield from self.client.fetch_data() + except PyFidoError as exp: + _LOGGER.error("Error on receive last Fido data: %s", exp) + return False # Update data self.data = self.client.get_data() + return True diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index d857ce57fce..e10abc14ff1 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -7,10 +7,10 @@ https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.hydroquebec/ """ +import asyncio import logging from datetime import timedelta -import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyhydroquebec==1.3.1'] +REQUIREMENTS = ['pyhydroquebec==2.1.0'] _LOGGER = logging.getLogger(__name__) @@ -93,7 +93,8 @@ DAILY_MAP = (('yesterday_total_consumption', 'consoTotalQuot'), ('yesterday_higher_price_consumption', 'consoHautQuot')) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HydroQuebec sensor.""" # Create a data fetcher to support all of the configured sensors. Then make # the first call to init the data. @@ -102,13 +103,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) contract = config.get(CONF_CONTRACT) - try: - hydroquebec_data = HydroquebecData(username, password, contract) - _LOGGER.info("Contract list: %s", - ", ".join(hydroquebec_data.get_contract_list())) - except requests.exceptions.HTTPError as error: - _LOGGER.error("Failt login: %s", error) - return False + httpsession = hass.helpers.aiohttp_client.async_get_clientsession() + hydroquebec_data = HydroquebecData(username, password, httpsession, + contract) + contracts = yield from hydroquebec_data.get_contract_list() + if not contracts: + return + _LOGGER.info("Contract list: %s", + ", ".join(contracts)) name = config.get(CONF_NAME) @@ -116,7 +118,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for variable in config[CONF_MONITORED_VARIABLES]: sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name)) - add_devices(sensors) + async_add_devices(sensors, True) class HydroQuebecSensor(Entity): @@ -152,41 +154,48 @@ class HydroQuebecSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data from Hydroquebec and update the state.""" - self.hydroquebec_data.update() - if self.type in self.hydroquebec_data.data: + yield from self.hydroquebec_data.async_update() + if self.hydroquebec_data.data.get(self.type) is not None: self._state = round(self.hydroquebec_data.data[self.type], 2) class HydroquebecData(object): """Get data from HydroQuebec.""" - def __init__(self, username, password, contract=None): + def __init__(self, username, password, httpsession, contract=None): """Initialize the data object.""" from pyhydroquebec import HydroQuebecClient self.client = HydroQuebecClient( - username, password, REQUESTS_TIMEOUT) + username, password, REQUESTS_TIMEOUT, httpsession) self._contract = contract self.data = {} + @asyncio.coroutine def get_contract_list(self): """Return the contract list.""" # Fetch data - self._fetch_data() - return self.client.get_contracts() + ret = yield from self._fetch_data() + if ret: + return self.client.get_contracts() + return [] + @asyncio.coroutine + @Throttle(MIN_TIME_BETWEEN_UPDATES) def _fetch_data(self): """Fetch latest data from HydroQuebec.""" from pyhydroquebec.client import PyHydroQuebecError try: - self.client.fetch_data() + yield from self.client.fetch_data() except PyHydroQuebecError as exp: _LOGGER.error("Error on receive last Hydroquebec data: %s", exp) - return + return False + return True - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + @asyncio.coroutine + def async_update(self): """Return the latest collected data from HydroQuebec.""" - self._fetch_data() + yield from self._fetch_data() self.data = self.client.get_data(self._contract)[self._contract] diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index f4d4db201e5..1f04cd606d6 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -219,17 +219,19 @@ class EmailContentSensor(Entity): return if self.sender_allowed(email_message): - message_body = EmailContentSensor.get_msg_text(email_message) + message = EmailContentSensor.get_msg_subject(email_message) if self._value_template is not None: - message_body = self.render_template(email_message) + message = self.render_template(email_message) - self._message = message_body + self._message = message self._state_attributes = { ATTR_FROM: EmailContentSensor.get_msg_sender(email_message), ATTR_SUBJECT: EmailContentSensor.get_msg_subject(email_message), ATTR_DATE: - email_message['Date'] + email_message['Date'], + ATTR_BODY: + EmailContentSensor.get_msg_text(email_message) } diff --git a/homeassistant/components/sensor/irish_rail_transport.py b/homeassistant/components/sensor/irish_rail_transport.py index ad2a312ce63..0c34a5f6ce8 100644 --- a/homeassistant/components/sensor/irish_rail_transport.py +++ b/homeassistant/components/sensor/irish_rail_transport.py @@ -148,7 +148,8 @@ class IrishRailTransportData(object): """Get the latest data from irishrail.""" trains = self._ir_api.get_station_by_name(self.station, direction=self.direction, - destination=self.destination) + destination=self.destination, + stops_at=self.stops_at) stops_at = self.stops_at if self.stops_at else '' self.info = [] for train in trains: diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index e961c63a1b5..76f026bba10 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -7,9 +7,11 @@ https://home-assistant.io/components/sensor.isy994/ import logging from typing import Callable # noqa -import homeassistant.components.isy994 as isy +from homeassistant.components.sensor import DOMAIN +from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, + ISYDevice) from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_OFF, STATE_ON, UNIT_UV_INDEX) + TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -232,37 +234,29 @@ UOM_TO_STATES = { } } -BINARY_UOM = ['2', '78'] - # pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 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 = [] - for node in isy.SENSOR_NODES: - if (not node.uom or node.uom[0] not in BINARY_UOM) and \ - STATE_OFF not in node.uom and STATE_ON not in node.uom: - _LOGGER.debug("Loading %s", node.name) - devices.append(ISYSensorDevice(node)) + for node in hass.data[ISY994_NODES][DOMAIN]: + _LOGGER.debug("Loading %s", node.name) + devices.append(ISYSensorDevice(node)) - for node in isy.WEATHER_NODES: + for node in hass.data[ISY994_WEATHER]: devices.append(ISYWeatherDevice(node)) add_devices(devices) -class ISYSensorDevice(isy.ISYDevice): +class ISYSensorDevice(ISYDevice): """Representation of an ISY994 sensor device.""" def __init__(self, node) -> None: """Initialize the ISY994 sensor device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def raw_unit_of_measurement(self) -> str: @@ -316,14 +310,12 @@ class ISYSensorDevice(isy.ISYDevice): return raw_units -class ISYWeatherDevice(isy.ISYDevice): +class ISYWeatherDevice(ISYDevice): """Representation of an ISY994 weather device.""" - _domain = 'sensor' - def __init__(self, node) -> None: """Initialize the ISY994 weather device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def unique_id(self) -> str: diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 7abc986bdd7..f803f406e1e 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -90,6 +90,11 @@ class KNXSensor(Entity): """Return the name of the KNX device.""" return self.device.name + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + @property def should_poll(self): """No polling needed within KNX.""" diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index 8c5fcc15ec2..ac977e52fce 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['luftdaten==0.1.1'] +REQUIREMENTS = ['luftdaten==0.1.3'] _LOGGER = logging.getLogger(__name__) @@ -114,17 +114,17 @@ class LuftdatenSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if self.luftdaten.data.meta is None: + try: + attr = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_SENSOR_ID: self._sensor_id, + 'lat': self.luftdaten.data.meta['latitude'], + 'long': self.luftdaten.data.meta['longitude'], + } + return attr + except KeyError: return - attr = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_SENSOR_ID: self._sensor_id, - 'lat': self.luftdaten.data.meta['latitude'], - 'long': self.luftdaten.data.meta['longitude'], - } - return attr - @asyncio.coroutine def async_update(self): """Get the latest data from luftdaten.info and update the state.""" diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index 25516eda5b1..43290d21e11 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -4,23 +4,28 @@ Support for UK Met Office weather service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.metoffice/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, STATE_UNKNOWN, CONF_NAME, - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE) + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['datapoint==0.4.3'] +ATTR_LAST_UPDATE = 'last_update' +ATTR_SENSOR_ID = 'sensor_id' +ATTR_SITE_ID = 'site_id' +ATTR_SITE_NAME = 'site_name' + CONF_ATTRIBUTION = "Data provided by the Met Office" CONDITION_CLASSES = { @@ -40,6 +45,8 @@ CONDITION_CLASSES = { 'exceptional': [], } +DEFAULT_NAME = "Met Office" + VISIBILTY_CLASSES = { 'VP': '<1', 'PO': '1-4', @@ -49,7 +56,7 @@ VISIBILTY_CLASSES = { 'EX': '>40' } -SCAN_INTERVAL = timedelta(minutes=35) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=35) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -68,77 +75,83 @@ SENSOR_TYPES = { } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=None): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coordinates', + 'Latitude and longitude must exist together'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates', + 'Latitude and longitude must exist together'): cv.longitude, }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Metoffice sensor platform.""" + """Set up the Met Office sensor platform.""" import datapoint as dp - datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) + api_key = config.get(CONF_API_KEY) latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + name = config.get(CONF_NAME) + + datapoint = dp.connection(api_key=api_key) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return try: - site = datapoint.get_nearest_site(latitude=latitude, - longitude=longitude) + site = datapoint.get_nearest_site( + latitude=latitude, longitude=longitude) except dp.exceptions.APIException as err: _LOGGER.error("Received error from Met Office Datapoint: %s", err) - return False + return if not site: _LOGGER.error("Unable to get nearest Met Office forecast site") - return False + return - # Get data data = MetOfficeCurrentData(hass, datapoint, site) - try: - data.update() - except (ValueError, dp.exceptions.APIException) as err: - _LOGGER.error("Received error from Met Office Datapoint: %s", err) - return False + data.update() + if data.data is None: + return - # Add - add_devices([MetOfficeCurrentSensor(site, data, variable) - for variable in config[CONF_MONITORED_CONDITIONS]]) - return True + sensors = [] + for variable in config[CONF_MONITORED_CONDITIONS]: + sensors.append(MetOfficeCurrentSensor(site, data, variable, name)) + + add_devices(sensors, True) class MetOfficeCurrentSensor(Entity): """Implementation of a Met Office current sensor.""" - def __init__(self, site, data, condition): + def __init__(self, site, data, condition, name): """Initialize the sensor.""" - self.site = site - self.data = data self._condition = condition + self.data = data + self._name = name + self.site = site @property def name(self): """Return the name of the sensor.""" - return 'Met Office {}'.format(SENSOR_TYPES[self._condition][0]) + return '{} {}'.format(self._name, SENSOR_TYPES[self._condition][0]) @property def state(self): """Return the state of the sensor.""" if (self._condition == 'visibility_distance' and - 'visibility' in self.data.data.__dict__.keys()): + hasattr(self.data.data, 'visibility')): return VISIBILTY_CLASSES.get(self.data.data.visibility.value) - if self._condition in self.data.data.__dict__.keys(): + if hasattr(self.data.data, self._condition): variable = getattr(self.data.data, self._condition) - if self._condition == "weather": + if self._condition == 'weather': return [k for k, v in CONDITION_CLASSES.items() if self.data.data.weather.value in v][0] return variable.value - return STATE_UNKNOWN + return None @property def unit_of_measurement(self): @@ -149,11 +162,11 @@ class MetOfficeCurrentSensor(Entity): def device_state_attributes(self): """Return the state attributes of the device.""" attr = {} - attr['Sensor Id'] = self._condition - attr['Site Id'] = self.site.id - attr['Site Name'] = self.site.name - attr['Last Update'] = self.data.data.date attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr[ATTR_LAST_UPDATE] = self.data.data.date + attr[ATTR_SENSOR_ID] = self._condition + attr[ATTR_SITE_ID] = self.site.id + attr[ATTR_SITE_NAME] = self.site.name return attr def update(self): @@ -166,21 +179,19 @@ class MetOfficeCurrentData(object): def __init__(self, hass, datapoint, site): """Initialize the data object.""" - self._hass = hass self._datapoint = datapoint self._site = site self.data = None - @Throttle(SCAN_INTERVAL) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Datapoint.""" import datapoint as dp try: forecast = self._datapoint.get_forecast_for_site( - self._site.id, "3hourly") + self._site.id, '3hourly') self.data = forecast.now() except (ValueError, dp.exceptions.APIException) as err: _LOGGER.error("Check Met Office %s", err.args) self.data = None - raise diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 349e55abb5d..77d77949ebd 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC ) -REQUIREMENTS = ['miflora==0.1.16'] +REQUIREMENTS = ['miflora==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -60,11 +60,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MiFlora sensor.""" from miflora import miflora_poller + from miflora.backends.gatttool import GatttoolBackend cache = config.get(CONF_CACHE) poller = miflora_poller.MiFloraPoller( config.get(CONF_MAC), cache_timeout=cache, - adapter=config.get(CONF_ADAPTER)) + adapter=config.get(CONF_ADAPTER), backend=GatttoolBackend) force_update = config.get(CONF_FORCE_UPDATE) median = config.get(CONF_MEDIAN) poller.ble_timeout = config.get(CONF_TIMEOUT) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index bf7de94b5d7..b19f5721e4f 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -6,12 +6,15 @@ https://home-assistant.io/components/sensor.mqtt/ """ import asyncio import logging +import json from datetime import timedelta import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS +from homeassistant.components.mqtt import ( + CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) @@ -24,6 +27,7 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) CONF_EXPIRE_AFTER = 'expire_after' +CONF_JSON_ATTRS = 'json_attributes' DEFAULT_NAME = 'MQTT Sensor' DEFAULT_FORCE_UPDATE = False @@ -32,9 +36,10 @@ DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -55,15 +60,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), value_template, + config.get(CONF_JSON_ATTRS), + config.get(CONF_AVAILABILITY_TOPIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), )]) -class MqttSensor(Entity): +class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, value_template): + force_update, expire_after, value_template, + json_attributes, availability_topic, payload_available, + payload_not_available): """Initialize the sensor.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -73,12 +86,14 @@ class MqttSensor(Entity): self._template = value_template self._expire_after = expire_after self._expiration_trigger = None + self._json_attributes = set(json_attributes) + self._attributes = None + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method must be run in the event loop and returns a coroutine. - """ @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -96,13 +111,27 @@ class MqttSensor(Entity): self._expiration_trigger = async_track_point_in_utc_time( self.hass, self.value_is_expired, expiration_at) + if self._json_attributes: + self._attributes = {} + try: + json_dict = json.loads(payload) + if isinstance(json_dict, dict): + attrs = {k: json_dict[k] for k in + self._json_attributes & json_dict.keys()} + self._attributes = attrs + else: + _LOGGER.warning("JSON result was not a dictionary") + except ValueError: + _LOGGER.warning("MQTT payload could not be parsed as JSON") + _LOGGER.debug("Erroneous JSON: %s", payload) + if self._template is not None: payload = self._template.async_render_with_possible_json_value( payload, self._state) self._state = payload self.async_schedule_update_ha_state() - return mqtt.async_subscribe( + yield from mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @callback @@ -136,3 +165,8 @@ class MqttSensor(Entity): def state(self): """Return the state of the entity.""" return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index 40c6ce7458c..2c0f8eb4d5a 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -80,11 +80,9 @@ class MQTTRoomSensor(Entity): self._distance = None self._updated = None + @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe to MQTT events.""" @callback def update_state(device_id, room, distance): """Update the sensor state.""" diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py old mode 100755 new mode 100644 index 2072251c205..d7443039e57 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -4,20 +4,20 @@ Support for the OpenWeatherMap (OWM) service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.openweathermap/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME, + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.7.1'] +REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) @@ -53,12 +53,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the OpenWeatherMap sensor.""" + from pyowm import OWM + if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - from pyowm import OWM - SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) @@ -136,7 +136,7 @@ class OpenWeatherMapSensor(Entity): data = self.owa_client.data fc_data = self.owa_client.fc_data - if data is None or fc_data is None: + if data is None: return if self.type == 'weather': @@ -174,6 +174,8 @@ class OpenWeatherMapSensor(Entity): self._state = 'not snowing' self._unit_of_measurement = '' elif self.type == 'forecast': + if fc_data is None: + return self._state = fc_data.get_weathers()[0].get_status() diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 0a75d0395ec..b0c40e8f007 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -10,12 +10,13 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN) + CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN, + CONF_SSL) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['plexapi==3.0.3'] +REQUIREMENTS = ['plexapi==3.0.5'] _LOGGER = logging.getLogger(__name__) @@ -24,6 +25,7 @@ CONF_SERVER = 'server' DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Plex' DEFAULT_PORT = 32400 +DEFAULT_SSL = False MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) @@ -35,6 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, }) @@ -48,11 +51,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): plex_host = config.get(CONF_HOST) plex_port = config.get(CONF_PORT) plex_token = config.get(CONF_TOKEN) - plex_url = 'http://{}:{}'.format(plex_host, plex_port) - add_devices([PlexSensor( - name, plex_url, plex_user, plex_password, plex_server, - plex_token)], True) + plex_url = '{}://{}:{}'.format('https' if config.get(CONF_SSL) else 'http', + plex_host, plex_port) + + import plexapi.exceptions + + try: + add_devices([PlexSensor( + name, plex_url, plex_user, plex_password, plex_server, + plex_token)], True) + except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound) as error: + _LOGGER.error(error) + return class PlexSensor(Entity): diff --git a/homeassistant/components/sensor/rainbird.py b/homeassistant/components/sensor/rainbird.py new file mode 100644 index 00000000000..875e9c37bd3 --- /dev/null +++ b/homeassistant/components/sensor/rainbird.py @@ -0,0 +1,80 @@ +""" +Support for Rain Bird Irrigation system LNK WiFi Module. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.rainbird/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.rainbird import DATA_RAINBIRD +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['rainbird'] + +_LOGGER = logging.getLogger(__name__) + +# sensor_type [ description, unit, icon ] +SENSOR_TYPES = { + 'rainsensor': ['Rainsensor', None, 'mdi:water'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a Rain Bird sensor.""" + controller = hass.data[DATA_RAINBIRD] + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append( + RainBirdSensor(controller, sensor_type)) + + add_devices(sensors, True) + + +class RainBirdSensor(Entity): + """A sensor implementation for Rain Bird device.""" + + def __init__(self, controller, sensor_type): + """Initialize the Rain Bird sensor.""" + self._sensor_type = sensor_type + self._controller = controller + self._name = SENSOR_TYPES[self._sensor_type][0] + self._icon = SENSOR_TYPES[self._sensor_type][2] + self._unit_of_measurement = SENSOR_TYPES[self._sensor_type][1] + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating sensor: %s", self._name) + if self._sensor_type == 'rainsensor': + self._state = self._controller.currentRainSensorState() + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return icon.""" + return self._icon diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index e01dbc83422..1c09bc01909 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -10,21 +10,28 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.helpers.entity import Entity from homeassistant.util import slugify +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rfxtrx import ( - ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_FIREEVENT, CONF_DEVICES, DATA_TYPES, - ATTR_DATA_TYPE, ATTR_ENTITY_ID) + ATTR_NAME, ATTR_FIRE_EVENT, ATTR_DATA_TYPE, CONF_AUTOMATIC_ADD, + CONF_FIRE_EVENT, CONF_DEVICES, DATA_TYPES, CONF_DATA_TYPE) DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): rfxtrx.DOMAIN, - vol.Optional(CONF_DEVICES, default={}): vol.All(dict, rfxtrx.valid_sensor), - vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_DATA_TYPE, default=[]): + vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, }, extra=vol.ALLOW_EXTRA) @@ -49,7 +56,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): break for _data_type in data_types: new_sensor = RfxtrxSensor(None, entity_info[ATTR_NAME], - _data_type, entity_info[ATTR_FIREEVENT]) + _data_type, entity_info[ATTR_FIRE_EVENT]) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor rfxtrx.RFX_DEVICES[device_id] = sub_sensors @@ -78,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return # Add entity if not exist and the automatic_add is True - if not config[ATTR_AUTOMATIC_ADD]: + if not config[CONF_AUTOMATIC_ADD]: return pkt_id = "".join("{0:02x}".format(x) for x in event.data) diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index 982e7d9559b..95bf207acf8 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VALUE_TEMPLATE) -REQUIREMENTS = ['pysnmp==4.4.2'] +REQUIREMENTS = ['pysnmp==4.4.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sochain.py b/homeassistant/components/sensor/sochain.py new file mode 100644 index 00000000000..572d0f52921 --- /dev/null +++ b/homeassistant/components/sensor/sochain.py @@ -0,0 +1,87 @@ +""" +Support for watching multiple cryptocurrencies. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sochain/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +REQUIREMENTS = ['python-sochain-api==0.0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ADDRESS = 'address' +CONF_NETWORK = 'network' +CONF_ATTRIBUTION = "Data provided by chain.so" + +DEFAULT_NAME = 'Crypto Balance' + +SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_NETWORK): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the sochain sensors.""" + from pysochain import ChainSo + address = config.get(CONF_ADDRESS) + network = config.get(CONF_NETWORK) + name = config.get(CONF_NAME) + + session = async_get_clientsession(hass) + chainso = ChainSo(network, address, hass.loop, session) + + async_add_devices([SochainSensor(name, network.upper(), chainso)], True) + + +class SochainSensor(Entity): + """Representation of a Sochain sensor.""" + + def __init__(self, name, unit_of_measurement, chainso): + """Initialize the sensor.""" + self._name = name + self._unit_of_measurement = unit_of_measurement + self.chainso = chainso + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self.chainso.data.get("confirmed_balance") \ + if self.chainso is not None else None + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + + @asyncio.coroutine + def async_update(self): + """Get the latest state of the sensor.""" + yield from self.chainso.async_get_data() diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index a6932e2aebb..19281d36d88 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -175,15 +175,20 @@ class StatisticsSensor(Entity): self._purge_old() if not self.is_binary: - try: + try: # require only one data point self.mean = round(statistics.mean(self.states), 2) self.median = round(statistics.median(self.states), 2) + except statistics.StatisticsError as err: + _LOGGER.error(err) + self.mean = self.median = STATE_UNKNOWN + + try: # require at least two data points self.stdev = round(statistics.stdev(self.states), 2) self.variance = round(statistics.variance(self.states), 2) except statistics.StatisticsError as err: _LOGGER.error(err) - self.mean = self.median = STATE_UNKNOWN self.stdev = self.variance = STATE_UNKNOWN + if self.states: self.total = round(sum(self.states), 2) self.min = min(self.states) diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index 8645d4ee7c6..88cb786e66d 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -21,12 +21,13 @@ CONF_ACCOUNTS = 'accounts' ICON = 'mdi:steam' -STATE_ONLINE = 'Online' -STATE_BUSY = 'Busy' -STATE_AWAY = 'Away' -STATE_SNOOZE = 'Snooze' -STATE_TRADE = 'Trade' -STATE_PLAY = 'Play' +STATE_OFFLINE = 'offline' +STATE_ONLINE = 'online' +STATE_BUSY = 'busy' +STATE_AWAY = 'away' +STATE_SNOOZE = 'snooze' +STATE_LOOKING_TO_TRADE = 'looking_to_trade' +STATE_LOOKING_TO_PLAY = 'looking_to_play' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -40,17 +41,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Steam platform.""" import steam as steamod steamod.api.key.set(config.get(CONF_API_KEY)) + # Initialize steammods app list before creating sensors + # to benefit from internal caching of the list. + steam_app_list = steamod.apps.app_list() add_devices( [SteamSensor(account, - steamod) for account in config.get(CONF_ACCOUNTS)], True) + steamod, + steam_app_list) + for account in config.get(CONF_ACCOUNTS)], True) class SteamSensor(Entity): """A class for the Steam account.""" - def __init__(self, account, steamod): + def __init__(self, account, steamod, steam_app_list): """Initialize the sensor.""" self._steamod = steamod + self._steam_app_list = steam_app_list self._account = account self._profile = None self._game = self._state = self._name = self._avatar = None @@ -75,28 +82,39 @@ class SteamSensor(Entity): """Update device state.""" try: self._profile = self._steamod.user.profile(self._account) - if self._profile.current_game[2] is None: - self._game = 'None' - else: - self._game = self._profile.current_game[2] + self._game = self._get_current_game() self._state = { 1: STATE_ONLINE, 2: STATE_BUSY, 3: STATE_AWAY, 4: STATE_SNOOZE, - 5: STATE_TRADE, - 6: STATE_PLAY, - }.get(self._profile.status, 'Offline') + 5: STATE_LOOKING_TO_TRADE, + 6: STATE_LOOKING_TO_PLAY, + }.get(self._profile.status, STATE_OFFLINE) self._name = self._profile.persona self._avatar = self._profile.avatar_medium except self._steamod.api.HTTPTimeoutError as error: _LOGGER.warning(error) self._game = self._state = self._name = self._avatar = None + def _get_current_game(self): + game_id = self._profile.current_game[0] + game_extra_info = self._profile.current_game[2] + + if game_extra_info: + return game_extra_info + + if game_id and game_id in self._steam_app_list: + # The app list always returns a tuple + # with the game id and the game name + return self._steam_app_list[game_id][1] + + return None + @property def device_state_attributes(self): """Return the state attributes.""" - return {'game': self._game} + return {'game': self._game} if self._game else None @property def entity_picture(self): diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 40b77d278af..a489adf6776 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -5,17 +5,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.swiss_public_transport/ """ import asyncio -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION -from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util REQUIREMENTS = ['python_opendata_transport==0.0.3'] @@ -51,36 +51,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Swiss public transport sensor.""" + from opendata_transport import OpendataTransport, exceptions + name = config.get(CONF_NAME) start = config.get(CONF_START) destination = config.get(CONF_DESTINATION) - connection = SwissPublicTransportSensor(hass, start, destination, name) - yield from connection.async_update() + session = async_get_clientsession(hass) + opendata = OpendataTransport(start, destination, hass.loop, session) - if connection.state is None: + try: + yield from opendata.async_get_data() + except exceptions.OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " "if your station names are valid") - return False + return - async_add_devices([connection]) + async_add_devices( + [SwissPublicTransportSensor(opendata, start, destination, name)]) class SwissPublicTransportSensor(Entity): """Implementation of an Swiss public transport sensor.""" - def __init__(self, hass, start, destination, name): + def __init__(self, opendata, start, destination, name): """Initialize the sensor.""" - from opendata_transport import OpendataTransport - - self.hass = hass + self._opendata = opendata self._name = name self._from = start self._to = destination - self._websession = async_get_clientsession(self.hass) - self._opendata = OpendataTransport( - self._from, self._to, self.hass.loop, self._websession) @property def name(self): @@ -131,4 +131,3 @@ class SwissPublicTransportSensor(Entity): yield from self._opendata.async_get_data() except OpendataTransportError: _LOGGER.error("Unable to retrieve data from transport.opendata.ch") - self._opendata = None diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py old mode 100755 new mode 100644 index 8e6f7b404fd..57e03cf153f --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.4.2'] +REQUIREMENTS = ['psutil==5.4.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 781f2e006d9..8c7259ff800 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -18,8 +18,10 @@ ATTR_DEVICE = 'device' ATTR_NAME = 'name' ATTR_ZONE = 'zone' -SENSOR_TYPES = ['temperature', 'humidity', 'power', - 'link', 'heating', 'tado mode', 'overlay'] +CLIMATE_SENSOR_TYPES = ['temperature', 'humidity', 'power', + 'link', 'heating', 'tado mode', 'overlay'] + +HOT_WATER_SENSOR_TYPES = ['power', 'link', 'tado mode', 'overlay'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -35,10 +37,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor_items = [] for zone in zones: if zone['type'] == 'HEATING': - for variable in SENSOR_TYPES: + for variable in CLIMATE_SENSOR_TYPES: sensor_items.append(create_zone_sensor( tado, zone, zone['name'], zone['id'], variable)) + elif zone['type'] == 'HOT_WATER': + for variable in HOT_WATER_SENSOR_TYPES: + sensor_items.append(create_zone_sensor( + tado, zone, zone['name'], zone['id'], + variable + )) me_data = tado.get_me() sensor_items.append(create_device_sensor( diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index b347439e08d..1d9bf0b7a9a 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -31,6 +31,11 @@ SENSOR_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) +SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + SENSOR_SCHEMA, +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 1eda9cb58fd..678d9afb81d 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -55,12 +55,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) port = config.get(CONF_PORT) - transmission_api = transmissionrpc.Client( - host, port=port, user=username, password=password) try: + transmission_api = transmissionrpc.Client( + host, port=port, user=username, password=password) transmission_api.session_stats() - except TransmissionError: - _LOGGER.exception("Connection to Transmission API failed") + except TransmissionError as error: + _LOGGER.error( + "Connection to Transmission API failed on %s:%s with message %s", + host, port, error.original + ) return False # pylint: disable=global-statement diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py index 3b59f06be31..c3c8cde0177 100644 --- a/homeassistant/components/sensor/xbox_live.py +++ b/homeassistant/components/sensor/xbox_live.py @@ -57,12 +57,12 @@ class XboxSensor(Entity): self._api = api # get profile info - profile = self._api.get_user_profile(self._xuid) + profile = self._api.get_user_gamercard(self._xuid) if profile.get('success', True) and profile.get('code', 0) != 28: self.success_init = True - self._gamertag = profile.get('Gamertag') - self._picture = profile.get('GameDisplayPicRaw') + self._gamertag = profile.get('gamertag') + self._picture = profile.get('gamerpicSmallSslImagePath') else: self.success_init = False diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index 846b221d5e3..e066e38fb1e 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['yahooweather==0.9'] +REQUIREMENTS = ['yahooweather==0.10'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index b31b942f486..1189a53bb09 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity import homeassistant.components.zoneminder as zoneminder import homeassistant.helpers.config_validation as cv @@ -22,9 +23,19 @@ CONF_INCLUDE_ARCHIVED = "include_archived" DEFAULT_INCLUDE_ARCHIVED = False +SENSOR_TYPES = { + 'all': ['Events'], + 'hour': ['Events Last Hour'], + 'day': ['Events Last Day'], + 'week': ['Events Last Week'], + 'month': ['Events Last Month'], +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_INCLUDE_ARCHIVED, default=DEFAULT_INCLUDE_ARCHIVED): cv.boolean, + vol.Optional(CONF_MONITORED_CONDITIONS, default=['all']): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), }) @@ -39,10 +50,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.append( ZMSensorMonitors(int(i['Monitor']['Id']), i['Monitor']['Name']) ) - sensors.append( - ZMSensorEvents(int(i['Monitor']['Id']), i['Monitor']['Name'], - include_archived) - ) + + for sensor in config[CONF_MONITORED_CONDITIONS]: + sensors.append( + ZMSensorEvents(int(i['Monitor']['Id']), + i['Monitor']['Name'], + include_archived, sensor) + ) add_devices(sensors) @@ -69,7 +83,7 @@ class ZMSensorMonitors(Entity): def update(self): """Update the sensor.""" monitor = zoneminder.get_state( - 'api/monitors/%i.json' % self._monitor_id + 'api/monitors/{}.json'.format(self._monitor_id) ) if monitor['monitor']['Monitor']['Function'] is None: self._state = STATE_UNKNOWN @@ -80,17 +94,20 @@ class ZMSensorMonitors(Entity): class ZMSensorEvents(Entity): """Get the number of events for each monitor.""" - def __init__(self, monitor_id, monitor_name, include_archived): + def __init__(self, monitor_id, monitor_name, include_archived, + sensor_type): """Initialize event sensor.""" self._monitor_id = monitor_id self._monitor_name = monitor_name self._include_archived = include_archived + self._type = sensor_type + self._name = SENSOR_TYPES[sensor_type][0] self._state = None @property def name(self): """Return the name of the sensor.""" - return '{} Events'.format(self._monitor_name) + return '{} {}'.format(self._monitor_name, self._name) @property def unit_of_measurement(self): @@ -104,13 +121,22 @@ class ZMSensorEvents(Entity): def update(self): """Update the sensor.""" - archived_filter = '/Archived:0' + date_filter = '1%20{}'.format(self._type) + if self._type == 'all': + # The consoleEvents API uses DATE_SUB, so give it + # something large + date_filter = '100%20year' + + archived_filter = '/Archived=:0' if self._include_archived: archived_filter = '' event = zoneminder.get_state( - 'api/events/index/MonitorId:%i%s.json' % (self._monitor_id, - archived_filter) + 'api/events/consoleEvents/{}{}.json'.format(date_filter, + archived_filter) ) - self._state = event['pagination']['count'] + try: + self._state = event['results'][str(self._monitor_id)] + except (TypeError, KeyError): + self._state = '0' diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 90a1bbbc613..522939a213a 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -364,6 +364,38 @@ abode: description: Entity id of the quick action to trigger. example: 'binary_sensor.home_quick_action' +snips: + say: + description: Send a TTS message to Snips. + fields: + text: + description: Text to say. + example: My name is snips + site_id: + description: Site to use to start session, defaults to default (optional) + example: bedroom + custom_data: + description: custom data that will be included with all messages in this session + example: user=UserName + say_action: + description: Send a TTS message to Snips to listen for a response. + fields: + text: + description: Text to say + example: My name is snips + site_id: + description: Site to use to start session, defaults to default (optional) + example: bedroom + custom_data: + description: custom data that will be included with all messages in this session + example: user=UserName + can_be_enqueued: + description: If True, session waits for an open session to end, if False session is dropped if one is running + example: True + intent_filter: + description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. + example: turnOnLights, turnOffLights + input_boolean: toggle: description: Toggles an input boolean. @@ -418,6 +450,38 @@ input_number: description: Entity id of the input number the should be decremented. example: 'input_number.threshold' +input_select: + select_option: + description: Select an option of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the value. + example: 'input_select.my_select' + option: + description: Option to be selected. + example: '"Item A"' + set_options: + description: Set the options of an input select entity. + fields: + entity_id: + description: Entity id of the input select to set the new options for. + example: 'input_select.my_select' + options: + description: Options for the input select entity. + example: '["Item A", "Item B", "Item C"]' + select_previous: + description: Select the previous options of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the previous value for. + example: 'input_select.my_select' + select_next: + description: Select the next options of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the next value for. + example: 'input_select.my_select' + homeassistant: check_config: description: Check the Home Assistant configuration files for errors. Errors will be displayed in the Home Assistant log. diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index a302f25bd00..d221c8512c6 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -7,16 +7,30 @@ https://home-assistant.io/components/snips/ import asyncio import json import logging +from datetime import timedelta + import voluptuous as vol + from homeassistant.helpers import intent, config_validation as cv +import homeassistant.components.mqtt as mqtt DOMAIN = 'snips' DEPENDENCIES = ['mqtt'] + CONF_INTENTS = 'intents' CONF_ACTION = 'action' +SERVICE_SAY = 'say' +SERVICE_SAY_ACTION = 'say_action' + INTENT_TOPIC = 'hermes/intent/#' +ATTR_TEXT = 'text' +ATTR_SITE_ID = 'site_id' +ATTR_CUSTOM_DATA = 'custom_data' +ATTR_CAN_BE_ENQUEUED = 'can_be_enqueued' +ATTR_INTENT_FILTER = 'intent_filter' + _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ @@ -38,6 +52,20 @@ INTENT_SCHEMA = vol.Schema({ }] }, extra=vol.ALLOW_EXTRA) +SERVICE_SCHEMA_SAY = vol.Schema({ + vol.Required(ATTR_TEXT): str, + vol.Optional(ATTR_SITE_ID, default='default'): str, + vol.Optional(ATTR_CUSTOM_DATA, default=''): str +}) + +SERVICE_SCHEMA_SAY_ACTION = vol.Schema({ + vol.Required(ATTR_TEXT): str, + vol.Optional(ATTR_SITE_ID, default='default'): str, + vol.Optional(ATTR_CUSTOM_DATA, default=''): str, + vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean, + vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list), +}) + @asyncio.coroutine def async_setup(hass, config): @@ -59,21 +87,87 @@ def async_setup(hass, config): _LOGGER.error('Intent has invalid schema: %s. %s', err, request) return - intent_type = request['intent']['intentName'].split('__')[-1] + if request['intent']['intentName'].startswith('user_'): + intent_type = request['intent']['intentName'].split('__')[-1] + else: + intent_type = request['intent']['intentName'].split(':')[-1] + snips_response = None slots = {} for slot in request.get('slots', []): - if 'value' in slot['value']: - slots[slot['slotName']] = {'value': slot['value']['value']} - else: - slots[slot['slotName']] = {'value': slot['rawValue']} + slots[slot['slotName']] = {'value': resolve_slot_values(slot)} try: - yield from intent.async_handle( + intent_response = yield from intent.async_handle( hass, DOMAIN, intent_type, slots, request['input']) + if 'plain' in intent_response.speech: + snips_response = intent_response.speech['plain']['speech'] + except intent.UnknownIntent as err: + _LOGGER.warning("Received unknown intent %s", + request['intent']['intentName']) + snips_response = "Unknown Intent" except intent.IntentError: _LOGGER.exception("Error while handling intent: %s.", intent_type) + snips_response = "Error while handling intent" + + notification = {'sessionId': request.get('sessionId', 'default'), + 'text': snips_response} + + _LOGGER.debug("send_response %s", json.dumps(notification)) + mqtt.async_publish(hass, 'hermes/dialogueManager/endSession', + json.dumps(notification)) yield from hass.components.mqtt.async_subscribe( INTENT_TOPIC, message_received) + @asyncio.coroutine + def snips_say(call): + """Send a Snips notification message.""" + notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), + 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), + 'init': {'type': 'notification', + 'text': call.data.get(ATTR_TEXT)}} + mqtt.async_publish(hass, 'hermes/dialogueManager/startSession', + json.dumps(notification)) + return + + @asyncio.coroutine + def snips_say_action(call): + """Send a Snips action message.""" + notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), + 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), + 'init': {'type': 'action', + 'text': call.data.get(ATTR_TEXT), + 'canBeEnqueued': call.data.get( + ATTR_CAN_BE_ENQUEUED, True), + 'intentFilter': + call.data.get(ATTR_INTENT_FILTER, [])}} + mqtt.async_publish(hass, 'hermes/dialogueManager/startSession', + json.dumps(notification)) + return + + hass.services.async_register( + DOMAIN, SERVICE_SAY, snips_say, + schema=SERVICE_SCHEMA_SAY) + hass.services.async_register( + DOMAIN, SERVICE_SAY_ACTION, snips_say_action, + schema=SERVICE_SCHEMA_SAY_ACTION) + return True + + +def resolve_slot_values(slot): + """Convert snips builtin types to useable values.""" + if 'value' in slot['value']: + value = slot['value']['value'] + else: + value = slot['rawValue'] + + if slot.get('entity') == "snips/duration": + delta = timedelta(weeks=slot['value']['weeks'], + days=slot['value']['days'], + hours=slot['value']['hours'], + minutes=slot['value']['minutes'], + seconds=slot['value']['seconds']) + value = delta.seconds + + return value diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 5bfea4eff0e..66a416c5bea 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -7,12 +7,10 @@ at https://home-assistant.io/components/switch/ import asyncio from datetime import timedelta import logging -import os import voluptuous as vol from homeassistant.core import callback -from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -123,19 +121,15 @@ def async_setup(hass, config): if update_tasks: 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')) - hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handle_switch_service, - descriptions.get(SERVICE_TURN_OFF), schema=SWITCH_SERVICE_SCHEMA) + schema=SWITCH_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TURN_ON, async_handle_switch_service, - descriptions.get(SERVICE_TURN_ON), schema=SWITCH_SERVICE_SCHEMA) + schema=SWITCH_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TOGGLE, async_handle_switch_service, - descriptions.get(SERVICE_TOGGLE), schema=SWITCH_SERVICE_SCHEMA) + schema=SWITCH_SERVICE_SCHEMA) return True diff --git a/homeassistant/components/switch/digitalloggers.py b/homeassistant/components/switch/digitalloggers.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py index 5fd37c84986..c20a638c00f 100644 --- a/homeassistant/components/switch/insteon_local.py +++ b/homeassistant/components/switch/insteon_local.py @@ -9,7 +9,6 @@ from datetime import timedelta from homeassistant.components.switch import SwitchDevice import homeassistant.util as util -from homeassistant.util.json import load_json, save_json _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -17,8 +16,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['insteon_local'] DOMAIN = 'switch' -INSTEON_LOCAL_SWITCH_CONF = 'insteon_local_switch.conf' - MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -26,83 +23,33 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Insteon local switch platform.""" insteonhub = hass.data['insteon_local'] - - conf_switches = load_json(hass.config.path(INSTEON_LOCAL_SWITCH_CONF)) - if conf_switches: - for device_id in conf_switches: - setup_switch( - device_id, conf_switches[device_id], insteonhub, hass, - add_devices) - else: - linked = insteonhub.get_linked() - - for device_id in linked: - if linked[device_id]['cat_type'] == 'switch'\ - and device_id not in conf_switches: - request_configuration(device_id, insteonhub, - linked[device_id]['model_name'] + ' ' + - linked[device_id]['sku'], - hass, add_devices) - - -def request_configuration( - device_id, insteonhub, model, hass, add_devices_callback): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - # We got an error if this method is called while we are configuring - if device_id in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[device_id], 'Failed to register, please try again.') - + if discovery_info is None: return - def insteon_switch_config_callback(data): - """Handle configuration changes.""" - setup_switch(device_id, data.get('name'), insteonhub, hass, - add_devices_callback) + linked = discovery_info['linked'] + device_list = [] + for device_id in linked: + if linked[device_id]['cat_type'] == 'switch': + device = insteonhub.switch(device_id) + device_list.append( + InsteonLocalSwitchDevice(device) + ) - _CONFIGURING[device_id] = configurator.request_config( - 'Insteon Switch ' + model + ' addr: ' + device_id, - insteon_switch_config_callback, - description=('Enter a name for ' + model + ' addr: ' + device_id), - entity_picture='/static/images/config_insteon.png', - submit_caption='Confirm', - fields=[{'id': 'name', 'name': 'Name', 'type': ''}] - ) - - -def setup_switch(device_id, name, insteonhub, hass, add_devices_callback): - """Set up the switch.""" - if device_id in _CONFIGURING: - request_id = _CONFIGURING.pop(device_id) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.info("Device configuration done") - - conf_switch = load_json(hass.config.path(INSTEON_LOCAL_SWITCH_CONF)) - if device_id not in conf_switch: - conf_switch[device_id] = name - - save_json(hass.config.path(INSTEON_LOCAL_SWITCH_CONF), conf_switch) - - device = insteonhub.switch(device_id) - add_devices_callback([InsteonLocalSwitchDevice(device, name)]) + add_devices(device_list) class InsteonLocalSwitchDevice(SwitchDevice): """An abstract Class for an Insteon node.""" - def __init__(self, node, name): + def __init__(self, node): """Initialize the device.""" self.node = node - self.node.deviceName = name self._state = False @property def name(self): """Return the name of the node.""" - return self.node.deviceName + return self.node.device_id @property def unique_id(self): diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 0f1ec62eaee..f0fd397710e 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -8,71 +8,39 @@ import logging from typing import Callable # noqa from homeassistant.components.switch import SwitchDevice, DOMAIN -import homeassistant.components.isy994 as isy -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN +from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, + ISYDevice) from homeassistant.helpers.typing import ConfigType # noqa _LOGGER = logging.getLogger(__name__) -VALUE_TO_STATE = { - False: STATE_OFF, - True: STATE_ON, -} - -UOM = ['2', '78'] -STATES = [STATE_OFF, STATE_ON, 'true', 'false'] - # pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 switch 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 = [] - - for node in isy.filter_nodes(isy.NODES, units=UOM, - states=STATES): + for node in hass.data[ISY994_NODES][DOMAIN]: if not node.dimmable: devices.append(ISYSwitchDevice(node)) - for node in isy.GROUPS: - devices.append(ISYSwitchDevice(node)) - - for program in isy.PROGRAMS.get(DOMAIN, []): - try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass - else: - devices.append(ISYSwitchProgram(program.name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + devices.append(ISYSwitchProgram(name, status, actions)) add_devices(devices) -class ISYSwitchDevice(isy.ISYDevice, SwitchDevice): +class ISYSwitchDevice(ISYDevice, SwitchDevice): """Representation of an ISY994 switch device.""" def __init__(self, node) -> None: """Initialize the ISY994 switch device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def is_on(self) -> bool: """Get whether the ISY994 device is in the on state.""" - return self.state == STATE_ON - - @property - def state(self) -> str: - """Get the state of the ISY994 device.""" - if self.is_unknown(): - return None - else: - return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN) + return bool(self.value) def turn_off(self, **kwargs) -> None: """Send the turn on command to the ISY994 switch.""" @@ -90,7 +58,7 @@ class ISYSwitchProgram(ISYSwitchDevice): def __init__(self, name: str, node, actions) -> None: """Initialize the ISY994 switch program.""" - ISYSwitchDevice.__init__(self, node) + super().__init__(node) self._name = name self._actions = actions diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index b340bf5f43a..d1c6d717945 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -89,6 +89,11 @@ class KNXSwitch(SwitchDevice): """Return the name of the KNX device.""" return self.device.name + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DATA_KNX].connected + @property def should_poll(self): """No polling needed within KNX.""" diff --git a/homeassistant/components/switch/mochad.py b/homeassistant/components/switch/mochad.py index da8f96dc1f0..f80784271c2 100644 --- a/homeassistant/components/switch/mochad.py +++ b/homeassistant/components/switch/mochad.py @@ -50,7 +50,12 @@ class MochadSwitch(SwitchDevice): self._comm_type = dev.get(mochad.CONF_COMM_TYPE, 'pl') self.device = device.Device(ctrl, self._address, comm_type=self._comm_type) - self._state = self._get_device_status() + # Init with false to avoid locking HA for long on CM19A (goes from rf + # to pl via TM751, but not other way around) + if self._comm_type == 'pl': + self._state = self._get_device_status() + else: + self._state = False @property def name(self): @@ -59,17 +64,37 @@ class MochadSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self._state = True + from pymochad.exceptions import MochadException + _LOGGER.debug("Reconnect %s:%s", self._controller.server, + self._controller.port) with mochad.REQ_LOCK: - self.device.send_cmd('on') - self._controller.read_data() + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + self.device.send_cmd('on') + # No read data on CM19A which is rf only + if self._comm_type == 'pl': + self._controller.read_data() + self._state = True + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) def turn_off(self, **kwargs): """Turn the switch off.""" - self._state = False + from pymochad.exceptions import MochadException + _LOGGER.debug("Reconnect %s:%s", self._controller.server, + self._controller.port) with mochad.REQ_LOCK: - self.device.send_cmd('off') - self._controller.read_data() + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + self.device.send_cmd('off') + # No read data on CM19A which is rf only + if self._comm_type == 'pl': + self._controller.read_data() + self._state = False + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) def _get_device_status(self): """Get the status of the switch from mochad.""" diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 21820b4a015..a4aea1ded9f 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -11,8 +11,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, - CONF_RETAIN) + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + MqttAvailability) from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, @@ -24,26 +25,17 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' - DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False -DEFAULT_PAYLOAD_AVAILABLE = 'ON' -DEFAULT_PAYLOAD_NOT_AVAILABLE = 'OFF' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_PAYLOAD_AVAILABLE, - default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -72,34 +64,31 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): )]) -class MqttSwitch(SwitchDevice): +class MqttSwitch(MqttAvailability, SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) self._state = False self._name = name self._state_topic = state_topic self._command_topic = command_topic - self._availability_topic = availability_topic - self._available = True if availability_topic is None else False self._qos = qos self._retain = retain self._payload_on = payload_on self._payload_off = payload_off self._optimistic = optimistic self._template = value_template - self._payload_available = payload_available - self._payload_not_available = payload_not_available @asyncio.coroutine def async_added_to_hass(self): - """Subscribe to MQTT events. + """Subscribe to MQTT events.""" + yield from super().async_added_to_hass() - This method is a coroutine. - """ @callback def state_message_received(topic, payload, qos): """Handle new MQTT state messages.""" @@ -113,16 +102,6 @@ class MqttSwitch(SwitchDevice): self.async_schedule_update_ha_state() - @callback - def availability_message_received(topic, payload, qos): - """Handle new MQTT availability messages.""" - if payload == self._payload_available: - self._available = True - elif payload == self._payload_not_available: - self._available = False - - self.async_schedule_update_ha_state() - if self._state_topic is None: # Force into optimistic mode. self._optimistic = True @@ -131,11 +110,6 @@ class MqttSwitch(SwitchDevice): self.hass, self._state_topic, state_message_received, self._qos) - if self._availability_topic is not None: - yield from mqtt.async_subscribe( - self.hass, self._availability_topic, - availability_message_received, self._qos) - @property def should_poll(self): """Return the polling state.""" @@ -146,11 +120,6 @@ class MqttSwitch(SwitchDevice): """Return the name of the switch.""" return self._name - @property - def available(self) -> bool: - """Return if switch is available.""" - return self._available - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 131ec58ae67..51184859fc6 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -4,14 +4,11 @@ Support for MySensors switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mysensors/ """ -import os - import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.config import load_yaml_config_file from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON ATTR_IR_CODE = 'V_IR_SEND' @@ -62,12 +59,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in _devices: device.turn_on(**kwargs) - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service, - descriptions.get(SERVICE_SEND_IR_CODE), schema=SEND_IR_CODE_SERVICE_SCHEMA) diff --git a/homeassistant/components/switch/rainbird.py b/homeassistant/components/switch/rainbird.py index c1dbfbc4e72..ee283b3c269 100644 --- a/homeassistant/components/switch/rainbird.py +++ b/homeassistant/components/switch/rainbird.py @@ -2,29 +2,26 @@ Support for Rain Bird Irrigation system LNK WiFi Module. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rainbird/ +https://home-assistant.io/components/switch.rainbird/ """ import logging import voluptuous as vol +from homeassistant.components.rainbird import DATA_RAINBIRD from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_PLATFORM, CONF_SWITCHES, CONF_ZONE, +from homeassistant.const import (CONF_SWITCHES, CONF_ZONE, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME, - CONF_SCAN_INTERVAL, CONF_HOST, CONF_PASSWORD) + CONF_SCAN_INTERVAL) from homeassistant.helpers import config_validation as cv -from homeassistant.exceptions import PlatformNotReady -REQUIREMENTS = ['pyrainbird==0.1.0'] +DEPENDENCIES = ['rainbird'] DOMAIN = 'rainbird' _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PLATFORM): DOMAIN, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SWITCHES, default={}): vol.Schema({ cv.string: { vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -38,20 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Rain Bird switches over a Rain Bird controller.""" - server = config.get(CONF_HOST) - password = config.get(CONF_PASSWORD) - - from pyrainbird import RainbirdController - controller = RainbirdController(_LOGGER) - controller.setConfig(server, password) - - _LOGGER.debug("Rain Bird Controller set to " + str(server)) - - if controller.currentIrrigation() == -1: - _LOGGER.error("Error getting state. Possible configuration issues") - raise PlatformNotReady - else: - _LOGGER.debug("Initialized Rain Bird Controller") + controller = hass.data[DATA_RAINBIRD] devices = [] for dev_id, switch in config.get(CONF_SWITCHES).items(): diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 1361d22de18..7dd1d25ad94 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -6,14 +6,31 @@ https://home-assistant.io/components/switch.rfxtrx/ """ import logging +import voluptuous as vol + import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchDevice, 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'] _LOGGER = logging.getLogger(__name__) -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): diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index e48ac1a4d7d..94a61314d1d 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -48,18 +48,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" import rpi_rf + from threading import RLock gpio = config.get(CONF_GPIO) rfdevice = rpi_rf.RFDevice(gpio) + rfdevice_lock = RLock() switches = config.get(CONF_SWITCHES) devices = [] for dev_name, properties in switches.items(): devices.append( RPiRFSwitch( - hass, properties.get(CONF_NAME, dev_name), rfdevice, + rfdevice_lock, properties.get(CONF_PROTOCOL), properties.get(CONF_PULSELENGTH), properties.get(CONF_SIGNAL_REPETITIONS), @@ -79,13 +81,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RPiRFSwitch(SwitchDevice): """Representation of a GPIO RF switch.""" - def __init__(self, hass, name, rfdevice, protocol, pulselength, + def __init__(self, name, rfdevice, lock, protocol, pulselength, signal_repetitions, code_on, code_off): """Initialize the switch.""" - self._hass = hass self._name = name self._state = False self._rfdevice = rfdevice + self._lock = lock self._protocol = protocol self._pulselength = pulselength self._code_on = code_on @@ -109,9 +111,10 @@ class RPiRFSwitch(SwitchDevice): def _send_code(self, code_list, protocol, pulselength): """Send the code(s) with a specified pulselength.""" - _LOGGER.info("Sending code(s): %s", code_list) - for code in code_list: - self._rfdevice.tx_code(code, protocol, pulselength) + with self._lock: + _LOGGER.info("Sending code(s): %s", code_list) + for code in code_list: + self._rfdevice.tx_code(code, protocol, pulselength) return True def turn_on(self): diff --git a/homeassistant/components/switch/snmp.py b/homeassistant/components/switch/snmp.py index 99ba9d8cd54..115e31cb733 100644 --- a/homeassistant/components/switch/snmp.py +++ b/homeassistant/components/switch/snmp.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysnmp==4.4.2'] +REQUIREMENTS = ['pysnmp==4.4.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 93ebf98e9ac..64dafdcadef 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -38,6 +38,11 @@ SWITCH_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) +SWITCH_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + SWITCH_SCHEMA, +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), }) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 0772cc9277c..aa2e70e0020 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -9,7 +9,8 @@ import time import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import ( + SwitchDevice, PLATFORM_SCHEMA, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH) from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) import homeassistant.helpers.config_validation as cv @@ -17,17 +18,17 @@ REQUIREMENTS = ['pyHS100==0.3.0'] _LOGGER = logging.getLogger(__name__) -ATTR_CURRENT_CONSUMPTION = 'current_consumption' -ATTR_TOTAL_CONSUMPTION = 'total_consumption' -ATTR_DAILY_CONSUMPTION = 'daily_consumption' -ATTR_CURRENT = 'current' +ATTR_TOTAL_ENERGY_KWH = 'total_energy_kwh' +ATTR_CURRENT_A = 'current_a' CONF_LEDS = 'enable_leds' +DEFAULT_NAME = 'TP-Link Switch' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_LEDS, default=True): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_LEDS): cv.boolean, }) @@ -49,7 +50,8 @@ class SmartPlugSwitch(SwitchDevice): """Initialize the switch.""" self.smartplug = smartplug self._name = name - self._leds_on = leds_on + if leds_on is not None: + self.smartplug.led = leds_on self._state = None self._available = True # Set up emeter cache @@ -94,24 +96,23 @@ class SmartPlugSwitch(SwitchDevice): if self._name is None: self._name = self.smartplug.alias - self.smartplug.led = self._leds_on - if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() - self._emeter_params[ATTR_CURRENT_CONSUMPTION] \ - = "%.1f W" % emeter_readings["power"] - self._emeter_params[ATTR_TOTAL_CONSUMPTION] \ - = "%.2f kW" % emeter_readings["total"] + self._emeter_params[ATTR_CURRENT_POWER_W] \ + = "{:.2f}".format(emeter_readings["power"]) + self._emeter_params[ATTR_TOTAL_ENERGY_KWH] \ + = "{:.3f}".format(emeter_readings["total"]) self._emeter_params[ATTR_VOLTAGE] \ - = "%.2f V" % emeter_readings["voltage"] - self._emeter_params[ATTR_CURRENT] \ - = "%.1f A" % emeter_readings["current"] + = "{:.1f}".format(emeter_readings["voltage"]) + self._emeter_params[ATTR_CURRENT_A] \ + = "{:.2f}".format(emeter_readings["current"]) emeter_statics = self.smartplug.get_emeter_daily() try: - self._emeter_params[ATTR_DAILY_CONSUMPTION] \ - = "%.2f kW" % emeter_statics[int(time.strftime("%e"))] + self._emeter_params[ATTR_TODAY_ENERGY_KWH] \ + = "{:.3f}".format( + emeter_statics[int(time.strftime("%e"))]) except KeyError: # Device returned no daily history pass diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index 656a6227358..840fdae44d9 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -43,12 +43,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) port = config.get(CONF_PORT) - transmission_api = transmissionrpc.Client( - host, port=port, user=username, password=password) try: + transmission_api = transmissionrpc.Client( + host, port=port, user=username, password=password) transmission_api.session_stats() - except TransmissionError: - _LOGGING.error("Connection to Transmission API failed") + except TransmissionError as error: + _LOGGING.error( + "Connection to Transmission API failed on %s:%s with message %s", + host, port, error.original + ) return False add_devices([TransmissionSwitch(transmission_api, name)]) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 534c4ac0a32..49a400f4a23 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.2'] +REQUIREMENTS = ['python-miio==0.3.3'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 60f707b1e33..d25f32eacc7 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -4,7 +4,6 @@ Support for system log. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/system_log/ """ -import os import re import asyncio import logging @@ -15,7 +14,6 @@ from collections import deque import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH -from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -84,13 +82,8 @@ def async_setup(hass, config): # Only one service so far handler.records.clear() - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml')) - hass.services.async_register( DOMAIN, SERVICE_CLEAR, async_service_handler, - descriptions[DOMAIN].get(SERVICE_CLEAR), schema=SERVICE_CLEAR_SCHEMA) return True diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 129c6506ac1..aebe1e0d88e 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -36,6 +36,15 @@ TAHOMA_COMPONENTS = [ 'sensor', 'cover' ] +TAHOMA_TYPES = { + 'rts:RollerShutterRTSComponent': 'cover', + 'rts:CurtainRTSComponent': 'cover', + 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', + 'io:RollerShutterVeluxIOComponent': 'cover', + 'io:WindowOpenerVeluxIOComponent': 'cover', + 'io:LightIOSystemSensor': 'sensor', +} + def setup(hass, config): """Activate Tahoma component.""" @@ -68,6 +77,8 @@ def setup(hass, config): if all(ext not in _device.type for ext in exclude): device_type = map_tahoma_device(_device) if device_type is None: + _LOGGER.warning('Unsupported type %s for Tahoma device %s', + _device.type, _device.label) continue hass.data[DOMAIN]['devices'][device_type].append(_device) @@ -78,12 +89,8 @@ def setup(hass, config): def map_tahoma_device(tahoma_device): - """Map tahoma classes to Home Assistant types.""" - if tahoma_device.type.lower().find("shutter") != -1: - return 'cover' - elif tahoma_device.type == 'io:LightIOSystemSensor': - return 'sensor' - return None + """Map Tahoma device types to Home Assistant components.""" + return TAHOMA_TYPES.get(tahoma_device.type) class TahomaDevice(Entity): diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index dc9389b1144..cb314c4a2b4 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -8,7 +8,6 @@ import asyncio import io from functools import partial import logging -import os import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth @@ -16,7 +15,6 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE) -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_COMMAND, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY, CONF_PLATFORM, CONF_TIMEOUT, HTTP_DIGEST_AUTHENTICATION) @@ -24,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==8.1.1'] +REQUIREMENTS = ['python-telegram-bot==9.0.0'] _LOGGER = logging.getLogger(__name__) @@ -216,9 +214,6 @@ def async_setup(hass, config): return False p_config = config[DOMAIN][0] - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) p_type = p_config.get(CONF_PLATFORM) @@ -301,7 +296,7 @@ def async_setup(hass, config): for service_notif, schema in SERVICE_MAP.items(): hass.services.async_register( DOMAIN, service_notif, async_send_telegram_message, - descriptions.get(service_notif), schema=schema) + schema=schema) return True diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index b2f5db88b5f..84d2d3f349d 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -6,14 +6,12 @@ at https://home-assistant.io/components/timer/ """ import asyncio import logging -import os from datetime import timedelta import voluptuous as vol import homeassistant.util.dt as dt_util 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.core import callback from homeassistant.helpers.entity import Entity @@ -29,6 +27,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DEFAULT_DURATION = 0 ATTR_DURATION = 'duration' +ATTR_REMAINING = 'remaining' CONF_DURATION = 'duration' STATUS_IDLE = 'idle' @@ -165,23 +164,18 @@ def async_setup(hass, config): if tasks: 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( DOMAIN, SERVICE_START, async_handler_service, - descriptions[SERVICE_START], SERVICE_SCHEMA_DURATION) + schema=SERVICE_SCHEMA_DURATION) hass.services.async_register( DOMAIN, SERVICE_PAUSE, async_handler_service, - descriptions[SERVICE_PAUSE], SERVICE_SCHEMA) + schema=SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_CANCEL, async_handler_service, - descriptions[SERVICE_CANCEL], SERVICE_SCHEMA) + schema=SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_FINISH, async_handler_service, - descriptions[SERVICE_FINISH], SERVICE_SCHEMA) + schema=SERVICE_SCHEMA) yield from component.async_add_entities(entities) return True @@ -227,6 +221,7 @@ class Timer(Entity): """Return the state attributes.""" return { ATTR_DURATION: str(self._duration), + ATTR_REMAINING: str(self._remaining) } @asyncio.coroutine diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index a7416bba117..d85b7d189c5 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -22,7 +22,6 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA) from homeassistant.components.media_player import DOMAIN as DOMAIN_MP -from homeassistant.config import load_yaml_config_file from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -96,10 +95,6 @@ def async_setup(hass, config): hass.http.register_view(TextToSpeechView(tts)) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, - os.path.join(os.path.dirname(__file__), 'services.yaml')) - @asyncio.coroutine def async_setup_platform(p_type, p_config, disc_info=None): """Set up a TTS platform.""" @@ -156,7 +151,7 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, "{}_{}".format(p_type, SERVICE_SAY), async_say_handle, - descriptions.get(SERVICE_SAY), schema=SCHEMA_SERVICE_SAY) + schema=SCHEMA_SERVICE_SAY) setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN)] @@ -171,7 +166,6 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_CLEAR_CACHE, async_clear_cache_handle, - descriptions.get(SERVICE_CLEAR_CACHE), schema=SCHEMA_SERVICE_CLEAR_CACHE) return True diff --git a/homeassistant/components/tts/baidu.py b/homeassistant/components/tts/baidu.py index 6f86a42bbc5..609f38454fe 100644 --- a/homeassistant/components/tts/baidu.py +++ b/homeassistant/components/tts/baidu.py @@ -1,5 +1,5 @@ """ -Support for the baidu speech service. +Support for Baidu speech service. For more details about this component, please refer to the documentation at https://home-assistant.io/components/tts.baidu/ @@ -8,22 +8,17 @@ https://home-assistant.io/components/tts.baidu/ import logging import voluptuous as vol +from homeassistant.components.tts import Provider, CONF_LANG, PLATFORM_SCHEMA from homeassistant.const import CONF_API_KEY -from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG import homeassistant.helpers.config_validation as cv - REQUIREMENTS = ["baidu-aip==1.6.6"] _LOGGER = logging.getLogger(__name__) - -SUPPORT_LANGUAGES = [ - 'zh', -] +SUPPORTED_LANGUAGES = ['zh'] DEFAULT_LANG = 'zh' - CONF_APP_ID = 'app_id' CONF_SECRET_KEY = 'secret_key' CONF_SPEED = 'speed' @@ -32,20 +27,39 @@ CONF_VOLUME = 'volume' CONF_PERSON = 'person' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), vol.Required(CONF_APP_ID): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_SECRET_KEY): cv.string, vol.Optional(CONF_SPEED, default=5): vol.All( - vol.Coerce(int), vol.Range(min=0, max=9)), + vol.Coerce(int), vol.Range(min=0, max=9) + ), vol.Optional(CONF_PITCH, default=5): vol.All( - vol.Coerce(int), vol.Range(min=0, max=9)), + vol.Coerce(int), vol.Range(min=0, max=9) + ), vol.Optional(CONF_VOLUME, default=5): vol.All( - vol.Coerce(int), vol.Range(min=0, max=15)), + vol.Coerce(int), vol.Range(min=0, max=15) + ), vol.Optional(CONF_PERSON, default=0): vol.All( - vol.Coerce(int), vol.Range(min=0, max=4)), + vol.Coerce(int), vol.Range(min=0, max=4) + ), }) +# Keys are options in the config file, and Values are options +# required by Baidu TTS API. +_OPTIONS = { + CONF_PERSON: 'per', + CONF_PITCH: 'pit', + CONF_SPEED: 'spd', + CONF_VOLUME: 'vol', +} +SUPPORTED_OPTIONS = [ + CONF_PERSON, + CONF_PITCH, + CONF_SPEED, + CONF_VOLUME, +] + def get_engine(hass, config): """Set up Baidu TTS component.""" @@ -66,14 +80,14 @@ class BaiduTTSProvider(Provider): 'appid': conf.get(CONF_APP_ID), 'apikey': conf.get(CONF_API_KEY), 'secretkey': conf.get(CONF_SECRET_KEY), - } + } self._speech_conf_data = { - 'spd': conf.get(CONF_SPEED), - 'pit': conf.get(CONF_PITCH), - 'vol': conf.get(CONF_VOLUME), - 'per': conf.get(CONF_PERSON), - } + _OPTIONS[CONF_PERSON]: conf.get(CONF_PERSON), + _OPTIONS[CONF_PITCH]: conf.get(CONF_PITCH), + _OPTIONS[CONF_SPEED]: conf.get(CONF_SPEED), + _OPTIONS[CONF_VOLUME]: conf.get(CONF_VOLUME), + } @property def default_language(self): @@ -82,8 +96,23 @@ class BaiduTTSProvider(Provider): @property def supported_languages(self): - """Return list of supported languages.""" - return SUPPORT_LANGUAGES + """Return a list of supported languages.""" + return SUPPORTED_LANGUAGES + + @property + def default_options(self): + """Return a dict including default options.""" + return { + CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]], + CONF_PITCH: self._speech_conf_data[_OPTIONS[CONF_PITCH]], + CONF_SPEED: self._speech_conf_data[_OPTIONS[CONF_SPEED]], + CONF_VOLUME: self._speech_conf_data[_OPTIONS[CONF_VOLUME]], + } + + @property + def supported_options(self): + """Return a list of supported options.""" + return SUPPORTED_OPTIONS def get_tts_audio(self, message, language, options=None): """Load TTS from BaiduTTS.""" @@ -92,17 +121,28 @@ class BaiduTTSProvider(Provider): self._app_data['appid'], self._app_data['apikey'], self._app_data['secretkey'] - ) + ) - result = aip_speech.synthesis( - message, language, 1, self._speech_conf_data) + if options is None: + result = aip_speech.synthesis( + message, language, 1, self._speech_conf_data + ) + else: + speech_data = self._speech_conf_data.copy() + for key, value in options.items(): + speech_data[_OPTIONS[key]] = value + + result = aip_speech.synthesis( + message, language, 1, speech_data + ) if isinstance(result, dict): _LOGGER.error( "Baidu TTS error-- err_no:%d; err_msg:%s; err_detail:%s", result['err_no'], result['err_msg'], - result['err_detail']) - return (None, None) + result['err_detail'] + ) + return None, None - return (self._codec, result) + return self._codec, result diff --git a/homeassistant/components/tts/marytts.py b/homeassistant/components/tts/marytts.py index d7db09856a6..072ea0e76e7 100644 --- a/homeassistant/components/tts/marytts.py +++ b/homeassistant/components/tts/marytts.py @@ -23,10 +23,6 @@ SUPPORT_LANGUAGES = [ 'de', 'en-GB', 'en-US', 'fr', 'it', 'lb', 'ru', 'sv', 'te', 'tr' ] -SUPPORT_VOICES = [ - 'cmu-slt-hsmm' -] - SUPPORT_CODEC = [ 'aiff', 'au', 'wav' ] @@ -44,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), - vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORT_VOICES), + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODEC) }) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index c67beee62dd..f7bf9774e42 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['distro==1.1.0'] +REQUIREMENTS = ['distro==1.2.0'] _LOGGER = logging.getLogger(__name__) @@ -97,9 +97,15 @@ def async_setup(hass, config): newest, releasenotes = result + # Skip on dev if newest is None or 'dev' in current_version: return + # Load data from supervisor on hass.io + if hass.components.hassio.is_hassio(): + newest = hass.components.hassio.get_homeassistant_version() + + # Validate version if StrictVersion(newest) > StrictVersion(current_version): _LOGGER.info("The latest available version is %s", newest) hass.states.async_set( @@ -131,6 +137,7 @@ def get_system_info(hass, include_components): 'timezone': dt_util.DEFAULT_TIME_ZONE.zone, 'version': current_version, 'virtualenv': os.environ.get('VIRTUAL_ENV') is not None, + 'hassio': hass.components.hassio.is_hassio(), } if include_components: diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 32839c08115..095e8bfb124 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -8,12 +8,10 @@ import asyncio from datetime import timedelta from functools import partial import logging -import os import voluptuous as vol from homeassistant.components import group -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) @@ -183,10 +181,6 @@ def async_setup(hass, 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 def async_handle_vacuum_service(service): """Map services to methods on VacuumDevice.""" @@ -210,7 +204,7 @@ def async_setup(hass, config): 'schema', VACUUM_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, service, async_handle_vacuum_service, - descriptions.get(service), schema=schema) + schema=schema) return True diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 9929ae46e09..54aea793a22 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -11,6 +11,7 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.components.mqtt import MqttAvailability from homeassistant.components.vacuum import ( DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -135,7 +136,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine @@ -187,6 +188,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) + availability_topic = config.get(mqtt.CONF_AVAILABILITY_TOPIC) + payload_available = config.get(mqtt.CONF_PAYLOAD_AVAILABLE) + payload_not_available = config.get(mqtt.CONF_PAYLOAD_NOT_AVAILABLE) + async_add_devices([ MqttVacuum( name, supported_features, qos, retain, command_topic, @@ -196,12 +201,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): charging_topic, charging_template, cleaning_topic, cleaning_template, docked_topic, docked_template, fan_speed_topic, fan_speed_template, set_fan_speed_topic, fan_speed_list, - send_command_topic + send_command_topic, availability_topic, payload_available, + payload_not_available ), ]) -class MqttVacuum(VacuumDevice): +class MqttVacuum(MqttAvailability, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" # pylint: disable=no-self-use @@ -213,8 +219,12 @@ class MqttVacuum(VacuumDevice): charging_topic, charging_template, cleaning_topic, cleaning_template, docked_topic, docked_template, fan_speed_topic, fan_speed_template, set_fan_speed_topic, fan_speed_list, - send_command_topic): + send_command_topic, availability_topic, payload_available, + payload_not_available): """Initialize the vacuum.""" + super().__init__(availability_topic, qos, payload_available, + payload_not_available) + self._name = name self._supported_features = supported_features self._qos = qos @@ -257,10 +267,9 @@ class MqttVacuum(VacuumDevice): @asyncio.coroutine 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 def message_received(topic, payload, qos): """Handle new MQTT message.""" diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index a2265706d87..294d4db9900 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/vacuum.xiaomi_miio/ import asyncio from functools import partial import logging -import os import voluptuous as vol @@ -16,12 +15,11 @@ from homeassistant.components.vacuum import ( SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VACUUM_SERVICE_SCHEMA, VacuumDevice) -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.2'] +REQUIREMENTS = ['python-miio==0.3.3'] _LOGGER = logging.getLogger(__name__) @@ -130,16 +128,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if update_tasks: 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 vacuum_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[vacuum_service].get( 'schema', VACUUM_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, vacuum_service, async_service_handler, - description=descriptions.get(vacuum_service), schema=schema) + schema=schema) class MiroboVacuum(VacuumDevice): diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 94f712896cc..b367752c247 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/verisure/ """ import logging import threading -import os.path from datetime import timedelta import voluptuous as vol @@ -15,7 +14,6 @@ from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery from homeassistant.util import Throttle -import homeassistant.config as conf_util import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['vsure==1.3.7', 'jsonpath==0.75'] @@ -78,9 +76,6 @@ def setup(hass, config): 'camera', 'binary_sensor'): discovery.load_platform(hass, component, DOMAIN, {}, config) - descriptions = conf_util.load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - def capture_smartcam(service): """Capture a new picture from a smartcam.""" device_id = service.data.get(ATTR_DEVICE_SERIAL) @@ -89,7 +84,6 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, - descriptions[DOMAIN][SERVICE_CAPTURE_SMARTCAM], schema=CAPTURE_IMAGE_SCHEMA) return True diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py index ab72aa989d7..7da0f3054f3 100644 --- a/homeassistant/components/wake_on_lan.py +++ b/homeassistant/components/wake_on_lan.py @@ -7,11 +7,9 @@ https://home-assistant.io/components/wake_on_lan/ import asyncio from functools import partial import logging -import os import voluptuous as vol -from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_MAC import homeassistant.helpers.config_validation as cv @@ -50,13 +48,8 @@ def async_setup(hass, config): yield from hass.async_add_job( partial(wol.send_magic_packet, mac_address)) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml')) - hass.services.async_register( DOMAIN, SERVICE_SEND_MAGIC_PACKET, send_magic_packet, - description=descriptions.get(DOMAIN).get(SERVICE_SEND_MAGIC_PACKET), schema=WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA) return True diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py new file mode 100644 index 00000000000..0566cc03662 --- /dev/null +++ b/homeassistant/components/weather/darksky.py @@ -0,0 +1,188 @@ +""" +Patform for retrieving meteorological data from Dark Sky. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/weather.darksky/ +""" +from datetime import datetime, timedelta +import logging + +from requests.exceptions import ( + ConnectionError as ConnectError, HTTPError, Timeout) +import voluptuous as vol + +from homeassistant.components.weather import ( + ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) +from homeassistant.const import ( + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, + TEMP_FAHRENHEIT) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['python-forecastio==1.3.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Powered by Dark Sky" + +ATTR_DAILY_FORECAST_SUMMARY = 'daily_forecast_summary' +ATTR_HOURLY_FORECAST_SUMMARY = 'hourly_forecast_summary' + +CONF_UNITS = 'units' + +DEFAULT_NAME = 'Dark Sky' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Dark Sky weather.""" + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + name = config.get(CONF_NAME) + + units = config.get(CONF_UNITS) + if not units: + units = 'si' if hass.config.units.is_metric else 'us' + + dark_sky = DarkSkyData( + config.get(CONF_API_KEY), latitude, longitude, units) + + add_devices([DarkSkyWeather(name, dark_sky)], True) + + +class DarkSkyWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, name, dark_sky): + """Initialize Dark Sky weather.""" + self._name = name + self._dark_sky = dark_sky + + self._ds_data = None + self._ds_currently = None + self._ds_hourly = None + self._ds_daily = None + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def temperature(self): + """Return the temperature.""" + return self._ds_currently.get('temperature') + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \ + else TEMP_CELSIUS + + @property + def humidity(self): + """Return the humidity.""" + return self._ds_currently.get('humidity') * 100.0 + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._ds_currently.get('windSpeed') + + @property + def pressure(self): + """Return the pressure.""" + return self._ds_currently.get('pressure') + + @property + def condition(self): + """Return the weather condition.""" + return self._ds_currently.get('summary') + + @property + def forecast(self): + """Return the forecast array.""" + return [{ + ATTR_FORECAST_TIME: + datetime.fromtimestamp(entry.d.get('time')).isoformat(), + ATTR_FORECAST_TEMP: entry.d.get('temperature')} + for entry in self._ds_hourly.data] + + @property + def hourly_forecast_summary(self): + """Return a summary of the hourly forecast.""" + return self._ds_hourly.summary + + @property + def daily_forecast_summary(self): + """Return a summary of the daily forecast.""" + return self._ds_daily.summary + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = { + ATTR_DAILY_FORECAST_SUMMARY: self.daily_forecast_summary, + ATTR_HOURLY_FORECAST_SUMMARY: self.hourly_forecast_summary + } + return attrs + + def update(self): + """Get the latest data from Dark Sky.""" + self._dark_sky.update() + + self._ds_data = self._dark_sky.data + self._ds_currently = self._dark_sky.currently.d + self._ds_hourly = self._dark_sky.hourly + self._ds_daily = self._dark_sky.daily + + +class DarkSkyData(object): + """Get the latest data from Dark Sky.""" + + def __init__(self, api_key, latitude, longitude, units): + """Initialize the data object.""" + self._api_key = api_key + self.latitude = latitude + self.longitude = longitude + self.requested_units = units + + self.data = None + self.currently = None + self.hourly = None + self.daily = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Dark Sky.""" + import forecastio + + try: + self.data = forecastio.load_forecast( + self._api_key, self.latitude, self.longitude, + units=self.requested_units) + self.currently = self.data.currently() + self.hourly = self.data.hourly() + self.daily = self.data.daily() + except (ConnectError, HTTPError, Timeout, ValueError) as error: + _LOGGER.error("Unable to connect to Dark Sky. %s", error) + self.data = None + + @property + def units(self): + """Get the unit system of returned data.""" + return self.data.json.get('flags').get('units') diff --git a/homeassistant/components/weather/metoffice.py b/homeassistant/components/weather/metoffice.py index 50bbb84faa7..d43d1d3c996 100644 --- a/homeassistant/components/weather/metoffice.py +++ b/homeassistant/components/weather/metoffice.py @@ -8,27 +8,34 @@ import logging import voluptuous as vol -from homeassistant.components.weather import WeatherEntity, PLATFORM_SCHEMA +from homeassistant.components.sensor.metoffice import ( + CONDITION_CLASSES, CONF_ATTRIBUTION, MetOfficeCurrentData) +from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.const import ( - CONF_NAME, TEMP_CELSIUS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE) + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv -# Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.metoffice import \ - MetOfficeCurrentData, CONF_ATTRIBUTION, CONDITION_CLASSES - -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['datapoint==0.4.3'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Met Office" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coordinates', + 'Latitude and longitude must exist together'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates', + 'Latitude and longitude must exist together'): cv.longitude, }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Met Office weather platform.""" import datapoint as dp + + name = config.get(CONF_NAME) datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -36,36 +43,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return try: - site = datapoint.get_nearest_site(latitude=latitude, - longitude=longitude) + site = datapoint.get_nearest_site( + latitude=latitude, longitude=longitude) except dp.exceptions.APIException as err: _LOGGER.error("Received error from Met Office Datapoint: %s", err) - return False + return if not site: _LOGGER.error("Unable to get nearest Met Office forecast site") - return False + return - # Get data data = MetOfficeCurrentData(hass, datapoint, site) try: data.update() except (ValueError, dp.exceptions.APIException) as err: _LOGGER.error("Received error from Met Office Datapoint: %s", err) - return False - add_devices([MetOfficeWeather(site, data, config.get(CONF_NAME))], - True) - return True + return + + add_devices([MetOfficeWeather(site, data, name)], True) class MetOfficeWeather(WeatherEntity): """Implementation of a Met Office weather condition.""" - def __init__(self, site, data, config): + def __init__(self, site, data, name): """Initialise the platform with a data instance and site.""" + self._name = name self.data = data self.site = site @@ -76,7 +82,7 @@ class MetOfficeWeather(WeatherEntity): @property def name(self): """Return the name of the sensor.""" - return 'Met Office ({})'.format(self.site.name) + return '{} {}'.format(self._name, self.site.name) @property def condition(self): @@ -84,8 +90,6 @@ class MetOfficeWeather(WeatherEntity): return [k for k, v in CONDITION_CLASSES.items() if self.data.data.weather.value in v][0] - # Now implement the WeatherEntity interface - @property def temperature(self): """Return the platform temperature.""" diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index a50e160cddb..1ff5eeaa535 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -4,27 +4,29 @@ Support for the OpenWeatherMap (OWM) service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/weather.openweathermap/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) -from homeassistant.const import (CONF_API_KEY, CONF_NAME, CONF_LATITUDE, - CONF_LONGITUDE, STATE_UNKNOWN, TEMP_CELSIUS) + ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) +from homeassistant.const import ( + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, STATE_UNKNOWN, + TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.7.1'] +REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'OpenWeatherMap' ATTRIBUTION = 'Data provided by OpenWeatherMap' -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +DEFAULT_NAME = 'OpenWeatherMap' + MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) CONDITION_CLASSES = { 'cloudy': [804], diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index a043f3c2212..20ed97ec249 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -15,7 +15,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN) -REQUIREMENTS = ["yahooweather==0.9"] +REQUIREMENTS = ["yahooweather==0.10"] _LOGGER = logging.getLogger(__name__) @@ -125,27 +125,28 @@ class YahooWeatherWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self._data.yahoo.Atmosphere['pressure'] + return round(float(self._data.yahoo.Atmosphere['pressure'])/33.8637526, + 2) @property def humidity(self): """Return the humidity.""" - return self._data.yahoo.Atmosphere['humidity'] + return int(self._data.yahoo.Atmosphere['humidity']) @property def visibility(self): """Return the visibility.""" - return self._data.yahoo.Atmosphere['visibility'] + return round(float(self._data.yahoo.Atmosphere['visibility'])/1.61, 2) @property def wind_speed(self): """Return the wind speed.""" - return self._data.yahoo.Wind['speed'] + return round(float(self._data.yahoo.Wind['speed'])/1.61, 2) @property def wind_bearing(self): """Return the wind direction.""" - return self._data.yahoo.Wind['direction'] + return int(self._data.yahoo.Wind['direction']) @property def attribution(self): diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index a1fb0ca9cac..a4bfc46bf83 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -21,6 +21,7 @@ from homeassistant.components import frontend from homeassistant.core import callback from homeassistant.remote import JSONEncoder from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.auth import validate_password from homeassistant.components.http.const import KEY_AUTHENTICATED @@ -263,7 +264,7 @@ class ActiveConnection: def handle(self): """Handle the websocket connection.""" request = self.request - wsock = self.wsock = web.WebSocketResponse() + wsock = self.wsock = web.WebSocketResponse(heartbeat=55) yield from wsock.prepare(request) self.debug("Connected") @@ -436,7 +437,7 @@ class ActiveConnection: def handle_call_service(self, msg): """Handle call service command. - This is a coroutine. + Async friendly. """ msg = CALL_SERVICE_MESSAGE_SCHEMA(msg) @@ -466,8 +467,13 @@ class ActiveConnection: """ msg = GET_SERVICES_MESSAGE_SCHEMA(msg) - self.to_write.put_nowait(result_message( - msg['id'], self.hass.services.async_services())) + @asyncio.coroutine + def get_services_helper(msg): + """Get available services and fire complete message.""" + descriptions = yield from async_get_all_descriptions(self.hass) + self.send_message_outside(result_message(msg['id'], descriptions)) + + self.hass.async_add_job(get_services_helper(msg)) def handle_get_config(self, msg): """Handle get config command. diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 0592ad4c124..aaeccaf6eba 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -14,7 +14,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['pywemo==0.4.20'] +REQUIREMENTS = ['pywemo==0.4.25'] DOMAIN = 'wemo' diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 18e14b2e912..c903b5a0ddf 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -25,7 +25,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent import homeassistant.helpers.config_validation as cv -from homeassistant.config import load_yaml_config_file from homeassistant.util.json import load_json, save_json REQUIREMENTS = ['python-wink==1.7.1', 'pubnubsub-handler==1.0.2'] @@ -232,9 +231,6 @@ def setup(hass, config): import pywink from pubnubsubhandler import PubNubSubscriptionHandler - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { 'unique_ids': [], @@ -374,8 +370,7 @@ def setup(hass, config): time.sleep(1) entity.schedule_update_ha_state(True) - hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update, - descriptions.get(SERVICE_REFRESH_STATES)) + hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update) def pull_new_devices(call): """Pull new devices added to users Wink account since startup.""" @@ -383,8 +378,7 @@ def setup(hass, config): for _component in WINK_COMPONENTS: discovery.load_platform(hass, _component, DOMAIN, {}, config) - hass.services.register(DOMAIN, SERVICE_ADD_NEW_DEVICES, pull_new_devices, - descriptions.get(SERVICE_ADD_NEW_DEVICES)) + hass.services.register(DOMAIN, SERVICE_ADD_NEW_DEVICES, pull_new_devices) def set_pairing_mode(call): """Put the hub in provided pairing mode.""" @@ -412,7 +406,6 @@ def setup(hass, config): found_device.wink.set_name(name) hass.services.register(DOMAIN, SERVICE_RENAME_DEVICE, rename_device, - descriptions.get(SERVICE_RENAME_DEVICE), schema=RENAME_DEVICE_SCHEMA) def delete_device(call): @@ -430,7 +423,6 @@ def setup(hass, config): found_device.wink.remove_device() hass.services.register(DOMAIN, SERVICE_DELETE_DEVICE, delete_device, - descriptions.get(SERVICE_DELETE_DEVICE), schema=DELETE_DEVICE_SCHEMA) hubs = pywink.get_hubs() @@ -441,7 +433,6 @@ def setup(hass, config): if WINK_HUBS: hass.services.register( DOMAIN, SERVICE_SET_PAIRING_MODE, set_pairing_mode, - descriptions.get(SERVICE_SET_PAIRING_MODE), schema=SET_PAIRING_MODE_SCHEMA) def service_handle(service): @@ -508,44 +499,36 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_SET_AUTO_SHUTOFF, service_handle, - descriptions.get(SERVICE_SET_AUTO_SHUTOFF), schema=SET_AUTO_SHUTOFF_SCHEMA) hass.services.register(DOMAIN, SERVICE_ENABLE_SIREN, service_handle, - descriptions.get(SERVICE_ENABLE_SIREN), schema=ENABLED_SIREN_SCHEMA) if has_dome_or_wink_siren: hass.services.register(DOMAIN, SERVICE_SET_SIREN_TONE, service_handle, - descriptions.get(SERVICE_SET_SIREN_TONE), schema=SET_SIREN_TONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_ENABLE_CHIME, service_handle, - descriptions.get(SERVICE_ENABLE_CHIME), schema=SET_CHIME_MODE_SCHEMA) hass.services.register(DOMAIN, SERVICE_SET_SIREN_VOLUME, service_handle, - descriptions.get(SERVICE_SET_SIREN_VOLUME), schema=SET_VOLUME_SCHEMA) hass.services.register(DOMAIN, SERVICE_SET_CHIME_VOLUME, service_handle, - descriptions.get(SERVICE_SET_CHIME_VOLUME), schema=SET_VOLUME_SCHEMA) hass.services.register(DOMAIN, SERVICE_SIREN_STROBE_ENABLED, service_handle, - descriptions.get(SERVICE_SIREN_STROBE_ENABLED), schema=SET_STROBE_ENABLED_SCHEMA) hass.services.register(DOMAIN, SERVICE_CHIME_STROBE_ENABLED, service_handle, - descriptions.get(SERVICE_CHIME_STROBE_ENABLED), schema=SET_STROBE_ENABLED_SCHEMA) component.add_entities(sirens) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 678ead981c1..e059d3d8772 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -9,7 +9,7 @@ from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import (ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, CONF_MAC, CONF_HOST, CONF_PORT) -REQUIREMENTS = ['PyXiaomiGateway==0.6.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.7.0'] ATTR_GW_MAC = 'gw_mac' ATTR_RINGTONE_ID = 'ringtone_id' @@ -105,8 +105,8 @@ def setup(hass, config): discovery.listen(hass, SERVICE_XIAOMI_GW, xiaomi_gw_discovered) - from PyXiaomiGateway import PyXiaomiGateway - xiaomi = hass.data[PY_XIAOMI_GATEWAY] = PyXiaomiGateway( + from xiaomi_gateway import XiaomiGatewayDiscovery + xiaomi = hass.data[PY_XIAOMI_GATEWAY] = XiaomiGatewayDiscovery( hass.add_job, gateways, interface) _LOGGER.debug("Expecting %s gateways", len(gateways)) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 3cd9446dc4f..a361fca9832 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -41,17 +41,6 @@ CONFIG_SCHEMA = vol.Schema({ ATTR_DURATION = 'duration' SERVICE_PERMIT = 'permit' -SERVICE_DESCRIPTIONS = { - SERVICE_PERMIT: { - "description": "Allow nodes to join the ZigBee network", - "fields": { - ATTR_DURATION: { - "description": "Time to permit joins, in seconds", - "example": "60", - }, - }, - }, -} SERVICE_SCHEMAS = { SERVICE_PERMIT: vol.Schema({ vol.Optional(ATTR_DURATION, default=60): @@ -103,8 +92,7 @@ def async_setup(hass, config): yield from APPLICATION_CONTROLLER.permit(duration) hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit, - SERVICE_DESCRIPTIONS[SERVICE_PERMIT], - SERVICE_SCHEMAS[SERVICE_PERMIT]) + schema=SERVICE_SCHEMAS[SERVICE_PERMIT]) return True diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml new file mode 100644 index 00000000000..a9ad0e7a1ca --- /dev/null +++ b/homeassistant/components/zha/services.yaml @@ -0,0 +1,8 @@ +# Describes the format for available zha services + +permit: + description: Allow nodes to join the ZigBee network. + fields: + duration: + description: Time to permit joins, in seconds + example: 60 diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 95b0971373d..3a84e963841 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -288,12 +288,15 @@ class ZigBeeDigitalIn(Entity): """ if not frame_is_relevant(self, frame): return - sample = frame['samples'].pop() + sample = next(iter(frame['samples'])) pin_name = DIGITAL_PINS[self._config.pin] if pin_name not in sample: # Doesn't contain information about our pin return - self._state = self._config.state2bool[sample[pin_name]] + # Set state to the value of sample, respecting any inversion + # logic from the on_state config variable. + self._state = self._config.state2bool[ + self._config.bool2state[sample[pin_name]]] self.schedule_update_ha_state() async_dispatcher_connect( diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py old mode 100755 new mode 100644 index 2faeccde154..cacdb4873e6 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/zwave/ import asyncio import copy import logging -import os.path import time from pprint import pprint @@ -23,7 +22,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import track_time_change from homeassistant.util import convert, slugify -import homeassistant.config as conf_util import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) @@ -249,9 +247,6 @@ def setup(hass, config): Will automatically load components to support devices found on the network. """ - descriptions = conf_util.load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - from pydispatch import dispatcher # pylint: disable=import-error from openzwave.option import ZWaveOption @@ -627,99 +622,65 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_network) # Register node services for Z-Wave network - hass.services.register(DOMAIN, const.SERVICE_ADD_NODE, add_node, - descriptions[const.SERVICE_ADD_NODE]) + hass.services.register(DOMAIN, const.SERVICE_ADD_NODE, add_node) hass.services.register(DOMAIN, const.SERVICE_ADD_NODE_SECURE, - add_node_secure, - descriptions[const.SERVICE_ADD_NODE_SECURE]) - hass.services.register(DOMAIN, const.SERVICE_REMOVE_NODE, remove_node, - descriptions[const.SERVICE_REMOVE_NODE]) + add_node_secure) + hass.services.register(DOMAIN, const.SERVICE_REMOVE_NODE, remove_node) hass.services.register(DOMAIN, const.SERVICE_CANCEL_COMMAND, - cancel_command, - descriptions[const.SERVICE_CANCEL_COMMAND]) + cancel_command) hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, - heal_network, - descriptions[const.SERVICE_HEAL_NETWORK]) - hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset, - descriptions[const.SERVICE_SOFT_RESET]) + heal_network) + hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, - test_network, - descriptions[const.SERVICE_TEST_NETWORK]) + test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, - stop_network, - descriptions[const.SERVICE_STOP_NETWORK]) + stop_network) hass.services.register(DOMAIN, const.SERVICE_START_NETWORK, - start_zwave, - descriptions[const.SERVICE_START_NETWORK]) + start_zwave) hass.services.register(DOMAIN, const.SERVICE_RENAME_NODE, rename_node, - descriptions[const.SERVICE_RENAME_NODE], schema=RENAME_NODE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_RENAME_VALUE, rename_value, - descriptions[const.SERVICE_RENAME_VALUE], schema=RENAME_VALUE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, set_config_parameter, - descriptions[ - const.SERVICE_SET_CONFIG_PARAMETER], schema=SET_CONFIG_PARAMETER_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_PRINT_CONFIG_PARAMETER, print_config_parameter, - descriptions[ - const.SERVICE_PRINT_CONFIG_PARAMETER], schema=PRINT_CONFIG_PARAMETER_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_REMOVE_FAILED_NODE, remove_failed_node, - descriptions[const.SERVICE_REMOVE_FAILED_NODE], schema=NODE_SERVICE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_REPLACE_FAILED_NODE, replace_failed_node, - descriptions[const.SERVICE_REPLACE_FAILED_NODE], schema=NODE_SERVICE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_CHANGE_ASSOCIATION, change_association, - descriptions[ - const.SERVICE_CHANGE_ASSOCIATION], schema=CHANGE_ASSOCIATION_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_SET_WAKEUP, set_wakeup, - descriptions[ - const.SERVICE_SET_WAKEUP], schema=SET_WAKEUP_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_PRINT_NODE, print_node, - descriptions[ - const.SERVICE_PRINT_NODE], schema=NODE_SERVICE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_REFRESH_ENTITY, async_refresh_entity, - descriptions[ - const.SERVICE_REFRESH_ENTITY], schema=REFRESH_ENTITY_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_REFRESH_NODE, refresh_node, - descriptions[ - const.SERVICE_REFRESH_NODE], schema=NODE_SERVICE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_RESET_NODE_METERS, reset_node_meters, - descriptions[ - const.SERVICE_RESET_NODE_METERS], schema=RESET_NODE_METERS_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_SET_POLL_INTENSITY, set_poll_intensity, - descriptions[const.SERVICE_SET_POLL_INTENSITY], schema=SET_POLL_INTENSITY_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_HEAL_NODE, heal_node, - descriptions[ - const.SERVICE_HEAL_NODE], schema=HEAL_NODE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_TEST_NODE, test_node, - descriptions[ - const.SERVICE_TEST_NODE], schema=TEST_NODE_SCHEMA) # Setup autoheal diff --git a/homeassistant/config.py b/homeassistant/config.py index fee7572a2c2..3f4c4c174d7 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -33,6 +33,8 @@ from homeassistant.helpers import config_per_platform, extract_domain_configs _LOGGER = logging.getLogger(__name__) DATA_PERSISTENT_ERRORS = 'bootstrap_persistent_errors' +RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") +RE_ASCII = re.compile(r"\033\[[^m]*m") HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)' YAML_CONFIG_FILE = 'configuration.yaml' VERSION_FILE = '.HA_VERSION' @@ -655,15 +657,19 @@ def async_check_ha_config_file(hass): proc = yield from asyncio.create_subprocess_exec( sys.executable, '-m', 'homeassistant', '--script', 'check_config', '--config', hass.config.config_dir, - stdout=asyncio.subprocess.PIPE, loop=hass.loop) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, loop=hass.loop) + # Wait for the subprocess exit - stdout_data, dummy = yield from proc.communicate() - result = yield from proc.wait() + log, _ = yield from proc.communicate() + exit_code = yield from proc.wait() - if not result: - return None + # Convert to ASCII + log = RE_ASCII.sub('', log.decode()) - return re.sub(r'\033\[[^m]*m', '', str(stdout_data, 'utf-8')) + if exit_code != 0 or RE_YAML_ERROR.search(log): + return log + return None @callback diff --git a/homeassistant/const.py b/homeassistant/const.py index b4fc8061d87..be085bd75f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 60 -PATCH_VERSION = '1' +MINOR_VERSION = 61 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) diff --git a/homeassistant/core.py b/homeassistant/core.py index 30be92af153..18cf40d3854 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -754,25 +754,15 @@ class StateMachine(object): class Service(object): """Representation of a callable service.""" - __slots__ = ['func', 'description', 'fields', 'schema', - 'is_callback', 'is_coroutinefunction'] + __slots__ = ['func', 'schema', 'is_callback', 'is_coroutinefunction'] - def __init__(self, func, description, fields, schema): + def __init__(self, func, schema): """Initialize a service.""" self.func = func - self.description = description or '' - self.fields = fields or {} self.schema = schema self.is_callback = is_callback(func) self.is_coroutinefunction = asyncio.iscoroutinefunction(func) - def as_dict(self): - """Return dictionary representation of this service.""" - return { - 'description': self.description, - 'fields': self.fields, - } - class ServiceCall(object): """Representation of a call to a service.""" @@ -826,8 +816,7 @@ class ServiceRegistry(object): This method must be run in the event loop. """ - return {domain: {key: value.as_dict() for key, value - in self._services[domain].items()} + return {domain: self._services[domain].copy() for domain in self._services} def has_service(self, domain, service): @@ -837,40 +826,29 @@ class ServiceRegistry(object): """ return service.lower() in self._services.get(domain.lower(), []) - def register(self, domain, service, service_func, description=None, - schema=None): + def register(self, domain, service, service_func, schema=None): """ Register a service. - Description is a dict containing key 'description' to describe - the service and a key 'fields' to describe the fields. - Schema is called to coerce and validate the service data. """ run_callback_threadsafe( self._hass.loop, - self.async_register, domain, service, service_func, description, - schema + self.async_register, domain, service, service_func, schema ).result() @callback - def async_register(self, domain, service, service_func, description=None, - schema=None): + def async_register(self, domain, service, service_func, schema=None): """ Register a service. - Description is a dict containing key 'description' to describe - the service and a key 'fields' to describe the fields. - Schema is called to coerce and validate the service data. This method must be run in the event loop. """ domain = domain.lower() service = service.lower() - description = description or {} - service_obj = Service(service_func, description.get('description'), - description.get('fields', {}), schema) + service_obj = Service(service_func, schema) if domain in self._services: self._services[domain][service] = service_obj @@ -1024,19 +1002,22 @@ class ServiceRegistry(object): service_call = ServiceCall(domain, service, service_data, call_id) - if service_handler.is_callback: - service_handler.func(service_call) - fire_service_executed() - elif service_handler.is_coroutinefunction: - yield from service_handler.func(service_call) - fire_service_executed() - else: - def execute_service(): - """Execute a service and fires a SERVICE_EXECUTED event.""" + try: + if service_handler.is_callback: service_handler.func(service_call) fire_service_executed() + elif service_handler.is_coroutinefunction: + yield from service_handler.func(service_call) + fire_service_executed() + else: + def execute_service(): + """Execute a service and fires a SERVICE_EXECUTED event.""" + service_handler.func(service_call) + fire_service_executed() - self._hass.async_add_job(execute_service) + yield from self._hass.async_add_job(execute_service) + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error executing service %s', service_call) class Config(object): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e5d0a34f76e..afb4483647d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -5,6 +5,8 @@ import os import re from urllib.parse import urlparse from socket import _GLOBAL_DEFAULT_TIMEOUT +import logging +import inspect from typing import Any, Union, TypeVar, Callable, Sequence, Dict @@ -430,6 +432,22 @@ def ensure_list_csv(value: Any) -> Sequence: return ensure_list(value) +def deprecated(key): + """Log key as deprecated.""" + module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ + + def validator(config): + """Check if key is in config and log warning.""" + if key in config: + logging.getLogger(module_name).warning( + "The '%s' option (with value '%s') is deprecated, please " + "remove it from your configuration.", key, config[key]) + + return config + + return validator + + # Validator helpers def key_dependency(key, dependency): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 78db0890ab1..61569b7cf53 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -35,10 +35,10 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], current_ids, hass ).result() - name = (name or DEVICE_DEFAULT_NAME).lower() + name = (slugify(name) or slugify(DEVICE_DEFAULT_NAME)).lower() return ensure_unique_string( - entity_id_format.format(slugify(name)), current_ids) + entity_id_format.format(name), current_ids) @callback diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index c5aad3ababc..6268b3cb9f7 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -41,7 +41,7 @@ def async_handle(hass, platform, intent_type, slots=None, text_input=None): handler = hass.data.get(DATA_KEY, {}).get(intent_type) if handler is None: - raise UnknownIntent() + raise UnknownIntent('Unknown intent {}'.format(intent_type)) intent = Intent(hass, platform, intent_type, slots or {}, text_input) @@ -50,9 +50,11 @@ def async_handle(hass, platform, intent_type, slots=None, text_input=None): result = yield from handler.async_handle(intent) return result except vol.Invalid as err: - raise InvalidSlotInfo from err + raise InvalidSlotInfo( + 'Received invalid slot info for {}'.format(intent_type)) from err except Exception as err: - raise IntentHandleError from err + raise IntentHandleError( + 'Error handling {}'.format(intent_type)) from err class IntentError(HomeAssistantError): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 98cd704144e..83df8b48ab6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,13 +3,15 @@ import asyncio import logging # pylint: disable=unused-import from typing import Optional # NOQA +from os import path import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant # NOQA +import homeassistant.core as ha from homeassistant.exceptions import TemplateError from homeassistant.loader import get_component, bind_hass +from homeassistant.util.yaml import load_yaml import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe @@ -21,6 +23,8 @@ CONF_SERVICE_DATA_TEMPLATE = 'data_template' _LOGGER = logging.getLogger(__name__) +SERVICE_DESCRIPTION_CACHE = 'service_description_cache' + @bind_hass def call_from_config(hass, config, blocking=False, variables=None, @@ -112,3 +116,76 @@ def extract_entity_ids(hass, service_call, expand_group=True): return [service_ent_id] return service_ent_id + + +@asyncio.coroutine +@bind_hass +def async_get_all_descriptions(hass): + """Return descriptions (i.e. user documentation) for all service calls.""" + if SERVICE_DESCRIPTION_CACHE not in hass.data: + hass.data[SERVICE_DESCRIPTION_CACHE] = {} + description_cache = hass.data[SERVICE_DESCRIPTION_CACHE] + + format_cache_key = '{}.{}'.format + + def domain_yaml_file(domain): + """Return the services.yaml location for a domain.""" + if domain == ha.DOMAIN: + import homeassistant.components as components + component_path = path.dirname(components.__file__) + else: + component_path = path.dirname(get_component(domain).__file__) + return path.join(component_path, 'services.yaml') + + def load_services_file(yaml_file): + """Load and cache a services.yaml file.""" + try: + yaml_cache[yaml_file] = load_yaml(yaml_file) + except FileNotFoundError: + pass + + services = hass.services.async_services() + + # Load missing files + yaml_cache = {} + loading_tasks = [] + for domain in services: + yaml_file = domain_yaml_file(domain) + + for service in services[domain]: + if format_cache_key(domain, service) not in description_cache: + if yaml_file not in yaml_cache: + yaml_cache[yaml_file] = {} + task = hass.async_add_job(load_services_file, yaml_file) + loading_tasks.append(task) + + if loading_tasks: + yield from asyncio.wait(loading_tasks, loop=hass.loop) + + # Build response + catch_all_yaml_file = domain_yaml_file(ha.DOMAIN) + descriptions = {} + for domain in services: + descriptions[domain] = {} + yaml_file = domain_yaml_file(domain) + + for service in services[domain]: + cache_key = format_cache_key(domain, service) + description = description_cache.get(cache_key) + + # Cache missing descriptions + if description is None: + if yaml_file == catch_all_yaml_file: + yaml_services = yaml_cache[yaml_file].get(domain, {}) + else: + yaml_services = yaml_cache[yaml_file] + yaml_description = yaml_services.get(service, {}) + + description = description_cache[cache_key] = { + 'description': yaml_description.get('description', ''), + 'fields': yaml_description.get('fields', {}) + } + + descriptions[domain][service] = description + + return descriptions diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3080160dfce..243c6d418df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,8 +5,8 @@ pip>=8.0.3 jinja2>=2.9.6 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.3.6 -yarl==0.16.0 +aiohttp==2.3.7 +yarl==0.18.0 async_timeout==2.0.0 chardet==3.0.4 astral==1.4 diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index e9eedeaa300..922bd9c7fe1 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -1,11 +1,11 @@ -"""Script to get, set, and delete secrets stored in the keyring.""" -import os +"""Script to get, set and delete secrets stored in the keyring.""" import argparse import getpass +import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring>=9.3,<10.0'] +REQUIREMENTS = ['keyring==10.3.2', 'keyrings.alt==2.3'] def run(args): @@ -39,8 +39,8 @@ def run(args): return 1 if args.action == 'set': - the_secret = getpass.getpass('Please enter the secret for {}: ' - .format(args.name)) + the_secret = getpass.getpass( + 'Please enter the secret for {}: '.format(args.name)) keyring.set_password(_SECRET_NAMESPACE, args.name, the_secret) print('Secret {} set successfully'.format(args.name)) elif args.action == 'get': diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 05a8ee1e2f1..12a39e80517 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -181,9 +181,15 @@ def _async_setup_component(hass: core.HomeAssistant, start = timer() _LOGGER.info("Setting up %s", domain) - warn_task = hass.loop.call_later( - SLOW_SETUP_WARNING, _LOGGER.warning, - "Setup of %s is taking over %s seconds.", domain, SLOW_SETUP_WARNING) + + if hasattr(component, 'PLATFORM_SCHEMA'): + # Entity components have their own warning + warn_task = None + else: + warn_task = hass.loop.call_later( + SLOW_SETUP_WARNING, _LOGGER.warning, + "Setup of %s is taking over %s seconds.", + domain, SLOW_SETUP_WARNING) try: if async_comp: @@ -197,7 +203,8 @@ def _async_setup_component(hass: core.HomeAssistant, return False finally: end = timer() - warn_task.cancel() + if warn_task: + warn_task.cancel() _LOGGER.info("Setup of domain %s took %.1f seconds.", domain, end - start) if result is False: diff --git a/requirements_all.txt b/requirements_all.txt index 02a53b9c26e..820d3894a39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,8 +6,8 @@ pip>=8.0.3 jinja2>=2.9.6 voluptuous==0.10.5 typing>=3,<4 -aiohttp==2.3.6 -yarl==0.16.0 +aiohttp==2.3.7 +yarl==0.18.0 async_timeout==2.0.0 chardet==3.0.4 astral==1.4 @@ -20,7 +20,7 @@ certifi>=2017.4.17 # Adafruit_BBIO==1.0.0 # homeassistant.components.doorbird -DoorBirdPy==0.1.0 +DoorBirdPy==0.1.2 # homeassistant.components.isy994 PyISY==1.1.0 @@ -35,7 +35,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.6.0 +PyXiaomiGateway==0.7.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 @@ -66,7 +66,7 @@ aiodns==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.5.3 +aiohttp_cors==0.6.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 @@ -81,10 +81,10 @@ aiolifx_effects==0.1.2 aiopvapi==1.5.4 # homeassistant.components.alarmdecoder -alarmdecoder==0.12.3 +alarmdecoder==1.13.2 # homeassistant.components.sensor.alpha_vantage -alpha_vantage==1.3.6 +alpha_vantage==1.8.0 # homeassistant.components.amcrest amcrest==1.2.1 @@ -166,6 +166,9 @@ caldav==0.5.0 # homeassistant.components.notify.ciscospark ciscosparkapi==0.4.2 +# homeassistant.components.coinbase +coinbase==2.0.6 + # homeassistant.components.sensor.coinmarketcap coinmarketcap==4.1.1 @@ -174,7 +177,7 @@ colorlog==3.0.1 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 -concord232==0.14 +concord232==0.15 # homeassistant.scripts.credstash # credstash==1.14.0 @@ -208,11 +211,14 @@ denonavr==0.5.5 # homeassistant.components.media_player.directv directpy==0.2 +# homeassistant.components.sensor.discogs +discogs_client==2.2.1 + # homeassistant.components.notify.discord discord.py==0.16.12 # homeassistant.components.updater -distro==1.1.0 +distro==1.2.0 # homeassistant.components.switch.digitalloggers dlipower==0.7.165 @@ -286,7 +292,7 @@ fritzhome==1.0.3 fsapi==0.0.7 # homeassistant.components.conversation -fuzzywuzzy==0.15.1 +fuzzywuzzy==0.16.0 # homeassistant.components.tts.google gTTS-token==1.1.1 @@ -312,6 +318,9 @@ googlemaps==2.5.1 # homeassistant.components.sensor.gpsd gps3==0.33.3 +# homeassistant.components.light.greenwave +greenwavereality==0.2.9 + # homeassistant.components.media_player.gstreamer gstreamer-player==1.1.0 @@ -340,7 +349,7 @@ hipnotify==1.0.8 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171216.0 +home-assistant-frontend==20180112.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -386,12 +395,15 @@ https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # homeassistant.components.sensor.htu21d # i2csense==0.0.4 +# homeassistant.components.light.iglo +iglo==1.1.3 + # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb influxdb==4.1.1 # homeassistant.components.insteon_local -insteonlocal==0.52 +insteonlocal==0.53 # homeassistant.components.insteon_plm insteonplm==0.7.5 @@ -407,7 +419,10 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.5 # homeassistant.scripts.keyring -keyring>=9.3,<10.0 +keyring==10.3.2 + +# homeassistant.scripts.keyring +keyrings.alt==2.3 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http @@ -429,7 +444,7 @@ libsoundtouch==0.7.2 liffylights==0.9.4 # homeassistant.components.light.osramlightify -lightify==1.0.6 +lightify==1.0.6.1 # homeassistant.components.light.limitlessled limitlessled==1.0.8 @@ -445,7 +460,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.sensor.luftdaten -luftdaten==0.1.1 +luftdaten==0.1.3 # homeassistant.components.sensor.lyft lyft_rides==0.2 @@ -464,7 +479,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.1.16 +miflora==0.2.0 # homeassistant.components.upnp miniupnpc==2.0.2 @@ -494,9 +509,12 @@ neurio==0.3.1 # homeassistant.components.sensor.nederlandse_spoorwegen nsapi==2.7.4 +# homeassistant.components.nuheat +nuheat==0.3.0 + # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.13.3 +numpy==1.14.0 # homeassistant.components.google oauth2client==4.0.0 @@ -556,7 +574,7 @@ pizzapi==0.0.3 # homeassistant.components.media_player.plex # homeassistant.components.sensor.plex -plexapi==3.0.3 +plexapi==3.0.5 # homeassistant.components.sensor.mhz19 # homeassistant.components.sensor.serial_pm @@ -569,10 +587,10 @@ pocketcasts==0.1 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.0.21 +prometheus_client==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==5.4.2 +psutil==5.4.3 # homeassistant.components.wink pubnubsub-handler==1.0.2 @@ -604,7 +622,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.20.1 +pyRFXtrx==0.21.1 # homeassistant.components.sensor.tibber pyTibber==0.2.1 @@ -622,7 +640,7 @@ pyairvisual==1.0.0 pyalarmdotcom==0.3.0 # homeassistant.components.arlo -pyarlo==0.1.0 +pyarlo==0.1.2 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 @@ -641,7 +659,7 @@ pybbox==0.0.5-alpha # pybluez==0.22 # homeassistant.components.media_player.cast -pychromecast==0.8.2 +pychromecast==1.0.3 # homeassistant.components.media_player.cmus pycmus==0.1.0 @@ -655,6 +673,13 @@ pycsspeechtts==1.0.2 # homeassistant.components.sensor.cups # pycups==1.9.73 +# homeassistant.components.daikin +# homeassistant.components.climate.daikin +pydaikin==0.4 + +# homeassistant.components.deconz +pydeconz==23 + # homeassistant.components.zwave pydispatcher==2.0.5 @@ -664,6 +689,9 @@ pydroid-ipcam==0.8 # homeassistant.components.sensor.ebox pyebox==0.1.0 +# homeassistant.components.climate.econet +pyeconet==0.0.4 + # homeassistant.components.eight_sleep pyeight==0.0.7 @@ -677,7 +705,7 @@ pyenvisalink==2.2 pyephember==0.1.1 # homeassistant.components.sensor.fido -pyfido==1.0.1 +pyfido==2.1.0 # homeassistant.components.climate.flexit pyflexit==0.3 @@ -692,13 +720,13 @@ pyharmony==1.0.18 pyhik==0.1.4 # homeassistant.components.hive -pyhiveapi==0.2.5 +pyhiveapi==0.2.10 # homeassistant.components.homematic -pyhomematic==0.1.36 +pyhomematic==0.1.37 # homeassistant.components.sensor.hydroquebec -pyhydroquebec==1.3.1 +pyhydroquebec==2.1.0 # homeassistant.components.alarm_control_panel.ialarm pyialarm==0.2 @@ -753,7 +781,7 @@ pymochad==0.1.1 pymodbus==1.3.1 # homeassistant.components.media_player.monoprice -pymonoprice==0.2 +pymonoprice==0.3 # homeassistant.components.media_player.yamaha_musiccast pymusiccast==0.1.6 @@ -788,16 +816,16 @@ pyotp==2.2.6 # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap -pyowm==2.7.1 +pyowm==2.8.0 # homeassistant.components.qwikswitch pyqwikswitch==0.4 -# homeassistant.components.switch.rainbird -pyrainbird==0.1.0 +# homeassistant.components.rainbird +pyrainbird==0.1.3 # homeassistant.components.climate.sensibo -pysensibo==1.0.1 +pysensibo==1.0.2 # homeassistant.components.sensor.serial pyserial-asyncio==0.4 @@ -814,7 +842,7 @@ pysma==0.1.3 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp # homeassistant.components.switch.snmp -pysnmp==4.4.2 +pysnmp==4.4.4 # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.3 @@ -830,7 +858,7 @@ python-blockchain-api==0.0.2 python-clementine-remote==1.0.1 # homeassistant.components.digital_ocean -python-digitalocean==1.12 +python-digitalocean==1.13.2 # homeassistant.components.ecobee python-ecobee-api==0.0.14 @@ -839,9 +867,10 @@ python-ecobee-api==0.0.14 # python-eq3bt==0.1.6 # homeassistant.components.sensor.etherscan -python-etherscan-api==0.0.1 +python-etherscan-api==0.0.2 # homeassistant.components.sensor.darksky +# homeassistant.components.weather.darksky python-forecastio==1.3.5 # homeassistant.components.gc100 @@ -864,7 +893,7 @@ python-juicenet==0.0.5 # homeassistant.components.light.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.2 +python-miio==0.3.3 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 @@ -888,6 +917,9 @@ python-ripple-api==0.0.3 # homeassistant.components.media_player.roku python-roku==3.1.3 +# homeassistant.components.sensor.sochain +python-sochain-api==0.0.2 + # homeassistant.components.sensor.synologydsm python-synology==0.1.0 @@ -895,7 +927,7 @@ python-synology==0.1.0 python-tado==0.2.2 # homeassistant.components.telegram_bot -python-telegram-bot==8.1.1 +python-telegram-bot==9.0.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 @@ -916,13 +948,16 @@ python_opendata_transport==0.0.3 python_openzwave==0.4.0.35 # homeassistant.components.alarm_control_panel.egardia -pythonegardia==1.0.22 +pythonegardia==1.0.26 # homeassistant.components.sensor.whois pythonwhois==2.4.3 # homeassistant.components.device_tracker.tile -pytile==1.0.0 +pytile==1.1.0 + +# homeassistant.components.climate.touchline +pytouchline==0.6 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 @@ -946,10 +981,10 @@ pyvizio==0.0.2 pyvlx==0.1.3 # homeassistant.components.notify.html5 -pywebpush==1.3.0 +pywebpush==1.5.0 # homeassistant.components.wemo -pywemo==0.4.20 +pywemo==0.4.25 # homeassistant.components.zabbix pyzabbix==0.7.4 @@ -1006,7 +1041,7 @@ samsungctl==0.6.0 satel_integra==0.1.0 # homeassistant.components.sensor.deutsche_bahn -schiene==0.19 +schiene==0.20 # homeassistant.components.scsgate scsgate==0.1.0 @@ -1060,7 +1095,7 @@ speedtest-cli==1.0.7 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.1.15 +sqlalchemy==1.2.0 # homeassistant.components.statsd statsd==3.2.1 @@ -1181,7 +1216,7 @@ yahoo-finance==1.4.0 # homeassistant.components.sensor.yweather # homeassistant.components.weather.yweather -yahooweather==0.9 +yahooweather==0.10 # homeassistant.components.light.yeelight yeelight==0.3.3 @@ -1190,7 +1225,7 @@ yeelight==0.3.3 yeelightsunflower==0.0.8 # homeassistant.components.media_extractor -youtube_dl==2017.12.10 +youtube_dl==2017.12.28 # homeassistant.components.light.zengge zengge==0.2 diff --git a/requirements_docs.txt b/requirements_docs.txt index 68fbec8cf97..04ebb074e03 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.6.5 +Sphinx==1.6.6 sphinx-autodoc-typehints==1.2.3 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test.txt b/requirements_test.txt index 3edfa168f79..22bb6623e16 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,17 +3,15 @@ # new version flake8==3.3 pylint==1.6.5 -mypy==0.550 +mypy==0.560 pydocstyle==1.1.1 -coveralls>=1.1 -pytest>=2.9.2 -pytest-aiohttp>=0.1.3 -pytest-cov>=2.3.1 -pytest-timeout>=1.2.0 -pytest-catchlog>=1.2.2 -pytest-sugar>=0.7.1 -requests_mock>=1.0 -mock-open>=1.3.1 +coveralls==1.2.0 +pytest==3.3.1 +pytest-aiohttp==0.3.0 +pytest-cov==2.5.1 +pytest-timeout>=1.2.1 +pytest-sugar==0.9.0 +requests_mock==1.4 +mock-open==1.3.1 flake8-docstrings==1.0.2 -asynctest>=0.8.0 -freezegun>=0.3.8 +asynctest>=0.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a96c3af1fd9..300f5496a16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,20 +4,18 @@ # new version flake8==3.3 pylint==1.6.5 -mypy==0.550 +mypy==0.560 pydocstyle==1.1.1 -coveralls>=1.1 -pytest>=2.9.2 -pytest-aiohttp>=0.1.3 -pytest-cov>=2.3.1 -pytest-timeout>=1.2.0 -pytest-catchlog>=1.2.2 -pytest-sugar>=0.7.1 -requests_mock>=1.0 -mock-open>=1.3.1 +coveralls==1.2.0 +pytest==3.3.1 +pytest-aiohttp==0.3.0 +pytest-cov==2.5.1 +pytest-timeout>=1.2.1 +pytest-sugar==0.9.0 +requests_mock==1.4 +mock-open==1.3.1 flake8-docstrings==1.0.2 -asynctest>=0.8.0 -freezegun>=0.3.8 +asynctest>=0.11.1 # homeassistant.components.notify.html5 @@ -31,7 +29,7 @@ aioautomatic==0.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.5.3 +aiohttp_cors==0.6.0 # homeassistant.components.notify.apns apns2==0.3.0 @@ -59,7 +57,7 @@ evohomeclient==0.2.5 feedparser==5.2.1 # homeassistant.components.conversation -fuzzywuzzy==0.15.1 +fuzzywuzzy==0.16.0 # homeassistant.components.tts.google gTTS-token==1.1.1 @@ -77,7 +75,7 @@ hbmqtt==0.9.1 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171216.0 +home-assistant-frontend==20180112.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -95,7 +93,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.13.3 +numpy==1.14.0 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -116,7 +114,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.0.21 +prometheus_client==0.1.0 # homeassistant.components.canary py-canary==0.2.3 @@ -127,11 +125,15 @@ pydispatcher==2.0.5 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.media_player.monoprice +pymonoprice==0.3 + # homeassistant.components.alarm_control_panel.nx584 # homeassistant.components.binary_sensor.nx584 pynx584==0.4 # homeassistant.components.sensor.darksky +# homeassistant.components.weather.darksky python-forecastio==1.3.5 # homeassistant.components.sensor.whois @@ -141,7 +143,7 @@ pythonwhois==2.4.3 pyunifi==2.13 # homeassistant.components.notify.html5 -pywebpush==1.3.0 +pywebpush==1.5.0 # homeassistant.components.python_script restrictedpython==4.0b2 @@ -163,7 +165,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.1.15 +sqlalchemy==1.2.0 # homeassistant.components.statsd statsd==3.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0bfb5f9e607..5f4d789fa77 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -66,6 +66,7 @@ TEST_REQUIREMENTS = ( 'pydispatcher', 'PyJWT', 'pylitejet', + 'pymonoprice', 'pynx584', 'python-forecastio', 'pyunifi', diff --git a/setup.py b/setup.py index fe60a15e32e..4b19e47fb2c 100755 --- a/setup.py +++ b/setup.py @@ -53,8 +53,8 @@ REQUIRES = [ 'jinja2>=2.9.6', 'voluptuous==0.10.5', 'typing>=3,<4', - 'aiohttp==2.3.6', # If updated, check if yarl also needs an update! - 'yarl==0.16.0', + 'aiohttp==2.3.7', # If updated, check if yarl also needs an update! + 'yarl==0.18.0', 'async_timeout==2.0.0', 'chardet==3.0.4', 'astral==1.4', diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index 368a43e6113..200978ea1a0 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -4,7 +4,8 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, + STATE_UNKNOWN) from homeassistant.components import alarm_control_panel from tests.common import ( @@ -190,3 +191,33 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): alarm_control_panel.alarm_disarm(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) + + state = self.hass.states.get('alarm_control_panel.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('alarm_control_panel.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('alarm_control_panel.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a0876dea5df..6ac56bc10a3 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -9,7 +9,7 @@ from homeassistant.helpers import entityfilter from tests.common import async_mock_service -DEFAULT_CONFIG = smart_home.Config(filter=lambda entity_id: True) +DEFAULT_CONFIG = smart_home.Config(should_expose=lambda entity_id: True) def get_new_request(namespace, name, endpoint=None): @@ -338,7 +338,7 @@ def test_exclude_filters(hass): hass.states.async_set( 'cover.deny', 'off', {'friendly_name': "Blocked cover"}) - config = smart_home.Config(filter=entityfilter.generate_filter( + config = smart_home.Config(should_expose=entityfilter.generate_filter( include_domains=[], include_entities=[], exclude_domains=['script'], @@ -371,7 +371,7 @@ def test_include_filters(hass): hass.states.async_set( 'group.allow', 'off', {'friendly_name': "Allowed group"}) - config = smart_home.Config(filter=entityfilter.generate_filter( + config = smart_home.Config(should_expose=entityfilter.generate_filter( include_domains=['automation', 'group'], include_entities=['script.deny'], exclude_domains=[], @@ -422,7 +422,7 @@ def test_api_function_not_implemented(hass): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['alert', 'automation', 'group', +@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', 'group', 'input_boolean', 'light', 'script', 'switch']) def test_api_turn_on(hass, domain): @@ -441,7 +441,10 @@ def test_api_turn_on(hass, domain): if domain == 'group': call_domain = 'homeassistant' - call = async_mock_service(hass, call_domain, 'turn_on') + if domain == 'cover': + call = async_mock_service(hass, call_domain, 'open_cover') + else: + call = async_mock_service(hass, call_domain, 'turn_on') msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) @@ -456,7 +459,7 @@ def test_api_turn_on(hass, domain): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['alert', 'automation', 'group', +@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', 'group', 'input_boolean', 'light', 'script', 'switch']) def test_api_turn_off(hass, domain): @@ -475,7 +478,10 @@ def test_api_turn_off(hass, domain): if domain == 'group': call_domain = 'homeassistant' - call = async_mock_service(hass, call_domain, 'turn_off') + if domain == 'cover': + call = async_mock_service(hass, call_domain, 'close_cover') + else: + call = async_mock_service(hass, call_domain, 'turn_off') msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) @@ -1110,3 +1116,40 @@ def test_api_mute(hass, domain): assert len(call) == 1 assert call[0].data['entity_id'] == '{}.test'.format(domain) assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_entity_config(hass): + """Test that we can configure things via entity config.""" + request = get_new_request('Alexa.Discovery', 'Discover') + + hass.states.async_set( + 'light.test_1', 'on', {'friendly_name': "Test light 1"}) + + config = smart_home.Config( + should_expose=lambda entity_id: True, + entity_config={ + 'light.test_1': { + 'name': 'Config name', + 'display_categories': 'SWITCH', + 'description': 'Config description' + } + } + ) + + msg = yield from smart_home.async_handle_message( + hass, config, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(msg['payload']['endpoints']) == 1 + + appliance = msg['payload']['endpoints'][0] + assert appliance['endpointId'] == 'light#test_1' + assert appliance['displayCategories'][0] == "SWITCH" + assert appliance['friendlyName'] == "Config name" + assert appliance['description'] == "Config description" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index b1ee0841e2d..bf54d24492a 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -409,6 +409,40 @@ class TestAutomationState(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_for_multiple_force_update(self): + """Test for firing on entity change with for and force update.""" + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.force_entity', + 'to': 'world', + 'for': { + 'seconds': 5 + }, + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + utcnow = dt_util.utcnow() + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = utcnow + self.hass.states.set('test.force_entity', 'world', None, True) + self.hass.block_till_done() + for _ in range(0, 4): + mock_utcnow.return_value += timedelta(seconds=1) + fire_time_changed(self.hass, mock_utcnow.return_value) + self.hass.states.set('test.force_entity', 'world', None, True) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + mock_utcnow.return_value += timedelta(seconds=4) + fire_time_changed(self.hass, mock_utcnow.return_value) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_for(self): """Test for firing on entity change with for.""" assert setup_component(self.hass, automation.DOMAIN, { diff --git a/tests/components/binary_sensor/test_rest.py b/tests/components/binary_sensor/test_rest.py new file mode 100644 index 00000000000..d0670bf5154 --- /dev/null +++ b/tests/components/binary_sensor/test_rest.py @@ -0,0 +1,192 @@ +"""The tests for the REST binary sensor platform.""" +import unittest +from unittest.mock import patch, Mock + +import requests +from requests.exceptions import Timeout, MissingSchema +import requests_mock + +from homeassistant.setup import setup_component +import homeassistant.components.binary_sensor as binary_sensor +import homeassistant.components.binary_sensor.rest as rest +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.helpers import template + +from tests.common import get_test_home_assistant, assert_setup_component + + +class TestRestBinarySensorSetup(unittest.TestCase): + """Tests for setting up the REST binary sensor platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_missing_config(self): + """Test setup with configuration missing required entries.""" + with assert_setup_component(0): + assert setup_component(self.hass, binary_sensor.DOMAIN, { + 'binary_sensor': {'platform': 'rest'}}) + + def test_setup_missing_schema(self): + """Test setup with resource missing schema.""" + with self.assertRaises(MissingSchema): + rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'localhost', + 'method': 'GET' + }, None) + + @patch('requests.Session.send', + side_effect=requests.exceptions.ConnectionError()) + def test_setup_failed_connect(self, mock_req): + """Test setup when connection error occurs.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost', + }, lambda devices, update=True: None)) + + @patch('requests.Session.send', side_effect=Timeout()) + def test_setup_timeout(self, mock_req): + """Test setup when connection timeout occurs.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost', + }, lambda devices, update=True: None)) + + @requests_mock.Mocker() + def test_setup_minimum(self, mock_req): + """Test setup with minimum configuration.""" + mock_req.get('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'rest', + 'resource': 'http://localhost' + } + })) + self.assertEqual(2, mock_req.call_count) + assert_setup_component(1, 'switch') + + @requests_mock.Mocker() + def test_setup_get(self, mock_req): + """Test setup with valid configuration.""" + mock_req.get('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'rest', + 'resource': 'http://localhost', + 'method': 'GET', + 'value_template': '{{ value_json.key }}', + 'name': 'foo', + 'unit_of_measurement': 'MB', + 'verify_ssl': 'true', + 'authentication': 'basic', + 'username': 'my username', + 'password': 'my password', + 'headers': {'Accept': 'application/json'} + } + })) + self.assertEqual(2, mock_req.call_count) + assert_setup_component(1, 'binary_sensor') + + @requests_mock.Mocker() + def test_setup_post(self, mock_req): + """Test setup with valid configuration.""" + mock_req.post('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'rest', + 'resource': 'http://localhost', + 'method': 'POST', + 'value_template': '{{ value_json.key }}', + 'payload': '{ "device": "toaster"}', + 'name': 'foo', + 'unit_of_measurement': 'MB', + 'verify_ssl': 'true', + 'authentication': 'basic', + 'username': 'my username', + 'password': 'my password', + 'headers': {'Accept': 'application/json'} + } + })) + self.assertEqual(2, mock_req.call_count) + assert_setup_component(1, 'binary_sensor') + + +class TestRestBinarySensor(unittest.TestCase): + """Tests for REST binary sensor platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.rest = Mock('RestData') + self.rest.update = Mock('RestData.update', + side_effect=self.update_side_effect( + '{ "key": false }')) + self.name = 'foo' + self.device_class = 'light' + self.value_template = \ + template.Template('{{ value_json.key }}', self.hass) + + self.binary_sensor = rest.RestBinarySensor( + self.hass, self.rest, self.name, self.device_class, + self.value_template) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def update_side_effect(self, data): + """Side effect function for mocking RestData.update().""" + self.rest.data = data + + def test_name(self): + """Test the name.""" + self.assertEqual(self.name, self.binary_sensor.name) + + def test_device_class(self): + """Test the device class.""" + self.assertEqual(self.device_class, self.binary_sensor.device_class) + + def test_initial_state(self): + """Test the initial state.""" + self.binary_sensor.update() + self.assertEqual(STATE_OFF, self.binary_sensor.state) + + def test_update_when_value_is_none(self): + """Test state gets updated to unknown when sensor returns no data.""" + self.rest.update = Mock( + 'RestData.update', + side_effect=self.update_side_effect(None)) + self.binary_sensor.update() + self.assertFalse(self.binary_sensor.available) + + def test_update_when_value_changed(self): + """Test state gets updated when sensor returns a new status.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": true }')) + self.binary_sensor.update() + self.assertEqual(STATE_ON, self.binary_sensor.state) + self.assertTrue(self.binary_sensor.available) + + def test_update_when_failed_request(self): + """Test state gets updated when sensor returns a new status.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect(None)) + self.binary_sensor.update() + self.assertFalse(self.binary_sensor.available) + + def test_update_with_no_template(self): + """Test update when there is no value template.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect('true')) + self.binary_sensor = rest.RestBinarySensor( + self.hass, self.rest, self.name, self.device_class, None) + self.binary_sensor.update() + self.assertEqual(STATE_ON, self.binary_sensor.state) + self.assertTrue(self.binary_sensor.available) diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py index d8c49de1cc0..38573b295d3 100644 --- a/tests/components/binary_sensor/test_threshold.py +++ b/tests/components/binary_sensor/test_threshold.py @@ -2,7 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS) from tests.common import get_test_home_assistant @@ -23,8 +24,7 @@ class TestThresholdSensor(unittest.TestCase): config = { 'binary_sensor': { 'platform': 'threshold', - 'threshold': '15', - 'type': 'upper', + 'upper': '15', 'entity_id': 'sensor.test_monitored', } } @@ -37,12 +37,14 @@ class TestThresholdSensor(unittest.TestCase): state = self.hass.states.get('binary_sensor.threshold') - self.assertEqual('upper', state.attributes.get('type')) self.assertEqual('sensor.test_monitored', state.attributes.get('entity_id')) self.assertEqual(16, state.attributes.get('sensor_value')) - self.assertEqual(float(config['binary_sensor']['threshold']), - state.attributes.get('threshold')) + self.assertEqual('above', state.attributes.get('position')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + self.assertEqual(0.0, state.attributes.get('hysteresis')) + self.assertEqual('upper', state.attributes.get('type')) assert state.state == 'on' @@ -65,9 +67,7 @@ class TestThresholdSensor(unittest.TestCase): config = { 'binary_sensor': { 'platform': 'threshold', - 'threshold': '15', - 'name': 'Test_threshold', - 'type': 'lower', + 'lower': '15', 'entity_id': 'sensor.test_monitored', } } @@ -77,8 +77,12 @@ class TestThresholdSensor(unittest.TestCase): self.hass.states.set('sensor.test_monitored', 16) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') + self.assertEqual('above', state.attributes.get('position')) + self.assertEqual(float(config['binary_sensor']['lower']), + state.attributes.get('lower')) + self.assertEqual(0.0, state.attributes.get('hysteresis')) self.assertEqual('lower', state.attributes.get('type')) assert state.state == 'off' @@ -86,26 +90,17 @@ class TestThresholdSensor(unittest.TestCase): self.hass.states.set('sensor.test_monitored', 14) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') assert state.state == 'on' - self.hass.states.set('sensor.test_monitored', 15) - self.hass.block_till_done() - - state = self.hass.states.get('binary_sensor.test_threshold') - - assert state.state == 'off' - def test_sensor_hysteresis(self): """Test if source is above threshold using hysteresis.""" config = { 'binary_sensor': { 'platform': 'threshold', - 'threshold': '15', + 'upper': '15', 'hysteresis': '2.5', - 'name': 'Test_threshold', - 'type': 'upper', 'entity_id': 'sensor.test_monitored', } } @@ -115,34 +110,226 @@ class TestThresholdSensor(unittest.TestCase): self.hass.states.set('sensor.test_monitored', 20) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('above', state.attributes.get('position')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + self.assertEqual(2.5, state.attributes.get('hysteresis')) + self.assertEqual('upper', state.attributes.get('type')) assert state.state == 'on' self.hass.states.set('sensor.test_monitored', 13) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') assert state.state == 'on' self.hass.states.set('sensor.test_monitored', 12) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') assert state.state == 'off' self.hass.states.set('sensor.test_monitored', 17) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') assert state.state == 'off' self.hass.states.set('sensor.test_monitored', 18) self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test_threshold') + state = self.hass.states.get('binary_sensor.threshold') assert state.state == 'on' + + def test_sensor_in_range_no_hysteresis(self): + """Test if source is within the range.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'lower': '10', + 'upper': '20', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('sensor.test_monitored', + state.attributes.get('entity_id')) + self.assertEqual(16, state.attributes.get('sensor_value')) + self.assertEqual('in_range', state.attributes.get('position')) + self.assertEqual(float(config['binary_sensor']['lower']), + state.attributes.get('lower')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + self.assertEqual(0.0, state.attributes.get('hysteresis')) + self.assertEqual('range', state.attributes.get('type')) + + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 9) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('below', state.attributes.get('position')) + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 21) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('above', state.attributes.get('position')) + assert state.state == 'off' + + def test_sensor_in_range_with_hysteresis(self): + """Test if source is within the range.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'lower': '10', + 'upper': '20', + 'hysteresis': '2', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('sensor.test_monitored', + state.attributes.get('entity_id')) + self.assertEqual(16, state.attributes.get('sensor_value')) + self.assertEqual('in_range', state.attributes.get('position')) + self.assertEqual(float(config['binary_sensor']['lower']), + state.attributes.get('lower')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + self.assertEqual(float(config['binary_sensor']['hysteresis']), + state.attributes.get('hysteresis')) + self.assertEqual('range', state.attributes.get('type')) + + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 8) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('in_range', state.attributes.get('position')) + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 7) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('below', state.attributes.get('position')) + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 12) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('below', state.attributes.get('position')) + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 13) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('in_range', state.attributes.get('position')) + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 22) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('in_range', state.attributes.get('position')) + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 23) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('above', state.attributes.get('position')) + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 18) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('above', state.attributes.get('position')) + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 17) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('in_range', state.attributes.get('position')) + assert state.state == 'on' + + def test_sensor_in_range_unknown_state(self): + """Test if source is within the range.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'lower': '10', + 'upper': '20', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('sensor.test_monitored', + state.attributes.get('entity_id')) + self.assertEqual(16, state.attributes.get('sensor_value')) + self.assertEqual('in_range', state.attributes.get('position')) + self.assertEqual(float(config['binary_sensor']['lower']), + state.attributes.get('lower')) + self.assertEqual(float(config['binary_sensor']['upper']), + state.attributes.get('upper')) + self.assertEqual(0.0, state.attributes.get('hysteresis')) + self.assertEqual('range', state.attributes.get('type')) + + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', STATE_UNKNOWN) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('unknown', state.attributes.get('position')) + assert state.state == 'off' diff --git a/tests/components/binary_sensor/test_vultr.py b/tests/components/binary_sensor/test_vultr.py index 2bcb220233b..91d5da34901 100644 --- a/tests/components/binary_sensor/test_vultr.py +++ b/tests/components/binary_sensor/test_vultr.py @@ -1,5 +1,8 @@ """Test the Vultr binary sensor platform.""" +import json import unittest +from unittest.mock import patch + import requests_mock import pytest import voluptuous as vol @@ -50,10 +53,6 @@ class TestVultrBinarySensorSetup(unittest.TestCase): """Stop our started services.""" self.hass.stop() - def test_failed_hub(self): - """Test a hub setup failure.""" - base_vultr.setup(self.hass, VALID_CONFIG) - @requests_mock.Mocker() def test_binary_sensor(self, mock): """Test successful instance.""" @@ -61,12 +60,12 @@ class TestVultrBinarySensorSetup(unittest.TestCase): 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - # Setup hub - base_vultr.setup(self.hass, VALID_CONFIG) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + # Setup hub + base_vultr.setup(self.hass, VALID_CONFIG) # Setup each of our test configs for config in self.configs: @@ -137,11 +136,12 @@ class TestVultrBinarySensorSetup(unittest.TestCase): 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - base_vultr.setup(self.hass, VALID_CONFIG) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + # Setup hub + base_vultr.setup(self.hass, VALID_CONFIG) bad_conf = {} # No subscription diff --git a/tests/components/binary_sensor/test_workday.py b/tests/components/binary_sensor/test_workday.py index 6abfa89d435..af7e856e417 100644 --- a/tests/components/binary_sensor/test_workday.py +++ b/tests/components/binary_sensor/test_workday.py @@ -1,5 +1,7 @@ """Tests the HASS workday binary sensor.""" -from freezegun import freeze_time +from datetime import date +from unittest.mock import patch + from homeassistant.components.binary_sensor.workday import day_to_string from homeassistant.setup import setup_component @@ -7,6 +9,9 @@ from tests.common import ( get_test_home_assistant, assert_setup_component) +FUNCTION_PATH = 'homeassistant.components.binary_sensor.workday.get_date' + + class TestWorkdaySetup(object): """Test class for workday sensor.""" @@ -94,46 +99,45 @@ class TestWorkdaySetup(object): def test_setup_component_province(self): """Setup workday component.""" with assert_setup_component(1, 'binary_sensor'): - setup_component(self.hass, 'binary_sensor', self.config_province) + setup_component(self.hass, 'binary_sensor', + self.config_province) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None + entity = self.hass.states.get('binary_sensor.workday_sensor') + assert entity is not None - # Freeze time to a workday - @freeze_time("Mar 15th, 2017") - def test_workday_province(self): + # Freeze time to a workday - Mar 15th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 3, 15)) + def test_workday_province(self, mock_date): """Test if workdays are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): - setup_component(self.hass, 'binary_sensor', self.config_province) - - assert self.hass.states.get('binary_sensor.workday_sensor') is not None + setup_component(self.hass, 'binary_sensor', + self.config_province) self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'on' - # Freeze time to a weekend - @freeze_time("Mar 12th, 2017") - def test_weekend_province(self): + # Freeze time to a weekend - Mar 12th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 3, 12)) + def test_weekend_province(self, mock_date): """Test if weekends are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): - setup_component(self.hass, 'binary_sensor', self.config_province) - - assert self.hass.states.get('binary_sensor.workday_sensor') is not None + setup_component(self.hass, 'binary_sensor', + self.config_province) self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'off' - # Freeze time to a public holiday in province BW - @freeze_time("Jan 6th, 2017") - def test_public_holiday_province(self): + # Freeze time to a public holiday in province BW - Jan 6th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 1, 6)) + def test_public_holiday_province(self, mock_date): """Test if public holidays are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): - setup_component(self.hass, 'binary_sensor', self.config_province) - - assert self.hass.states.get('binary_sensor.workday_sensor') is not None + setup_component(self.hass, 'binary_sensor', + self.config_province) self.hass.start() @@ -143,47 +147,44 @@ class TestWorkdaySetup(object): def test_setup_component_noprovince(self): """Setup workday component.""" with assert_setup_component(1, 'binary_sensor'): - setup_component(self.hass, 'binary_sensor', self.config_noprovince) + setup_component(self.hass, 'binary_sensor', + self.config_noprovince) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None + entity = self.hass.states.get('binary_sensor.workday_sensor') + assert entity is not None - # Freeze time to a public holiday in province BW - @freeze_time("Jan 6th, 2017") - def test_public_holiday_noprovince(self): + # Freeze time to a public holiday in province BW - Jan 6th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 1, 6)) + def test_public_holiday_noprovince(self, mock_date): """Test if public holidays are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): - setup_component(self.hass, 'binary_sensor', self.config_noprovince) - - assert self.hass.states.get('binary_sensor.workday_sensor') is not None + setup_component(self.hass, 'binary_sensor', + self.config_noprovince) self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'on' - # Freeze time to a public holiday in state CA - @freeze_time("Mar 31st, 2017") - def test_public_holiday_state(self): + # Freeze time to a public holiday in state CA - Mar 31st, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 3, 31)) + def test_public_holiday_state(self, mock_date): """Test if public holidays are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): setup_component(self.hass, 'binary_sensor', self.config_state) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None - self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'off' - # Freeze time to a public holiday in state CA - @freeze_time("Mar 31st, 2017") - def test_public_holiday_nostate(self): + # Freeze time to a public holiday in state CA - Mar 31st, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 3, 31)) + def test_public_holiday_nostate(self, mock_date): """Test if public holidays are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): setup_component(self.hass, 'binary_sensor', self.config_nostate) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None - self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') @@ -195,63 +196,56 @@ class TestWorkdaySetup(object): setup_component(self.hass, 'binary_sensor', self.config_invalidprovince) - assert self.hass.states.get('binary_sensor.workday_sensor') is None + entity = self.hass.states.get('binary_sensor.workday_sensor') + assert entity is None - # Freeze time to a public holiday in province BW - @freeze_time("Jan 6th, 2017") - def test_public_holiday_includeholiday(self): + # Freeze time to a public holiday in province BW - Jan 6th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 1, 6)) + def test_public_holiday_includeholiday(self, mock_date): """Test if public holidays are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): setup_component(self.hass, 'binary_sensor', self.config_includeholiday) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None - self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'on' - # Freeze time to a saturday to test offset - @freeze_time("Aug 5th, 2017") - def test_tomorrow(self): + # Freeze time to a saturday to test offset - Aug 5th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 8, 5)) + def test_tomorrow(self, mock_date): """Test if tomorrow are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): setup_component(self.hass, 'binary_sensor', self.config_tomorrow) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None - self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'off' - # Freeze time to a saturday to test offset - @freeze_time("Aug 5th, 2017") - def test_day_after_tomorrow(self): + # Freeze time to a saturday to test offset - Aug 5th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 8, 5)) + def test_day_after_tomorrow(self, mock_date): """Test if the day after tomorrow are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): setup_component(self.hass, 'binary_sensor', self.config_day_after_tomorrow) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None - self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') assert entity.state == 'on' - # Freeze time to a saturday to test offset - @freeze_time("Aug 5th, 2017") - def test_yesterday(self): + # Freeze time to a saturday to test offset - Aug 5th, 2017 + @patch(FUNCTION_PATH, return_value=date(2017, 8, 5)) + def test_yesterday(self, mock_date): """Test if yesterday are reported correctly.""" with assert_setup_component(1, 'binary_sensor'): setup_component(self.hass, 'binary_sensor', self.config_yesterday) - assert self.hass.states.get('binary_sensor.workday_sensor') is not None - self.hass.start() entity = self.hass.states.get('binary_sensor.workday_sensor') diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py old mode 100755 new mode 100644 diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index d15249d61f3..9098494bf48 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -250,3 +250,20 @@ class TestDemoClimate(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) + + def test_set_on_off(self): + """Test on/off service.""" + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('auto', state.state) + + self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_OFF, + {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('off', state.state) + + self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_ON, + {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('auto', state.state) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 63bbce2e7c6..776e79a6827 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -29,6 +29,7 @@ from tests.common import (assert_setup_component, get_test_home_assistant, ENTITY = 'climate.test' ENT_SENSOR = 'sensor.test' ENT_SWITCH = 'switch.test' +ATTR_AWAY_MODE = 'away_mode' MIN_TEMP = 3.0 MAX_TEMP = 65.0 TARGET_TEMP = 42.0 @@ -69,22 +70,6 @@ class TestSetupClimateGenericThermostat(unittest.TestCase): }}) ) - def test_setup_with_sensor(self): - """Test set up heat_control with sensor to trigger update at init.""" - self.hass.states.set(ENT_SENSOR, 22.0, { - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS - }) - assert setup_component(self.hass, climate.DOMAIN, {'climate': { - 'platform': 'generic_thermostat', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR - }}) - state = self.hass.states.get(ENTITY) - self.assertEqual( - TEMP_CELSIUS, state.attributes.get('unit_of_measurement')) - self.assertEqual(22.0, state.attributes.get('current_temperature')) - class TestGenericThermostatHeaterSwitching(unittest.TestCase): """Test the Generic thermostat heater switching. @@ -197,7 +182,7 @@ class TestClimateGenericThermostat(unittest.TestCase): """Test that the operation list returns the correct modes.""" state = self.hass.states.get(ENTITY) modes = state.attributes.get('operation_list') - self.assertEqual([climate.STATE_AUTO, STATE_OFF], modes) + self.assertEqual([climate.STATE_HEAT, STATE_OFF], modes) def test_set_target_temp(self): """Test the setting of the target temperature.""" @@ -210,6 +195,31 @@ class TestClimateGenericThermostat(unittest.TestCase): state = self.hass.states.get(ENTITY) self.assertEqual(30.0, state.attributes.get('temperature')) + def test_set_away_mode(self): + """Test the setting away mode.""" + climate.set_temperature(self.hass, 23) + self.hass.block_till_done() + climate.set_away_mode(self.hass, True) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(16, state.attributes.get('temperature')) + + def test_set_away_mode_and_restore_prev_temp(self): + """Test the setting and removing away mode. + + Verify original temperature is restored. + """ + climate.set_temperature(self.hass, 23) + self.hass.block_till_done() + climate.set_away_mode(self.hass, True) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(16, state.attributes.get('temperature')) + climate.set_away_mode(self.hass, False) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(23, state.attributes.get('temperature')) + def test_sensor_bad_unit(self): """Test sensor that have bad unit.""" state = self.hass.states.get(ENTITY) @@ -337,8 +347,8 @@ class TestClimateGenericThermostat(unittest.TestCase): self.hass.block_till_done() self.assertEqual(log_mock.call_count, 1) - def test_operating_mode_auto(self): - """Test change mode from OFF to AUTO. + def test_operating_mode_heat(self): + """Test change mode from OFF to HEAT. Switch turns on when temp below setpoint and mode changes. """ @@ -347,7 +357,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self._setup_sensor(25) self.hass.block_till_done() self._setup_switch(False) - climate.set_operation_mode(self.hass, climate.STATE_AUTO) + climate.set_operation_mode(self.hass, climate.STATE_HEAT) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -387,6 +397,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): 'name': 'test', 'cold_tolerance': 2, 'hot_tolerance': 4, + 'away_temp': 30, 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR, 'ac_mode': True @@ -409,6 +420,35 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def test_turn_away_mode_on_cooling(self): + """Test the setting away mode when cooling.""" + self._setup_sensor(25) + self.hass.block_till_done() + climate.set_temperature(self.hass, 19) + self.hass.block_till_done() + climate.set_away_mode(self.hass, True) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(30, state.attributes.get('temperature')) + + def test_operating_mode_cool(self): + """Test change mode from OFF to COOL. + + Switch turns on when temp below setpoint and mode changes. + """ + climate.set_operation_mode(self.hass, STATE_OFF) + climate.set_temperature(self.hass, 25) + self._setup_sensor(30) + self.hass.block_till_done() + self._setup_switch(False) + climate.set_operation_mode(self.hass, climate.STATE_COOL) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('homeassistant', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def test_set_target_temp_ac_on(self): """Test if target temperature turn ac on.""" self._setup_switch(False) @@ -891,15 +931,13 @@ def test_custom_setup_params(hass): 'target_sensor': ENT_SENSOR, 'min_temp': MIN_TEMP, 'max_temp': MAX_TEMP, - 'target_temp': TARGET_TEMP, - 'initial_operation_mode': STATE_OFF, + 'target_temp': TARGET_TEMP }}) assert result state = hass.states.get(ENTITY) assert state.attributes.get('min_temp') == MIN_TEMP assert state.attributes.get('max_temp') == MAX_TEMP assert state.attributes.get('temperature') == TARGET_TEMP - assert state.attributes.get(climate.ATTR_OPERATION_MODE) == STATE_OFF @asyncio.coroutine @@ -907,7 +945,7 @@ def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache(hass, ( State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", - climate.ATTR_OPERATION_MODE: "off"}), + climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}), )) hass.state = CoreState.starting @@ -927,10 +965,13 @@ def test_restore_state(hass): @asyncio.coroutine def test_no_restore_state(hass): - """Ensure states are not restored on startup if not needed.""" + """Ensure states are restored on startup if they exist. + + Allows for graceful reboot. + """ mock_restore_cache(hass, ( State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", - climate.ATTR_OPERATION_MODE: "off"}), + climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}), )) hass.state = CoreState.starting @@ -941,10 +982,8 @@ def test_no_restore_state(hass): 'name': 'test_thermostat', 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR, - 'target_temp': 22, - 'initial_operation_mode': 'auto', + 'target_temp': 22 }}) state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 22) - assert(state.attributes[climate.ATTR_OPERATION_MODE] != "off") diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 43f90eeee20..4c179fa8042 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -7,7 +7,7 @@ from homeassistant.util.unit_system import ( ) from homeassistant.setup import setup_component from homeassistant.components import climate -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, @@ -432,3 +432,27 @@ class TestMQTTClimate(unittest.TestCase): self.mock_publish.mock_calls[-2][1]) state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['availability_topic'] = 'availability-topic' + config['climate']['payload_available'] = 'good' + config['climate']['payload_not_available'] = 'nogood' + + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get('climate.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('climate.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('climate.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py new file mode 100644 index 00000000000..6ec63646bec --- /dev/null +++ b/tests/components/climate/test_nuheat.py @@ -0,0 +1,232 @@ +"""The test for the NuHeat thermostat module.""" +import unittest +from unittest.mock import Mock, patch +from tests.common import get_test_home_assistant + +from homeassistant.components.climate import ( + SUPPORT_HOLD_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + STATE_HEAT, + STATE_IDLE) +import homeassistant.components.climate.nuheat as nuheat +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +SCHEDULE_HOLD = 3 +SCHEDULE_RUN = 1 +SCHEDULE_TEMPORARY_HOLD = 2 + + +class TestNuHeat(unittest.TestCase): + """Tests for NuHeat climate.""" + + # pylint: disable=protected-access, no-self-use + + def setUp(self): # pylint: disable=invalid-name + """Set up test variables.""" + serial_number = "12345" + temperature_unit = "F" + + thermostat = Mock( + serial_number=serial_number, + room="Master bathroom", + online=True, + heating=True, + temperature=2222, + celsius=22, + fahrenheit=72, + max_celsius=69, + max_fahrenheit=157, + min_celsius=5, + min_fahrenheit=41, + schedule_mode=SCHEDULE_RUN, + target_celsius=22, + target_fahrenheit=72) + + thermostat.get_data = Mock() + thermostat.resume_schedule = Mock() + + self.api = Mock() + self.api.get_thermostat.return_value = thermostat + + self.hass = get_test_home_assistant() + self.thermostat = nuheat.NuHeatThermostat( + self.api, serial_number, temperature_unit) + + def tearDown(self): # pylint: disable=invalid-name + """Stop hass.""" + self.hass.stop() + + @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") + def test_setup_platform(self, mocked_thermostat): + """Test setup_platform.""" + mocked_thermostat.return_value = self.thermostat + thermostat = mocked_thermostat(self.api, "12345", "F") + thermostats = [thermostat] + + self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + + config = {} + add_devices = Mock() + discovery_info = {} + + nuheat.setup_platform(self.hass, config, add_devices, discovery_info) + add_devices.assert_called_once_with(thermostats, True) + + @patch("homeassistant.components.climate.nuheat.NuHeatThermostat") + def test_resume_program_service(self, mocked_thermostat): + """Test resume program service.""" + mocked_thermostat.return_value = self.thermostat + thermostat = mocked_thermostat(self.api, "12345", "F") + thermostat.resume_program = Mock() + thermostat.schedule_update_ha_state = Mock() + thermostat.entity_id = "climate.master_bathroom" + + self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + nuheat.setup_platform(self.hass, {}, Mock(), {}) + + # Explicit entity + self.hass.services.call(nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, + {"entity_id": "climate.master_bathroom"}, True) + + thermostat.resume_program.assert_called_with() + thermostat.schedule_update_ha_state.assert_called_with(True) + + thermostat.resume_program.reset_mock() + thermostat.schedule_update_ha_state.reset_mock() + + # All entities + self.hass.services.call( + nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True) + + thermostat.resume_program.assert_called_with() + thermostat.schedule_update_ha_state.assert_called_with(True) + + def test_name(self): + """Test name property.""" + self.assertEqual(self.thermostat.name, "Master bathroom") + + def test_icon(self): + """Test name property.""" + self.assertEqual(self.thermostat.icon, "mdi:thermometer") + + def test_supported_features(self): + """Test name property.""" + features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE | + SUPPORT_OPERATION_MODE) + self.assertEqual(self.thermostat.supported_features, features) + + def test_temperature_unit(self): + """Test temperature unit.""" + self.assertEqual(self.thermostat.temperature_unit, TEMP_FAHRENHEIT) + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.temperature_unit, TEMP_CELSIUS) + + def test_current_temperature(self): + """Test current temperature.""" + self.assertEqual(self.thermostat.current_temperature, 72) + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.current_temperature, 22) + + def test_current_operation(self): + """Test current operation.""" + self.assertEqual(self.thermostat.current_operation, STATE_HEAT) + self.thermostat._thermostat.heating = False + self.assertEqual(self.thermostat.current_operation, STATE_IDLE) + + def test_min_temp(self): + """Test min temp.""" + self.assertEqual(self.thermostat.min_temp, 41) + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.min_temp, 5) + + def test_max_temp(self): + """Test max temp.""" + self.assertEqual(self.thermostat.max_temp, 157) + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.max_temp, 69) + + def test_target_temperature(self): + """Test target temperature.""" + self.assertEqual(self.thermostat.target_temperature, 72) + self.thermostat._temperature_unit = "C" + self.assertEqual(self.thermostat.target_temperature, 22) + + def test_current_hold_mode(self): + """Test current hold mode.""" + self.thermostat._thermostat.schedule_mode = SCHEDULE_RUN + self.assertEqual(self.thermostat.current_hold_mode, nuheat.MODE_AUTO) + + self.thermostat._thermostat.schedule_mode = SCHEDULE_HOLD + self.assertEqual( + self.thermostat.current_hold_mode, nuheat.MODE_HOLD_TEMPERATURE) + + self.thermostat._thermostat.schedule_mode = SCHEDULE_TEMPORARY_HOLD + self.assertEqual( + self.thermostat.current_hold_mode, nuheat.MODE_TEMPORARY_HOLD) + + self.thermostat._thermostat.schedule_mode = None + self.assertEqual( + self.thermostat.current_hold_mode, nuheat.MODE_AUTO) + + def test_operation_list(self): + """Test the operation list.""" + self.assertEqual( + self.thermostat.operation_list, + [STATE_HEAT, STATE_IDLE] + ) + + def test_resume_program(self): + """Test resume schedule.""" + self.thermostat.resume_program() + self.thermostat._thermostat.resume_schedule.assert_called_once_with() + self.assertTrue(self.thermostat._force_update) + + def test_set_hold_mode(self): + """Test set hold mode.""" + self.thermostat.set_hold_mode("temperature") + self.assertEqual( + self.thermostat._thermostat.schedule_mode, SCHEDULE_HOLD) + self.assertTrue(self.thermostat._force_update) + + self.thermostat.set_hold_mode("temporary_temperature") + self.assertEqual( + self.thermostat._thermostat.schedule_mode, SCHEDULE_TEMPORARY_HOLD) + self.assertTrue(self.thermostat._force_update) + + self.thermostat.set_hold_mode("auto") + self.assertEqual( + self.thermostat._thermostat.schedule_mode, SCHEDULE_RUN) + self.assertTrue(self.thermostat._force_update) + + def test_set_temperature(self): + """Test set temperature.""" + self.thermostat.set_temperature(temperature=85) + self.assertEqual(self.thermostat._thermostat.target_fahrenheit, 85) + self.assertTrue(self.thermostat._force_update) + + self.thermostat._temperature_unit = "C" + self.thermostat.set_temperature(temperature=23) + self.assertEqual(self.thermostat._thermostat.target_celsius, 23) + self.assertTrue(self.thermostat._force_update) + + @patch.object(nuheat.NuHeatThermostat, "_throttled_update") + def test_update_without_throttle(self, throttled_update): + """Test update without throttle.""" + self.thermostat._force_update = True + self.thermostat.update() + throttled_update.assert_called_once_with(no_throttle=True) + self.assertFalse(self.thermostat._force_update) + + @patch.object(nuheat.NuHeatThermostat, "_throttled_update") + def test_update_with_throttle(self, throttled_update): + """Test update with throttle.""" + self.thermostat._force_update = False + self.thermostat.update() + throttled_update.assert_called_once_with() + self.assertFalse(self.thermostat._force_update) + + def test_throttled_update(self): + """Test update with throttle.""" + self.thermostat._throttled_update() + self.thermostat._thermostat.get_data.assert_called_once_with() diff --git a/tests/components/cloud/test_auth_api.py b/tests/components/cloud/test_auth_api.py index f94c2691cd7..70cd5d83f41 100644 --- a/tests/components/cloud/test_auth_api.py +++ b/tests/components/cloud/test_auth_api.py @@ -78,21 +78,17 @@ def test_login(mock_cognito): def test_register(mock_cognito): """Test registering an account.""" cloud = MagicMock() - cloud.cognito_email_based = False cloud = MagicMock() - cloud.cognito_email_based = False auth_api.register(cloud, 'email@home-assistant.io', 'password') assert len(mock_cognito.register.mock_calls) == 1 result_user, result_password = mock_cognito.register.mock_calls[0][1] - assert result_user == \ - auth_api._generate_username('email@home-assistant.io') + assert result_user == 'email@home-assistant.io' assert result_password == 'password' def test_register_fails(mock_cognito): """Test registering an account.""" cloud = MagicMock() - cloud.cognito_email_based = False mock_cognito.register.side_effect = aws_error('SomeError') with pytest.raises(auth_api.CloudError): auth_api.register(cloud, 'email@home-assistant.io', 'password') @@ -101,28 +97,40 @@ def test_register_fails(mock_cognito): def test_confirm_register(mock_cognito): """Test confirming a registration of an account.""" cloud = MagicMock() - cloud.cognito_email_based = False auth_api.confirm_register(cloud, '123456', 'email@home-assistant.io') assert len(mock_cognito.confirm_sign_up.mock_calls) == 1 result_code, result_user = mock_cognito.confirm_sign_up.mock_calls[0][1] - assert result_user == \ - auth_api._generate_username('email@home-assistant.io') + assert result_user == 'email@home-assistant.io' assert result_code == '123456' def test_confirm_register_fails(mock_cognito): """Test an error during confirmation of an account.""" cloud = MagicMock() - cloud.cognito_email_based = False mock_cognito.confirm_sign_up.side_effect = aws_error('SomeError') with pytest.raises(auth_api.CloudError): auth_api.confirm_register(cloud, '123456', 'email@home-assistant.io') +def test_resend_email_confirm(mock_cognito): + """Test starting forgot password flow.""" + cloud = MagicMock() + auth_api.resend_email_confirm(cloud, 'email@home-assistant.io') + assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 1 + + +def test_resend_email_confirm_fails(mock_cognito): + """Test failure when starting forgot password flow.""" + cloud = MagicMock() + mock_cognito.client.resend_confirmation_code.side_effect = \ + aws_error('SomeError') + with pytest.raises(auth_api.CloudError): + auth_api.resend_email_confirm(cloud, 'email@home-assistant.io') + + def test_forgot_password(mock_cognito): """Test starting forgot password flow.""" cloud = MagicMock() - cloud.cognito_email_based = False auth_api.forgot_password(cloud, 'email@home-assistant.io') assert len(mock_cognito.initiate_forgot_password.mock_calls) == 1 @@ -130,7 +138,6 @@ def test_forgot_password(mock_cognito): def test_forgot_password_fails(mock_cognito): """Test failure when starting forgot password flow.""" cloud = MagicMock() - cloud.cognito_email_based = False mock_cognito.initiate_forgot_password.side_effect = aws_error('SomeError') with pytest.raises(auth_api.CloudError): auth_api.forgot_password(cloud, 'email@home-assistant.io') @@ -139,7 +146,6 @@ def test_forgot_password_fails(mock_cognito): def test_confirm_forgot_password(mock_cognito): """Test confirming forgot password.""" cloud = MagicMock() - cloud.cognito_email_based = False auth_api.confirm_forgot_password( cloud, '123456', 'email@home-assistant.io', 'new password') assert len(mock_cognito.confirm_forgot_password.mock_calls) == 1 @@ -152,7 +158,6 @@ def test_confirm_forgot_password(mock_cognito): def test_confirm_forgot_password_fails(mock_cognito): """Test failure when confirming forgot password.""" cloud = MagicMock() - cloud.cognito_email_based = False mock_cognito.confirm_forgot_password.side_effect = aws_error('SomeError') with pytest.raises(auth_api.CloudError): auth_api.confirm_forgot_password( diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 423ca1092eb..7623b25d401 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -14,7 +14,8 @@ from tests.common import mock_coro @pytest.fixture def cloud_client(hass, test_client): """Fixture that can fetch from the cloud client.""" - with patch('homeassistant.components.cloud.Cloud.initialize'): + with patch('homeassistant.components.cloud.Cloud.initialize', + return_value=mock_coro(True)): hass.loop.run_until_complete(async_setup_component(hass, 'cloud', { 'cloud': { 'mode': 'development', @@ -24,6 +25,8 @@ def cloud_client(hass, test_client): 'relayer': 'relayer', } })) + hass.data['cloud']._decode_claims = \ + lambda token: jwt.get_unverified_claims(token) with patch('homeassistant.components.cloud.Cloud.write_user_info'): yield hass.loop.run_until_complete(test_client(hass.http.app)) @@ -315,6 +318,48 @@ def test_forgot_password_view_unknown_error(mock_cognito, cloud_client): assert req.status == 502 +@asyncio.coroutine +def test_resend_confirm_view(mock_cognito, cloud_client): + """Test logging out.""" + req = yield from cloud_client.post('/api/cloud/resend_confirm', json={ + 'email': 'hello@bla.com', + }) + assert req.status == 200 + assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 1 + + +@asyncio.coroutine +def test_resend_confirm_view_bad_data(mock_cognito, cloud_client): + """Test logging out.""" + req = yield from cloud_client.post('/api/cloud/resend_confirm', json={ + 'not_email': 'hello@bla.com', + }) + assert req.status == 400 + assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 0 + + +@asyncio.coroutine +def test_resend_confirm_view_request_timeout(mock_cognito, cloud_client): + """Test timeout while logging out.""" + mock_cognito.client.resend_confirmation_code.side_effect = \ + asyncio.TimeoutError + req = yield from cloud_client.post('/api/cloud/resend_confirm', json={ + 'email': 'hello@bla.com', + }) + assert req.status == 502 + + +@asyncio.coroutine +def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client): + """Test unknown error while logging out.""" + mock_cognito.client.resend_confirmation_code.side_effect = \ + auth_api.UnknownError + req = yield from cloud_client.post('/api/cloud/resend_confirm', json={ + 'email': 'hello@bla.com', + }) + assert req.status == 502 + + @asyncio.coroutine def test_confirm_forgot_password_view(mock_cognito, cloud_client): """Test logging out.""" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index c5bb6f7fda7..7d23d9faad4 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -3,7 +3,6 @@ import asyncio import json from unittest.mock import patch, MagicMock, mock_open -from jose import jwt import pytest from homeassistant.components import cloud @@ -31,7 +30,8 @@ def test_constructor_loads_info_from_constant(): 'region': 'test-region', 'relayer': 'test-relayer', } - }): + }), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', + return_value=mock_coro(True)): result = yield from cloud.async_setup(hass, { 'cloud': {cloud.CONF_MODE: 'beer'} }) @@ -50,15 +50,17 @@ def test_constructor_loads_info_from_config(): """Test non-dev mode loads info from SERVERS constant.""" hass = MagicMock(data={}) - result = yield from cloud.async_setup(hass, { - 'cloud': { - cloud.CONF_MODE: cloud.MODE_DEV, - 'cognito_client_id': 'test-cognito_client_id', - 'user_pool_id': 'test-user_pool_id', - 'region': 'test-region', - 'relayer': 'test-relayer', - } - }) + with patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', + return_value=mock_coro(True)): + result = yield from cloud.async_setup(hass, { + 'cloud': { + cloud.CONF_MODE: cloud.MODE_DEV, + 'cognito_client_id': 'test-cognito_client_id', + 'user_pool_id': 'test-user_pool_id', + 'region': 'test-region', + 'relayer': 'test-relayer', + } + }) assert result cl = hass.data['cloud'] @@ -79,12 +81,13 @@ def test_initialize_loads_info(mock_os, hass): 'refresh_token': 'test-refresh-token', })) - cl = cloud.Cloud(hass, cloud.MODE_DEV) + cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None) cl.iot = MagicMock() cl.iot.connect.return_value = mock_coro() - with patch('homeassistant.components.cloud.open', mopen, create=True): - yield from cl.initialize() + with patch('homeassistant.components.cloud.open', mopen, create=True), \ + patch('homeassistant.components.cloud.Cloud._decode_claims'): + cl._start_cloud(None) assert cl.id_token == 'test-id-token' assert cl.access_token == 'test-access-token' @@ -95,7 +98,7 @@ def test_initialize_loads_info(mock_os, hass): @asyncio.coroutine def test_logout_clears_info(mock_os, hass): """Test logging out disconnects and removes info.""" - cl = cloud.Cloud(hass, cloud.MODE_DEV) + cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None) cl.iot = MagicMock() cl.iot.disconnect.return_value = mock_coro() @@ -113,7 +116,7 @@ def test_write_user_info(): """Test writing user info works.""" mopen = mock_open() - cl = cloud.Cloud(MagicMock(), cloud.MODE_DEV) + cl = cloud.Cloud(MagicMock(), cloud.MODE_DEV, None, None) cl.id_token = 'test-id-token' cl.access_token = 'test-access-token' cl.refresh_token = 'test-refresh-token' @@ -135,24 +138,24 @@ def test_write_user_info(): @asyncio.coroutine def test_subscription_expired(): """Test subscription being expired.""" - cl = cloud.Cloud(None, cloud.MODE_DEV) - cl.id_token = jwt.encode({ + cl = cloud.Cloud(None, cloud.MODE_DEV, None, None) + token_val = { 'custom:sub-exp': '2017-11-13' - }, 'test') - - with patch('homeassistant.util.dt.utcnow', - return_value=utcnow().replace(year=2018)): + } + with patch.object(cl, '_decode_claims', return_value=token_val), \ + patch('homeassistant.util.dt.utcnow', + return_value=utcnow().replace(year=2018)): assert cl.subscription_expired @asyncio.coroutine def test_subscription_not_expired(): """Test subscription not being expired.""" - cl = cloud.Cloud(None, cloud.MODE_DEV) - cl.id_token = jwt.encode({ + cl = cloud.Cloud(None, cloud.MODE_DEV, None, None) + token_val = { 'custom:sub-exp': '2017-11-13' - }, 'test') - - with patch('homeassistant.util.dt.utcnow', - return_value=utcnow().replace(year=2017, month=11, day=9)): + } + with patch.object(cl, '_decode_claims', return_value=token_val), \ + patch('homeassistant.util.dt.utcnow', + return_value=utcnow().replace(year=2017, month=11, day=9)): assert not cl.subscription_expired diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index be5a93c9e47..529559f56af 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -5,7 +5,9 @@ from unittest.mock import patch, MagicMock, PropertyMock from aiohttp import WSMsgType, client_exceptions import pytest +from homeassistant.setup import async_setup_component from homeassistant.components.cloud import iot, auth_api +from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro @@ -254,3 +256,97 @@ def test_refresh_token_before_expiration_fails(hass, mock_cloud): assert len(mock_check_token.mock_calls) == 1 assert len(mock_create.mock_calls) == 1 + + +@asyncio.coroutine +def test_handler_alexa(hass): + """Test handler Alexa.""" + hass.states.async_set( + 'switch.test', 'on', {'friendly_name': "Test switch"}) + hass.states.async_set( + 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) + + with patch('homeassistant.components.cloud.Cloud.initialize', + return_value=mock_coro(True)): + setup = yield from async_setup_component(hass, 'cloud', { + 'cloud': { + 'alexa': { + 'filter': { + 'exclude_entities': 'switch.test2' + }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'description': 'Config description', + 'display_categories': 'LIGHT' + } + } + } + } + }) + assert setup + + resp = yield from iot.async_handle_alexa( + hass, hass.data['cloud'], + test_alexa.get_new_request('Alexa.Discovery', 'Discover')) + + endpoints = resp['event']['payload']['endpoints'] + + assert len(endpoints) == 1 + device = endpoints[0] + + assert device['description'] == 'Config description' + assert device['friendlyName'] == 'Config name' + assert device['displayCategories'] == ['LIGHT'] + assert device['manufacturerName'] == 'Home Assistant' + + +@asyncio.coroutine +def test_handler_google_actions(hass): + """Test handler Google Actions.""" + hass.states.async_set( + 'switch.test', 'on', {'friendly_name': "Test switch"}) + hass.states.async_set( + 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) + + with patch('homeassistant.components.cloud.Cloud.initialize', + return_value=mock_coro(True)): + setup = yield from async_setup_component(hass, 'cloud', { + 'cloud': { + 'google_actions': { + 'filter': { + 'exclude_entities': 'switch.test2' + }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'type': 'light', + 'aliases': 'Config alias' + } + } + } + } + }) + assert setup + + reqid = '5711642932632160983' + data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]} + + with patch('homeassistant.components.cloud.Cloud._decode_claims', + return_value={'cognito:username': 'myUserName'}): + resp = yield from iot.async_handle_google_actions( + hass, hass.data['cloud'], data) + + assert resp['requestId'] == reqid + payload = resp['payload'] + + assert payload['agentUserId'] == 'myUserName' + + devices = payload['devices'] + assert len(devices) == 1 + + device = devices[0] + assert device['id'] == 'switch.test' + assert device['name']['name'] == 'Config name' + assert device['name']['nicknames'] == ['Config alias'] + assert device['type'] == 'action.devices.types.LIGHT' diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index a6827d165cd..0159eec2eff 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -144,7 +144,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): update_mock.start() self.addCleanup(update_mock.stop) asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) - asuswrt.connection.get_result() + asuswrt.connection.run_command('ls') self.assertEqual(ssh.login.call_count, 1) self.assertEqual( ssh.login.call_args, @@ -170,7 +170,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): update_mock.start() self.addCleanup(update_mock.stop) asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) - asuswrt.connection.get_result() + asuswrt.connection.run_command('ls') self.assertEqual(ssh.login.call_count, 1) self.assertEqual( ssh.login.call_args, @@ -225,9 +225,9 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): update_mock.start() self.addCleanup(update_mock.stop) asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) - asuswrt.connection.get_result() - self.assertEqual(telnet.read_until.call_count, 5) - self.assertEqual(telnet.write.call_count, 4) + asuswrt.connection.run_command('ls') + self.assertEqual(telnet.read_until.call_count, 4) + self.assertEqual(telnet.write.call_count, 3) self.assertEqual( telnet.read_until.call_args_list[0], mock.call(b'login: ') diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 34c7ecf465d..78813d9ff0b 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -345,6 +345,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): CONF_PLATFORM: 'test', device_tracker.CONF_CONSIDER_HOME: 59, }}) + self.hass.block_till_done() self.assertEqual(STATE_HOME, self.hass.states.get('device_tracker.dev1').state) @@ -586,6 +587,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): CONF_PLATFORM: 'test', device_tracker.CONF_CONSIDER_HOME: 59, }}) + self.hass.block_till_done() state = self.hass.states.get('device_tracker.dev1') attrs = state.attributes @@ -675,6 +677,30 @@ class TestComponentsDeviceTracker(unittest.TestCase): assert len(config) == 1 self.assertTrue(config[0].hidden) + def test_backward_compatibility_for_track_new(self): + """Test backward compatibility for track new.""" + tracker = device_tracker.DeviceTracker( + self.hass, timedelta(seconds=60), False, + {device_tracker.CONF_TRACK_NEW: True}, []) + tracker.see(dev_id=13) + self.hass.block_till_done() + config = device_tracker.load_config(self.yaml_devices, self.hass, + timedelta(seconds=0)) + assert len(config) == 1 + self.assertFalse(config[0].track) + + def test_old_style_track_new_is_skipped(self): + """Test old style config is skipped.""" + tracker = device_tracker.DeviceTracker( + self.hass, timedelta(seconds=60), None, + {device_tracker.CONF_TRACK_NEW: False}, []) + tracker.see(dev_id=14) + self.hass.block_till_done() + config = device_tracker.load_config(self.yaml_devices, self.hass, + timedelta(seconds=0)) + assert len(config) == 1 + self.assertFalse(config[0].track) + @asyncio.coroutine def test_async_added_to_hass(hass): diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 4f5efb9d09d..5f1f29e7697 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -35,6 +35,9 @@ CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_WAYPOINT_IMPORT = owntracks.CONF_WAYPOINT_IMPORT CONF_WAYPOINT_WHITELIST = owntracks.CONF_WAYPOINT_WHITELIST CONF_SECRET = owntracks.CONF_SECRET +CONF_MQTT_TOPIC = owntracks.CONF_MQTT_TOPIC +CONF_EVENTS_ONLY = owntracks.CONF_EVENTS_ONLY +CONF_REGION_MAPPING = owntracks.CONF_REGION_MAPPING TEST_ZONE_LAT = 45.0 TEST_ZONE_LON = 90.0 @@ -179,6 +182,13 @@ REGION_GPS_LEAVE_MESSAGE_OUTER = build_message( 'event': 'leave'}, DEFAULT_TRANSITION_MESSAGE) +REGION_GPS_ENTER_MESSAGE_OUTER = build_message( + {'lon': OUTER_ZONE['longitude'], + 'lat': OUTER_ZONE['latitude'], + 'desc': 'outer', + 'event': 'enter'}, + DEFAULT_TRANSITION_MESSAGE) + # Region Beacon messages REGION_BEACON_ENTER_MESSAGE = DEFAULT_BEACON_TRANSITION_MESSAGE @@ -616,6 +626,46 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): self.send_message(EVENT_TOPIC, message) self.assert_location_state('inner') + def test_events_only_on(self): + """Test events_only config suppresses location updates.""" + # Sending a location message that is not home + self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE_NOT_HOME) + self.assert_location_state(STATE_NOT_HOME) + + self.context.events_only = True + + # Enter and Leave messages + self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE_OUTER) + self.assert_location_state('outer') + self.send_message(EVENT_TOPIC, REGION_GPS_LEAVE_MESSAGE_OUTER) + self.assert_location_state(STATE_NOT_HOME) + + # Sending a location message that is inside outer zone + self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE) + + # Ignored location update. Location remains at previous. + self.assert_location_state(STATE_NOT_HOME) + + def test_events_only_off(self): + """Test when events_only is False.""" + # Sending a location message that is not home + self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE_NOT_HOME) + self.assert_location_state(STATE_NOT_HOME) + + self.context.events_only = False + + # Enter and Leave messages + self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE_OUTER) + self.assert_location_state('outer') + self.send_message(EVENT_TOPIC, REGION_GPS_LEAVE_MESSAGE_OUTER) + self.assert_location_state(STATE_NOT_HOME) + + # Sending a location message that is inside outer zone + self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE) + + # Location update processed + self.assert_location_state('outer') + # Region Beacon based event entry / exit testing def test_event_region_entry_exit(self): @@ -1111,7 +1161,8 @@ class TestDeviceTrackerOwnTracks(BaseMQTT): test_config = { CONF_PLATFORM: 'owntracks', CONF_MAX_GPS_ACCURACY: 200, - CONF_WAYPOINT_IMPORT: True + CONF_WAYPOINT_IMPORT: True, + CONF_MQTT_TOPIC: 'owntracks/#', } run_coroutine_threadsafe(owntracks.async_setup_scanner( self.hass, test_config, mock_see), self.hass.loop).result() @@ -1353,3 +1404,37 @@ class TestDeviceTrackerOwnTrackConfigs(BaseMQTT): self.send_message(LOCATION_TOPIC, ENCRYPTED_LOCATION_MESSAGE) self.assert_location_latitude(LOCATION_MESSAGE['lat']) + + def test_customized_mqtt_topic(self): + """Test subscribing to a custom mqtt topic.""" + with assert_setup_component(1, device_tracker.DOMAIN): + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'owntracks', + CONF_MQTT_TOPIC: 'mytracks/#', + }}) + + topic = 'mytracks/{}/{}'.format(USER, DEVICE) + + self.send_message(topic, LOCATION_MESSAGE) + self.assert_location_latitude(LOCATION_MESSAGE['lat']) + + def test_region_mapping(self): + """Test region to zone mapping.""" + with assert_setup_component(1, device_tracker.DOMAIN): + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'owntracks', + CONF_REGION_MAPPING: { + 'foo': 'inner' + }, + }}) + + self.hass.states.set( + 'zone.inner', 'zoning', INNER_ZONE) + + message = build_message({'desc': 'foo'}, REGION_GPS_ENTER_MESSAGE) + self.assertEqual(message['desc'], 'foo') + + self.send_message(EVENT_TOPIC, message) + self.assert_location_state('inner') diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py old mode 100755 new mode 100644 diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py new file mode 100644 index 00000000000..3846887f21c --- /dev/null +++ b/tests/components/fan/test_mqtt.py @@ -0,0 +1,64 @@ +"""Test MQTT fans.""" +import unittest + +from homeassistant.setup import setup_component +from homeassistant.components import fan +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE + +from tests.common import ( + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + + +class TestMqttFan(unittest.TestCase): + """Test the MQTT fan platform.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """"Stop everything that was started.""" + self.hass.stop() + + def test_custom_availability_payload(self): + """Test the availability payload.""" + assert setup_component(self.hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability_topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) + + state = self.hass.states.get('fan.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability_topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'availability_topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '1') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability_topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('fan.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index bcb12c70b58..eb8d17a83aa 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -95,7 +95,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], 'type': - 'action.devices.types.LIGHT', + 'action.devices.types.SWITCH', 'willReportState': False }, { @@ -107,7 +107,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], 'type': - 'action.devices.types.LIGHT', + 'action.devices.types.SWITCH', 'willReportState': False }, { @@ -116,7 +116,7 @@ DEMO_DEVICES = [{ 'name': 'Garage Door' }, 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.LIGHT', + 'type': 'action.devices.types.SWITCH', 'willReportState': False }, { 'id': 'cover.kitchen_window', @@ -124,7 +124,7 @@ DEMO_DEVICES = [{ 'name': 'Kitchen Window' }, 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.LIGHT', + 'type': 'action.devices.types.SWITCH', 'willReportState': False }, { 'id': 'group.all_covers', @@ -143,7 +143,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], 'type': - 'action.devices.types.LIGHT', + 'action.devices.types.SWITCH', 'willReportState': False }, { @@ -155,7 +155,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], 'type': - 'action.devices.types.LIGHT', + 'action.devices.types.SWITCH', 'willReportState': False }, { @@ -164,7 +164,7 @@ DEMO_DEVICES = [{ 'name': 'Lounge room' }, 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.LIGHT', + 'type': 'action.devices.types.SWITCH', 'willReportState': False }, { 'id': @@ -175,7 +175,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], 'type': - 'action.devices.types.LIGHT', + 'action.devices.types.SWITCH', 'willReportState': False }, { diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 05178649c88..3b9ad7f3ef7 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -5,43 +5,50 @@ import json from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION import pytest -from tests.common import get_test_instance_port from homeassistant import core, const, setup from homeassistant.components import ( - fan, http, cover, light, switch, climate, async_setup, media_player) + fan, cover, light, switch, climate, async_setup, media_player) from homeassistant.components import google_assistant as ga from homeassistant.util.unit_system import IMPERIAL_SYSTEM from . import DEMO_DEVICES API_PASSWORD = "test1234" -SERVER_PORT = get_test_instance_port() -BASE_API_URL = "http://127.0.0.1:{}".format(SERVER_PORT) HA_HEADERS = { const.HTTP_HEADER_HA_AUTH: API_PASSWORD, CONTENT_TYPE: const.CONTENT_TYPE_JSON, } -AUTHCFG = { - 'project_id': 'hasstest-1234', - 'client_id': 'helloworld', - 'access_token': 'superdoublesecret' -} -AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(AUTHCFG['access_token'])} +PROJECT_ID = 'hasstest-1234' +CLIENT_ID = 'helloworld' +ACCESS_TOKEN = 'superdoublesecret' +AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)} @pytest.fixture -def assistant_client(loop, hass_fixture, test_client): +def assistant_client(loop, hass, test_client): """Create web client for the Google Assistant API.""" - hass = hass_fixture - web_app = hass.http.app + loop.run_until_complete( + setup.async_setup_component(hass, 'google_assistant', { + 'google_assistant': { + 'project_id': PROJECT_ID, + 'client_id': CLIENT_ID, + 'access_token': ACCESS_TOKEN, + 'entity_config': { + 'light.ceiling_lights': { + 'aliases': ['top lights', 'ceiling lights'], + 'name': 'Roof Lights', + }, + 'switch.decorative_lights': { + 'type': 'light' + } + } + } + })) - ga.http.GoogleAssistantView(hass, AUTHCFG).register(web_app.router) - ga.auth.GoogleAssistantAuthView(hass, AUTHCFG).register(web_app.router) - - return loop.run_until_complete(test_client(web_app)) + return loop.run_until_complete(test_client(hass.http.app)) @pytest.fixture @@ -50,13 +57,6 @@ def hass_fixture(loop, hass): # We need to do this to get access to homeassistant/turn_(on,off) loop.run_until_complete(async_setup(hass, {core.DOMAIN: {}})) - loop.run_until_complete( - setup.async_setup_component(hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_SERVER_PORT: SERVER_PORT - } - })) - loop.run_until_complete( setup.async_setup_component(hass, light.DOMAIN, { 'light': [{ @@ -97,44 +97,24 @@ def hass_fixture(loop, hass): }] })) - # Kitchen light is explicitly excluded from being exposed - ceiling_lights_entity = hass.states.get('light.ceiling_lights') - attrs = dict(ceiling_lights_entity.attributes) - attrs[ga.const.ATTR_GOOGLE_ASSISTANT_NAME] = "Roof Lights" - attrs[ga.const.CONF_ALIASES] = ['top lights', 'ceiling lights'] - hass.states.async_set( - ceiling_lights_entity.entity_id, - ceiling_lights_entity.state, - attributes=attrs) - - # By setting the google_assistant_type = 'light' - # we can override how a device is reported to GA - switch_light = hass.states.get('switch.decorative_lights') - attrs = dict(switch_light.attributes) - attrs[ga.const.ATTR_GOOGLE_ASSISTANT_TYPE] = "light" - hass.states.async_set( - switch_light.entity_id, - switch_light.state, - attributes=attrs) - return hass @asyncio.coroutine -def test_auth(hass_fixture, assistant_client): +def test_auth(assistant_client): """Test the auth process.""" result = yield from assistant_client.get( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT + '/auth', params={ 'redirect_uri': - 'http://testurl/r/{}'.format(AUTHCFG['project_id']), - 'client_id': AUTHCFG['client_id'], + 'http://testurl/r/{}'.format(PROJECT_ID), + 'client_id': CLIENT_ID, 'state': 'random1234', }, allow_redirects=False) assert result.status == 301 loc = result.headers.get('Location') - assert AUTHCFG['access_token'] in loc + assert ACCESS_TOKEN in loc @asyncio.coroutine @@ -167,9 +147,6 @@ def test_sync_request(hass_fixture, assistant_client): @asyncio.coroutine def test_query_request(hass_fixture, assistant_client): """Test a query request.""" - # hass.states.set("light.bedroom", "on") - # hass.states.set("switch.outside", "off") - # res = _sync_req() reqid = '5711642932632160984' data = { 'requestId': @@ -301,9 +278,6 @@ def test_query_climate_request_f(hass_fixture, assistant_client): @asyncio.coroutine def test_execute_request(hass_fixture, assistant_client): """Test a execute request.""" - # hass.states.set("light.bedroom", "on") - # hass.states.set("switch.outside", "off") - # res = _sync_req() reqid = '5711642932632160985' data = { 'requestId': diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 2668c0cecfc..bb8f1b706e6 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -179,16 +179,6 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness }] -@asyncio.coroutine -def test_make_actions_response(): - """Test make response helper.""" - reqid = 1234 - payload = 'hello' - result = ga.smart_home.make_actions_response(reqid, payload) - assert result['requestId'] == reqid - assert result['payload'] == payload - - @asyncio.coroutine def test_determine_service(): """Test all branches of determine service.""" diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 7371ecf6e56..07dda7ff3b2 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import patch from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN, ATTR_ICON, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, STATE_NOT_HOME) + ATTR_ASSUMED_STATE, STATE_NOT_HOME, ATTR_FRIENDLY_NAME) import homeassistant.components.group as group from tests.common import get_test_home_assistant, assert_setup_component @@ -395,6 +395,29 @@ class TestComponentsGroup(unittest.TestCase): group_state = self.hass.states.get(group_entity_id) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) + def test_modify_group(self): + """Test modifying a group.""" + group_conf = OrderedDict() + group_conf['modify_group'] = { + 'name': 'friendly_name', + 'icon': 'mdi:work' + } + + assert setup_component(self.hass, 'group', {'group': group_conf}) + + # The old way would create a new group modify_group1 because + # internally it didn't know anything about those created in the config + group.set_group(self.hass, 'modify_group', icon="mdi:play") + self.hass.block_till_done() + + group_state = self.hass.states.get( + group.ENTITY_ID_FORMAT.format('modify_group')) + + assert self.hass.states.entity_ids() == ['group.modify_group'] + assert group_state.attributes.get(ATTR_ICON) == 'mdi:play' + assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == \ + 'friendly_name' + @asyncio.coroutine def test_service_group_services(hass): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 7955cecba04..611f1240d45 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -12,6 +12,8 @@ from tests.common import get_test_home_assistant, MockDependency _LOGGER = logging.getLogger(__name__) +HUE_LIGHT_NS = 'homeassistant.components.light.hue.' + class TestSetup(unittest.TestCase): """Test the Hue light platform.""" @@ -29,12 +31,10 @@ class TestSetup(unittest.TestCase): def setup_mocks_for_update_lights(self): """Set up all mocks for update_lights tests.""" self.mock_bridge = MagicMock() + self.mock_bridge.bridge_id = 'bridge-id' self.mock_bridge.allow_hue_groups = False self.mock_api = MagicMock() self.mock_bridge.get_api.return_value = self.mock_api - self.mock_bridge_type = MagicMock() - self.mock_lights = [] - self.mock_groups = [] self.mock_add_devices = MagicMock() def setup_mocks_for_process_lights(self): @@ -43,7 +43,6 @@ class TestSetup(unittest.TestCase): self.mock_api = MagicMock() self.mock_api.get.return_value = {} self.mock_bridge.get_api.return_value = self.mock_api - self.mock_bridge_type = MagicMock() def setup_mocks_for_process_groups(self): """Set up all mocks for process_groups tests.""" @@ -55,11 +54,10 @@ class TestSetup(unittest.TestCase): self.mock_api.get.return_value = {} self.mock_bridge.get_api.return_value = self.mock_api - self.mock_bridge_type = MagicMock() - def create_mock_bridge(self, host, allow_hue_groups=True): """Return a mock HueBridge with reasonable defaults.""" mock_bridge = MagicMock() + mock_bridge.bridge_id = 'bridge-id' mock_bridge.host = host mock_bridge.allow_hue_groups = allow_hue_groups mock_bridge.lights = {} @@ -76,6 +74,14 @@ class TestSetup(unittest.TestCase): return mock_bridge_lights + def build_mock_light(self, bridge, light_id, name): + """Return a mock HueLight.""" + light = MagicMock() + light.bridge = bridge + light.light_id = light_id + light.name = name + return light + def test_setup_platform_no_discovery_info(self): """Test setup_platform without discovery info.""" self.hass.data[hue.DOMAIN] = {} @@ -100,8 +106,8 @@ class TestSetup(unittest.TestCase): self.hass.data[hue.DOMAIN] = {'10.0.0.1': mock_bridge} mock_add_devices = MagicMock() - with patch('homeassistant.components.light.hue.' + - 'unthrottled_update_lights') as mock_update_lights: + with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ + as mock_update_lights: hue_light.setup_platform( self.hass, {}, mock_add_devices, {'bridge_id': '10.0.0.1'}) @@ -118,8 +124,8 @@ class TestSetup(unittest.TestCase): } mock_add_devices = MagicMock() - with patch('homeassistant.components.light.hue.' + - 'unthrottled_update_lights') as mock_update_lights: + with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ + as mock_update_lights: hue_light.setup_platform( self.hass, {}, mock_add_devices, {'bridge_id': '10.0.0.1'}) @@ -137,97 +143,105 @@ class TestSetup(unittest.TestCase): """Test the update_lights function when no lights are found.""" self.setup_mocks_for_update_lights() - with patch('homeassistant.components.light.hue.get_bridge_type', - return_value=self.mock_bridge_type): - with patch('homeassistant.components.light.hue.process_lights', - return_value=[]) as mock_process_lights: - with patch('homeassistant.components.light.hue.process_groups', - return_value=self.mock_groups) \ - as mock_process_groups: + with patch(HUE_LIGHT_NS + 'process_lights', return_value=[]) \ + as mock_process_lights: + with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ + as mock_process_groups: + with patch.object(self.hass.helpers.dispatcher, + 'dispatcher_send') as dispatcher_send: hue_light.unthrottled_update_lights( self.hass, self.mock_bridge, self.mock_add_devices) mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, - self.mock_bridge_type, mock.ANY) + self.hass, self.mock_api, self.mock_bridge, mock.ANY) mock_process_groups.assert_not_called() self.mock_add_devices.assert_not_called() + dispatcher_send.assert_not_called() @MockDependency('phue') def test_update_lights_with_some_lights(self, mock_phue): """Test the update_lights function with some lights.""" self.setup_mocks_for_update_lights() - self.mock_lights = ['some', 'light'] + mock_lights = [ + self.build_mock_light(self.mock_bridge, 42, 'some'), + self.build_mock_light(self.mock_bridge, 84, 'light'), + ] - with patch('homeassistant.components.light.hue.get_bridge_type', - return_value=self.mock_bridge_type): - with patch('homeassistant.components.light.hue.process_lights', - return_value=self.mock_lights) as mock_process_lights: - with patch('homeassistant.components.light.hue.process_groups', - return_value=self.mock_groups) \ - as mock_process_groups: + with patch(HUE_LIGHT_NS + 'process_lights', + return_value=mock_lights) as mock_process_lights: + with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ + as mock_process_groups: + with patch.object(self.hass.helpers.dispatcher, + 'dispatcher_send') as dispatcher_send: hue_light.unthrottled_update_lights( self.hass, self.mock_bridge, self.mock_add_devices) mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, - self.mock_bridge_type, mock.ANY) + self.hass, self.mock_api, self.mock_bridge, mock.ANY) mock_process_groups.assert_not_called() self.mock_add_devices.assert_called_once_with( - self.mock_lights) + mock_lights) + dispatcher_send.assert_not_called() @MockDependency('phue') def test_update_lights_no_groups(self, mock_phue): """Test the update_lights function when no groups are found.""" self.setup_mocks_for_update_lights() self.mock_bridge.allow_hue_groups = True - self.mock_lights = ['some', 'light'] + mock_lights = [ + self.build_mock_light(self.mock_bridge, 42, 'some'), + self.build_mock_light(self.mock_bridge, 84, 'light'), + ] - with patch('homeassistant.components.light.hue.get_bridge_type', - return_value=self.mock_bridge_type): - with patch('homeassistant.components.light.hue.process_lights', - return_value=self.mock_lights) as mock_process_lights: - with patch('homeassistant.components.light.hue.process_groups', - return_value=self.mock_groups) \ - as mock_process_groups: + with patch(HUE_LIGHT_NS + 'process_lights', + return_value=mock_lights) as mock_process_lights: + with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ + as mock_process_groups: + with patch.object(self.hass.helpers.dispatcher, + 'dispatcher_send') as dispatcher_send: hue_light.unthrottled_update_lights( self.hass, self.mock_bridge, self.mock_add_devices) mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, - self.mock_bridge_type, mock.ANY) + self.hass, self.mock_api, self.mock_bridge, mock.ANY) mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, - self.mock_bridge_type, mock.ANY) + self.hass, self.mock_api, self.mock_bridge, mock.ANY) self.mock_add_devices.assert_called_once_with( - self.mock_lights) + mock_lights) + dispatcher_send.assert_not_called() @MockDependency('phue') def test_update_lights_with_lights_and_groups(self, mock_phue): """Test the update_lights function with both lights and groups.""" self.setup_mocks_for_update_lights() self.mock_bridge.allow_hue_groups = True - self.mock_lights = ['some', 'light'] - self.mock_groups = ['and', 'groups'] + mock_lights = [ + self.build_mock_light(self.mock_bridge, 42, 'some'), + self.build_mock_light(self.mock_bridge, 84, 'light'), + ] + mock_groups = [ + self.build_mock_light(self.mock_bridge, 15, 'and'), + self.build_mock_light(self.mock_bridge, 72, 'groups'), + ] - with patch('homeassistant.components.light.hue.get_bridge_type', - return_value=self.mock_bridge_type): - with patch('homeassistant.components.light.hue.process_lights', - return_value=self.mock_lights) as mock_process_lights: - with patch('homeassistant.components.light.hue.process_groups', - return_value=self.mock_groups) \ - as mock_process_groups: + with patch(HUE_LIGHT_NS + 'process_lights', + return_value=mock_lights) as mock_process_lights: + with patch(HUE_LIGHT_NS + 'process_groups', + return_value=mock_groups) as mock_process_groups: + with patch.object(self.hass.helpers.dispatcher, + 'dispatcher_send') as dispatcher_send: hue_light.unthrottled_update_lights( self.hass, self.mock_bridge, self.mock_add_devices) mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, - self.mock_bridge_type, mock.ANY) + self.hass, self.mock_api, self.mock_bridge, mock.ANY) mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, - self.mock_bridge_type, mock.ANY) + self.hass, self.mock_api, self.mock_bridge, mock.ANY) + # note that mock_lights has been modified in place and + # now contains both lights and groups self.mock_add_devices.assert_called_once_with( - self.mock_lights) + mock_lights) + dispatcher_send.assert_not_called() @MockDependency('phue') def test_update_lights_with_two_bridges(self, mock_phue): @@ -242,23 +256,21 @@ class TestSetup(unittest.TestCase): mock_bridge_two_lights = self.create_mock_lights( {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}}) - with patch('homeassistant.components.light.hue.get_bridge_type', - return_value=self.mock_bridge_type): - with patch('homeassistant.components.light.hue.HueLight.' - 'schedule_update_ha_state'): - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_one_lights - with patch.object(mock_bridge_one, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_one, self.mock_add_devices) + with patch('homeassistant.components.light.hue.HueLight.' + 'schedule_update_ha_state'): + mock_api = MagicMock() + mock_api.get.return_value = mock_bridge_one_lights + with patch.object(mock_bridge_one, 'get_api', + return_value=mock_api): + hue_light.unthrottled_update_lights( + self.hass, mock_bridge_one, self.mock_add_devices) - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_two_lights - with patch.object(mock_bridge_two, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_two, self.mock_add_devices) + mock_api = MagicMock() + mock_api.get.return_value = mock_bridge_two_lights + with patch.object(mock_bridge_two, 'get_api', + return_value=mock_api): + hue_light.unthrottled_update_lights( + self.hass, mock_bridge_two, self.mock_add_devices) self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2]) self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3]) @@ -299,8 +311,7 @@ class TestSetup(unittest.TestCase): self.mock_api.get.return_value = None ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + self.hass, self.mock_api, self.mock_bridge, None) self.assertEquals([], ret) self.assertEquals(self.mock_bridge.lights, {}) @@ -309,38 +320,42 @@ class TestSetup(unittest.TestCase): """Test the process_lights function when bridge returns no lights.""" self.setup_mocks_for_process_lights() - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ + as mock_dispatcher_send: + ret = hue_light.process_lights( + self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) - self.assertEquals(self.mock_bridge.lights, {}) + self.assertEquals([], ret) + mock_dispatcher_send.assert_not_called() + self.assertEquals(self.mock_bridge.lights, {}) - @patch('homeassistant.components.light.hue.HueLight') + @patch(HUE_LIGHT_NS + 'HueLight') def test_process_lights_some_lights(self, mock_hue_light): """Test the process_lights function with multiple groups.""" self.setup_mocks_for_process_lights() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ + as mock_dispatcher_send: + ret = hue_light.process_lights( + self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge_type, self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge_type, self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - self.assertEquals(len(self.mock_bridge.lights), 2) + self.assertEquals(len(ret), 2) + mock_hue_light.assert_has_calls([ + call( + 1, {'state': 'on'}, self.mock_bridge, mock.ANY, + self.mock_bridge.allow_unreachable, + self.mock_bridge.allow_in_emulated_hue), + call( + 2, {'state': 'off'}, self.mock_bridge, mock.ANY, + self.mock_bridge.allow_unreachable, + self.mock_bridge.allow_in_emulated_hue), + ]) + mock_dispatcher_send.assert_not_called() + self.assertEquals(len(self.mock_bridge.lights), 2) - @patch('homeassistant.components.light.hue.HueLight') + @patch(HUE_LIGHT_NS + 'HueLight') def test_process_lights_new_light(self, mock_hue_light): """ Test the process_lights function with new groups. @@ -350,22 +365,24 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_lights() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lights = {1: MagicMock()} + self.mock_bridge.lights = { + 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ + as mock_dispatcher_send: + ret = hue_light.process_lights( + self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge_type, self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - self.assertEquals(len(self.mock_bridge.lights), 2) - self.mock_bridge.lights[1]\ - .schedule_update_ha_state.assert_called_once_with() + self.assertEquals(len(ret), 1) + mock_hue_light.assert_has_calls([ + call( + 2, {'state': 'off'}, self.mock_bridge, mock.ANY, + self.mock_bridge.allow_unreachable, + self.mock_bridge.allow_in_emulated_hue), + ]) + mock_dispatcher_send.assert_called_once_with( + 'hue_light_callback_bridge-id_1') + self.assertEquals(len(self.mock_bridge.lights), 2) def test_process_groups_api_error(self): """Test the process_groups function when the bridge errors out.""" @@ -373,8 +390,7 @@ class TestSetup(unittest.TestCase): self.mock_api.get.return_value = None ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + self.hass, self.mock_api, self.mock_bridge, None) self.assertEquals([], ret) self.assertEquals(self.mock_bridge.lightgroups, {}) @@ -384,38 +400,42 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_groups() self.mock_bridge.get_group.return_value = {'name': 'Group 0'} - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ + as mock_dispatcher_send: + ret = hue_light.process_groups( + self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals([], ret) - self.assertEquals(self.mock_bridge.lightgroups, {}) + self.assertEquals([], ret) + mock_dispatcher_send.assert_not_called() + self.assertEquals(self.mock_bridge.lightgroups, {}) - @patch('homeassistant.components.light.hue.HueLight') + @patch(HUE_LIGHT_NS + 'HueLight') def test_process_groups_some_groups(self, mock_hue_light): """Test the process_groups function with multiple groups.""" self.setup_mocks_for_process_groups() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ + as mock_dispatcher_send: + ret = hue_light.process_groups( + self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge_type, self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge_type, self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.assertEquals(len(ret), 2) + mock_hue_light.assert_has_calls([ + call( + 1, {'state': 'on'}, self.mock_bridge, mock.ANY, + self.mock_bridge.allow_unreachable, + self.mock_bridge.allow_in_emulated_hue, True), + call( + 2, {'state': 'off'}, self.mock_bridge, mock.ANY, + self.mock_bridge.allow_unreachable, + self.mock_bridge.allow_in_emulated_hue, True), + ]) + mock_dispatcher_send.assert_not_called() + self.assertEquals(len(self.mock_bridge.lightgroups), 2) - @patch('homeassistant.components.light.hue.HueLight') + @patch(HUE_LIGHT_NS + 'HueLight') def test_process_groups_new_group(self, mock_hue_light): """ Test the process_groups function with new groups. @@ -425,22 +445,24 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_groups() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lightgroups = {1: MagicMock()} + self.mock_bridge.lightgroups = { + 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, - None) + with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ + as mock_dispatcher_send: + ret = hue_light.process_groups( + self.hass, self.mock_api, self.mock_bridge, None) - self.assertEquals(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge_type, self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - self.assertEquals(len(self.mock_bridge.lightgroups), 2) - self.mock_bridge.lightgroups[1]\ - .schedule_update_ha_state.assert_called_once_with() + self.assertEquals(len(ret), 1) + mock_hue_light.assert_has_calls([ + call( + 2, {'state': 'off'}, self.mock_bridge, mock.ANY, + self.mock_bridge.allow_unreachable, + self.mock_bridge.allow_in_emulated_hue, True), + ]) + mock_dispatcher_send.assert_called_once_with( + 'hue_light_callback_bridge-id_1') + self.assertEquals(len(self.mock_bridge.lightgroups), 2) class TestHueLight(unittest.TestCase): @@ -455,7 +477,6 @@ class TestHueLight(unittest.TestCase): self.mock_info = MagicMock() self.mock_bridge = MagicMock() self.mock_update_lights = MagicMock() - self.mock_bridge_type = MagicMock() self.mock_allow_unreachable = MagicMock() self.mock_is_group = MagicMock() self.mock_allow_in_emulated_hue = MagicMock() @@ -469,6 +490,10 @@ class TestHueLight(unittest.TestCase): def buildLight( self, light_id=None, info=None, update_lights=None, is_group=None): """Helper to build a HueLight object with minimal fuss.""" + if 'state' not in info: + on_key = 'any_on' if is_group is not None else 'on' + info['state'] = {on_key: False} + return hue_light.HueLight( light_id if light_id is not None else self.light_id, info if info is not None else self.mock_info, @@ -476,7 +501,6 @@ class TestHueLight(unittest.TestCase): (update_lights if update_lights is not None else self.mock_update_lights), - self.mock_bridge_type, self.mock_allow_unreachable, self.mock_allow_in_emulated_hue, is_group if is_group is not None else self.mock_is_group) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index db7c35107d8..d6dabaf9a4f 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -142,7 +142,8 @@ import unittest from unittest import mock from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, @@ -546,17 +547,17 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', rgb_color=[255, 255, 255]) + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 64]) self.hass.block_till_done() self.mock_publish().async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 0, False), - mock.call('test_light_rgb/rgb/set', '#ffffff', 0, False), + mock.call('test_light_rgb/rgb/set', '#ff8040', 0, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 255, 255), state.attributes['rgb_color']) + self.assertEqual((255, 128, 64), state.attributes['rgb_color']) def test_show_brightness_if_only_command_topic(self): """Test the brightness if only a command topic is present.""" @@ -794,3 +795,33 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.mock_calls[-4][1]) self.assertEqual(('test_light/bright', 50, 0, False), self.mock_publish.mock_calls[-2][1]) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test_light/set', + 'brightness_command_topic': 'test_light/bright', + 'rgb_command_topic': "test_light/rgb", + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py old mode 100755 new mode 100644 index 10bb3f030e9..6bf24f595ac --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -82,7 +82,8 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import ( - STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, @@ -472,3 +473,32 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(255, state.attributes.get('white_value')) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py old mode 100755 new mode 100644 index a28d862bf53..fddb75880cc --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -29,7 +29,8 @@ If your light doesn't support RGB feature, omit `(red|green|blue)_template`. import unittest from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, @@ -145,12 +146,12 @@ class TestLightMQTTTemplate(unittest.TestCase): # turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', - 'on,255,145,123,255-255-255,') + 'on,255,145,123,255-128-64,') self.hass.block_till_done() state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual([255, 128, 64], state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(145, state.attributes.get('color_temp')) self.assertEqual(123, state.attributes.get('white_value')) @@ -463,3 +464,33 @@ class TestLightMQTTTemplate(unittest.TestCase): # effect should not have changed state = self.hass.states.get('light.test') self.assertEqual('rainbow', state.attributes.get('effect')) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index c66ed5f2b26..667908e13fa 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -2,8 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, - ATTR_ASSUMED_STATE) +from homeassistant.const import ( + STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) @@ -111,3 +111,34 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNLOCKED, state.state) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, lock.DOMAIN, { + lock.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_lock': 'LOCK', + 'payload_unlock': 'UNLOCK', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('lock.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('lock.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('lock.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py index 2bcd02e69aa..399cdc67ca6 100644 --- a/tests/components/media_player/test_monoprice.py +++ b/tests/components/media_player/test_monoprice.py @@ -1,27 +1,30 @@ """The tests for Monoprice Media player platform.""" import unittest +from unittest import mock import voluptuous as vol from collections import defaultdict - from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, + DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE) from homeassistant.const import STATE_ON, STATE_OFF +import tests.common from homeassistant.components.media_player.monoprice import ( - MonopriceZone, PLATFORM_SCHEMA) + DATA_MONOPRICE, PLATFORM_SCHEMA, SERVICE_SNAPSHOT, + SERVICE_RESTORE, setup_platform) -class MockState(object): - """Mock for zone state object.""" +class AttrDict(dict): + """Helper class for mocking attributes.""" - def __init__(self): - """Init zone state.""" - self.power = True - self.volume = 0 - self.mute = True - self.source = 1 + def __setattr__(self, name, value): + """Set attribute.""" + self[name] = value + + def __getattr__(self, item): + """Get attribute.""" + return self[item] class MockMonoprice(object): @@ -29,11 +32,16 @@ class MockMonoprice(object): def __init__(self): """Init mock object.""" - self.zones = defaultdict(lambda *a: MockState()) + self.zones = defaultdict(lambda: AttrDict(power=True, + volume=0, + mute=True, + source=1)) def zone_status(self, zone_id): """Get zone status.""" - return self.zones[zone_id] + status = self.zones[zone_id] + status.zone = zone_id + return AttrDict(status) def set_source(self, zone_id, source_idx): """Set source for zone.""" @@ -51,6 +59,10 @@ class MockMonoprice(object): """Set volume for zone.""" self.zones[zone_id].volume = volume + def restore_zone(self, zone): + """Restore zone status.""" + self.zones[zone.zone] = AttrDict(zone) + class TestMonopriceSchema(unittest.TestCase): """Test Monoprice schema.""" @@ -147,11 +159,144 @@ class TestMonopriceMediaPlayer(unittest.TestCase): def setUp(self): """Set up the test case.""" self.monoprice = MockMonoprice() + self.hass = tests.common.get_test_home_assistant() + self.hass.start() # Note, source dictionary is unsorted! - self.media_player = MonopriceZone(self.monoprice, {1: 'one', - 3: 'three', - 2: 'two'}, - 12, 'Zone name') + with mock.patch('pymonoprice.get_monoprice', + new=lambda *a: self.monoprice): + setup_platform(self.hass, { + 'platform': 'monoprice', + 'port': '/dev/ttyS0', + 'name': 'Name', + 'zones': {12: {'name': 'Zone name'}}, + 'sources': {1: {'name': 'one'}, + 3: {'name': 'three'}, + 2: {'name': 'two'}}, + }, lambda *args, **kwargs: None, {}) + self.hass.block_till_done() + self.media_player = self.hass.data[DATA_MONOPRICE][0] + self.media_player.hass = self.hass + self.media_player.entity_id = 'media_player.zone_1' + + def tearDown(self): + """Tear down the test case.""" + self.hass.stop() + + def test_setup_platform(self, *args): + """Test setting up platform.""" + # Two services must be registered + self.assertTrue(self.hass.services.has_service(DOMAIN, + SERVICE_RESTORE)) + self.assertTrue(self.hass.services.has_service(DOMAIN, + SERVICE_SNAPSHOT)) + self.assertEqual(len(self.hass.data[DATA_MONOPRICE]), 1) + self.assertEqual(self.hass.data[DATA_MONOPRICE][0].name, 'Zone name') + + def test_service_calls_with_entity_id(self): + """Test snapshot save/restore service calls.""" + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + self.assertTrue(self.media_player.is_volume_muted) + self.assertEqual('one', self.media_player.source) + + # Saving default values + self.hass.services.call(DOMAIN, SERVICE_SNAPSHOT, + {'entity_id': 'media_player.zone_1'}, + blocking=True) + # self.hass.block_till_done() + + # Changing media player to new state + self.media_player.set_volume_level(1) + self.media_player.select_source('two') + self.media_player.mute_volume(False) + self.media_player.turn_off() + + # Checking that values were indeed changed + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_OFF, self.media_player.state) + self.assertEqual(1.0, self.media_player.volume_level, 0.0001) + self.assertFalse(self.media_player.is_volume_muted) + self.assertEqual('two', self.media_player.source) + + # Restoring wrong media player to its previous state + # Nothing should be done + self.hass.services.call(DOMAIN, SERVICE_RESTORE, + {'entity_id': 'not_existing'}, + blocking=True) + # self.hass.block_till_done() + + # Checking that values were not (!) restored + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_OFF, self.media_player.state) + self.assertEqual(1.0, self.media_player.volume_level, 0.0001) + self.assertFalse(self.media_player.is_volume_muted) + self.assertEqual('two', self.media_player.source) + + # Restoring media player to its previous state + self.hass.services.call(DOMAIN, SERVICE_RESTORE, + {'entity_id': 'media_player.zone_1'}, + blocking=True) + self.hass.block_till_done() + + # Checking that values were restored + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + self.assertTrue(self.media_player.is_volume_muted) + self.assertEqual('one', self.media_player.source) + + def test_service_calls_without_entity_id(self): + """Test snapshot save/restore service calls.""" + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + self.assertTrue(self.media_player.is_volume_muted) + self.assertEqual('one', self.media_player.source) + + # Restoring media player + # since there is no snapshot, nothing should be done + self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True) + self.hass.block_till_done() + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + self.assertTrue(self.media_player.is_volume_muted) + self.assertEqual('one', self.media_player.source) + + # Saving default values + self.hass.services.call(DOMAIN, SERVICE_SNAPSHOT, blocking=True) + self.hass.block_till_done() + + # Changing media player to new state + self.media_player.set_volume_level(1) + self.media_player.select_source('two') + self.media_player.mute_volume(False) + self.media_player.turn_off() + + # Checking that values were indeed changed + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_OFF, self.media_player.state) + self.assertEqual(1.0, self.media_player.volume_level, 0.0001) + self.assertFalse(self.media_player.is_volume_muted) + self.assertEqual('two', self.media_player.source) + + # Restoring media player to its previous state + self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True) + self.hass.block_till_done() + + # Checking that values were restored + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual(0.0, self.media_player.volume_level, 0.0001) + self.assertTrue(self.media_player.is_volume_muted) + self.assertEqual('one', self.media_player.source) def test_update(self): """Test updating values from monoprice.""" diff --git a/tests/components/media_player/test_yamaha.py b/tests/components/media_player/test_yamaha.py index ad443fadebb..176cf7c5bf2 100644 --- a/tests/components/media_player/test_yamaha.py +++ b/tests/components/media_player/test_yamaha.py @@ -4,6 +4,15 @@ import xml.etree.ElementTree as ET import rxv +import homeassistant.components.media_player.yamaha as yamaha + +TEST_CONFIG = { + 'name': "Test Receiver", + 'source_ignore': ['HDMI5'], + 'source_names': {'HDMI1': 'Laserdisc'}, + 'zone_names': {'Main_Zone': "Laser Dome"} +} + def sample_content(name): """Read content into a string from a file.""" @@ -12,6 +21,14 @@ def sample_content(name): return content.read() +def yamaha_player(receiver): + """Create a YamahaDevice from a given receiver, presumably a Mock.""" + zone_controller = receiver.zone_controllers()[0] + player = yamaha.YamahaDevice(receiver=zone_controller, **TEST_CONFIG) + player.build_source_list() + return player + + class FakeYamaha(rxv.rxv.RXV): """Fake Yamaha receiver. @@ -74,6 +91,7 @@ class TestYamaha(unittest.TestCase): """Setup things to be run when tests are started.""" super(TestYamaha, self).setUp() self.rec = FakeYamaha("http://10.0.0.0:80/YamahaRemoteControl/ctrl") + self.player = yamaha_player(self.rec) def test_get_playback_support(self): """Test the playback.""" @@ -92,3 +110,20 @@ class TestYamaha(unittest.TestCase): self.assertTrue(support.stop) self.assertFalse(support.skip_f) self.assertFalse(support.skip_r) + + def test_configuration_options(self): + """Test configuration options.""" + rec_name = TEST_CONFIG['name'] + src_zone = 'Main_Zone' + src_zone_alt = src_zone.replace('_', ' ') + renamed_zone = TEST_CONFIG['zone_names'][src_zone] + ignored_src = TEST_CONFIG['source_ignore'][0] + renamed_src = 'HDMI1' + new_src = TEST_CONFIG['source_names'][renamed_src] + self.assertFalse(self.player.name == rec_name + ' ' + src_zone) + self.assertFalse(self.player.name == rec_name + ' ' + src_zone_alt) + self.assertTrue(self.player.name == rec_name + ' ' + renamed_zone) + + self.assertFalse(ignored_src in self.player.source_list) + self.assertFalse(renamed_src in self.player.source_list) + self.assertTrue(new_src in self.player.source_list) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 0bd0333a6fb..7715ff168be 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -3,7 +3,6 @@ import io import unittest from unittest.mock import Mock, patch, mock_open -from apns2.errors import Unregistered import yaml import homeassistant.components.notify as notify @@ -359,6 +358,8 @@ class TestApns(unittest.TestCase): @patch('homeassistant.components.notify.apns._write_device') def test_disable_when_unregistered(self, mock_write, mock_client): """Test disabling a device when it is unregistered.""" + from apns2.errors import Unregistered + send = mock_client.return_value.send_notification send.side_effect = Unregistered() diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index c3998b6db64..6fb2e6454de 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -4,10 +4,14 @@ import json from unittest.mock import patch, MagicMock, mock_open from aiohttp.hdrs import AUTHORIZATION +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.json import save_json from homeassistant.components.notify import html5 from tests.common import mock_http_component_app +CONFIG_FILE = 'file.conf' + SUBSCRIPTION_1 = { 'browser': 'chrome', 'subscription': { @@ -108,36 +112,30 @@ class TestHtml5Notify(object): 'unnamed device': SUBSCRIPTION_1, } - m = mock_open() - with patch( - 'homeassistant.util.json.open', - m, create=True - ): - hass.config.path.return_value = 'file.conf' - service = html5.get_service(hass, {}) + hass.config.path.return_value = CONFIG_FILE + service = html5.get_service(hass, {}) - assert service is not None + assert service is not None - # assert hass.called - assert len(hass.mock_calls) == 3 + assert len(hass.mock_calls) == 3 - view = hass.mock_calls[1][1][0] - assert view.json_path == hass.config.path.return_value - assert view.registrations == {} + view = hass.mock_calls[1][1][0] + assert view.json_path == hass.config.path.return_value + assert view.registrations == {} - hass.loop = loop - app = mock_http_component_app(hass) - view.register(app.router) - client = yield from test_client(app) - hass.http.is_banned_ip.return_value = False - resp = yield from client.post(REGISTER_URL, - data=json.dumps(SUBSCRIPTION_1)) + hass.loop = loop + app = mock_http_component_app(hass) + view.register(app.router) + client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False + resp = yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_1)) - content = yield from resp.text() - assert resp.status == 200, content - assert view.registrations == expected - handle = m() - assert json.loads(handle.write.call_args[0][0]) == expected + content = yield from resp.text() + assert resp.status == 200, content + assert view.registrations == expected + + hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, expected) @asyncio.coroutine def test_registering_new_device_expiration_view(self, loop, test_client): @@ -147,36 +145,114 @@ class TestHtml5Notify(object): 'unnamed device': SUBSCRIPTION_4, } - m = mock_open() - with patch( - 'homeassistant.util.json.open', - m, create=True - ): - hass.config.path.return_value = 'file.conf' - service = html5.get_service(hass, {}) + hass.config.path.return_value = CONFIG_FILE + service = html5.get_service(hass, {}) - assert service is not None + assert service is not None - # assert hass.called - assert len(hass.mock_calls) == 3 + # assert hass.called + assert len(hass.mock_calls) == 3 - view = hass.mock_calls[1][1][0] - assert view.json_path == hass.config.path.return_value - assert view.registrations == {} + view = hass.mock_calls[1][1][0] + assert view.json_path == hass.config.path.return_value + assert view.registrations == {} - hass.loop = loop - app = mock_http_component_app(hass) - view.register(app.router) - client = yield from test_client(app) - hass.http.is_banned_ip.return_value = False - resp = yield from client.post(REGISTER_URL, - data=json.dumps(SUBSCRIPTION_4)) + hass.loop = loop + app = mock_http_component_app(hass) + view.register(app.router) + client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False + resp = yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_4)) - content = yield from resp.text() - assert resp.status == 200, content - assert view.registrations == expected - handle = m() - assert json.loads(handle.write.call_args[0][0]) == expected + content = yield from resp.text() + assert resp.status == 200, content + assert view.registrations == expected + + hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, expected) + + @asyncio.coroutine + def test_registering_new_device_fails_view(self, loop, test_client): + """Test subs. are not altered when registering a new device fails.""" + hass = MagicMock() + expected = {} + + hass.config.path.return_value = CONFIG_FILE + html5.get_service(hass, {}) + view = hass.mock_calls[1][1][0] + + hass.loop = loop + app = mock_http_component_app(hass) + view.register(app.router) + client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False + + hass.async_add_job.side_effect = HomeAssistantError() + + resp = yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_1)) + + content = yield from resp.text() + assert resp.status == 500, content + assert view.registrations == expected + + @asyncio.coroutine + def test_registering_existing_device_view(self, loop, test_client): + """Test subscription is updated when registering existing device.""" + hass = MagicMock() + expected = { + 'unnamed device': SUBSCRIPTION_4, + } + + hass.config.path.return_value = CONFIG_FILE + html5.get_service(hass, {}) + view = hass.mock_calls[1][1][0] + + hass.loop = loop + app = mock_http_component_app(hass) + view.register(app.router) + client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False + + yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_1)) + resp = yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_4)) + + content = yield from resp.text() + assert resp.status == 200, content + assert view.registrations == expected + + hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, expected) + + @asyncio.coroutine + def test_registering_existing_device_fails_view(self, loop, test_client): + """Test sub. is not updated when registering existing device fails.""" + hass = MagicMock() + expected = { + 'unnamed device': SUBSCRIPTION_1, + } + + hass.config.path.return_value = CONFIG_FILE + html5.get_service(hass, {}) + view = hass.mock_calls[1][1][0] + + hass.loop = loop + app = mock_http_component_app(hass) + view.register(app.router) + client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False + + yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_1)) + + hass.async_add_job.side_effect = HomeAssistantError() + resp = yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_4)) + + content = yield from resp.text() + assert resp.status == 500, content + assert view.registrations == expected @asyncio.coroutine def test_registering_new_device_validation(self, loop, test_client): @@ -188,7 +264,7 @@ class TestHtml5Notify(object): 'homeassistant.util.json.open', m, create=True ): - hass.config.path.return_value = 'file.conf' + hass.config.path.return_value = CONFIG_FILE service = html5.get_service(hass, {}) assert service is not None @@ -240,7 +316,7 @@ class TestHtml5Notify(object): 'homeassistant.util.json.open', m, create=True ): - hass.config.path.return_value = 'file.conf' + hass.config.path.return_value = CONFIG_FILE service = html5.get_service(hass, {}) assert service is not None @@ -266,8 +342,9 @@ class TestHtml5Notify(object): assert resp.status == 200, resp.response assert view.registrations == config - handle = m() - assert json.loads(handle.write.call_args[0][0]) == config + + hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, + config) @asyncio.coroutine def test_unregister_device_view_handle_unknown_subscription( @@ -285,7 +362,7 @@ class TestHtml5Notify(object): 'homeassistant.util.json.open', m, create=True ): - hass.config.path.return_value = 'file.conf' + hass.config.path.return_value = CONFIG_FILE service = html5.get_service(hass, {}) assert service is not None @@ -309,13 +386,13 @@ class TestHtml5Notify(object): assert resp.status == 200, resp.response assert view.registrations == config - handle = m() - assert handle.write.call_count == 0 + + hass.async_add_job.assert_not_called() @asyncio.coroutine - def test_unregistering_device_view_handles_json_safe_error( + def test_unregistering_device_view_handles_save_error( self, loop, test_client): - """Test that the HTML unregister view handles JSON write errors.""" + """Test that the HTML unregister view handles save errors.""" hass = MagicMock() config = { @@ -328,7 +405,7 @@ class TestHtml5Notify(object): 'homeassistant.util.json.open', m, create=True ): - hass.config.path.return_value = 'file.conf' + hass.config.path.return_value = CONFIG_FILE service = html5.get_service(hass, {}) assert service is not None @@ -346,16 +423,13 @@ class TestHtml5Notify(object): client = yield from test_client(app) hass.http.is_banned_ip.return_value = False - with patch('homeassistant.components.notify.html5.save_json', - return_value=False): - resp = yield from client.delete(REGISTER_URL, data=json.dumps({ - 'subscription': SUBSCRIPTION_1['subscription'], - })) + hass.async_add_job.side_effect = HomeAssistantError() + resp = yield from client.delete(REGISTER_URL, data=json.dumps({ + 'subscription': SUBSCRIPTION_1['subscription'], + })) assert resp.status == 500, resp.response assert view.registrations == config - handle = m() - assert handle.write.call_count == 0 @asyncio.coroutine def test_callback_view_no_jwt(self, loop, test_client): @@ -367,7 +441,7 @@ class TestHtml5Notify(object): 'homeassistant.util.json.open', m, create=True ): - hass.config.path.return_value = 'file.conf' + hass.config.path.return_value = CONFIG_FILE service = html5.get_service(hass, {}) assert service is not None @@ -404,7 +478,7 @@ class TestHtml5Notify(object): 'homeassistant.util.json.open', m, create=True ): - hass.config.path.return_value = 'file.conf' + hass.config.path.return_value = CONFIG_FILE service = html5.get_service(hass, {'gcm_sender_id': '100'}) assert service is not None diff --git a/tests/components/notify/test_pushbullet.py b/tests/components/notify/test_pushbullet.py new file mode 100644 index 00000000000..ba3046e8fd7 --- /dev/null +++ b/tests/components/notify/test_pushbullet.py @@ -0,0 +1,42 @@ +"""The tests for the pushbullet notification platform.""" + +import unittest + +from homeassistant.setup import setup_component +import homeassistant.components.notify as notify +from tests.common import assert_setup_component, get_test_home_assistant + + +class TestPushbullet(unittest.TestCase): + """Test the pushbullet notifications.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_setup(self): + """Test setup.""" + with assert_setup_component(1) as handle_config: + assert setup_component(self.hass, 'notify', { + 'notify': { + 'name': 'test', + 'platform': 'pushbullet', + 'api_key': 'MYFAKEKEY', } + }) + assert handle_config[notify.DOMAIN] + + def test_bad_config(self): + """Test set up the platform with bad/missing configuration.""" + config = { + notify.DOMAIN: { + 'name': 'test', + 'platform': 'pushbullet', + } + } + with assert_setup_component(0) as handle_config: + assert setup_component(self.hass, notify.DOMAIN, config) + assert not handle_config[notify.DOMAIN] diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 5db710882d9..bbb87fb5016 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -55,6 +55,23 @@ class TestRecorderPurge(unittest.TestCase): event_id=event_id + 1000 )) + # if self._add_test_events was called, we added a special event + # that should be protected from deletion, too + protected_event_id = getattr(self, "_protected_event_id", 2000) + + # add a state that is old but the only state of its entity and + # should be protected + session.add(States( + entity_id='test.rarely_updated_entity', + domain='sensor', + state='iamprotected', + attributes=json.dumps(attributes), + last_changed=five_days_ago, + last_updated=five_days_ago, + created=five_days_ago, + event_id=protected_event_id + )) + def _add_test_events(self): """Add a few events for testing.""" now = datetime.now() @@ -81,19 +98,32 @@ class TestRecorderPurge(unittest.TestCase): time_fired=timestamp, )) + # Add an event for the protected state + protected_event = Events( + event_type='EVENT_TEST_FOR_PROTECTED', + event_data=json.dumps(event_data), + origin='LOCAL', + created=five_days_ago, + time_fired=five_days_ago, + ) + session.add(protected_event) + session.flush() + + self._protected_event_id = protected_event.event_id + def test_purge_old_states(self): """Test deleting old states.""" self._add_test_states() - # make sure we start with 5 states + # make sure we start with 6 states with session_scope(hass=self.hass) as session: states = session.query(States) - self.assertEqual(states.count(), 5) + self.assertEqual(states.count(), 6) # run purge_old_data() purge_old_data(self.hass.data[DATA_INSTANCE], 4) - # we should only have 2 states left after purging - self.assertEqual(states.count(), 2) + # we should only have 3 states left after purging + self.assertEqual(states.count(), 3) def test_purge_old_events(self): """Test deleting old events.""" @@ -102,7 +132,7 @@ class TestRecorderPurge(unittest.TestCase): with session_scope(hass=self.hass) as session: events = session.query(Events).filter( Events.event_type.like("EVENT_TEST%")) - self.assertEqual(events.count(), 5) + self.assertEqual(events.count(), 6) # run purge_old_data() purge_old_data(self.hass.data[DATA_INSTANCE], 4) @@ -113,17 +143,17 @@ class TestRecorderPurge(unittest.TestCase): def test_purge_method(self): """Test purge method.""" service_data = {'keep_days': 4} - self._add_test_states() self._add_test_events() + self._add_test_states() - # make sure we start with 5 states + # make sure we start with 6 states with session_scope(hass=self.hass) as session: states = session.query(States) - self.assertEqual(states.count(), 5) + self.assertEqual(states.count(), 6) events = session.query(Events).filter( Events.event_type.like("EVENT_TEST%")) - self.assertEqual(events.count(), 5) + self.assertEqual(events.count(), 6) self.hass.data[DATA_INSTANCE].block_till_done() @@ -134,11 +164,9 @@ class TestRecorderPurge(unittest.TestCase): # Small wait for recorder thread sleep(0.1) - # we should only have 2 states left after purging - self.assertEqual(states.count(), 5) - - # now we should only have 3 events left - self.assertEqual(events.count(), 5) + # we should still have everything from before + self.assertEqual(states.count(), 6) + self.assertEqual(events.count(), 6) # run purge method - correct service data self.hass.services.call('recorder', 'purge', @@ -148,8 +176,18 @@ class TestRecorderPurge(unittest.TestCase): # Small wait for recorder thread sleep(0.1) - # we should only have 2 states left after purging - self.assertEqual(states.count(), 2) + # we should only have 3 states left after purging + self.assertEqual(states.count(), 3) - # now we should only have 3 events left - self.assertEqual(events.count(), 3) + # the protected state is among them + self.assertTrue('iamprotected' in ( + state.state for state in states)) + + # now we should only have 4 events left + self.assertEqual(events.count(), 4) + + # and the protected event is among them + self.assertTrue('EVENT_TEST_FOR_PROTECTED' in ( + event.event_type for event in events.all())) + self.assertFalse('EVENT_TEST_PURGE' in ( + event.event_type for event in events.all())) diff --git a/tests/components/remote/__init__.py b/tests/components/remote/__init__.py old mode 100755 new mode 100644 diff --git a/tests/components/remote/test_demo.py b/tests/components/remote/test_demo.py old mode 100755 new mode 100644 diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py old mode 100755 new mode 100644 diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py index 99df05f36a4..b35b5630d60 100644 --- a/tests/components/sensor/test_canary.py +++ b/tests/components/sensor/test_canary.py @@ -1,10 +1,9 @@ """The tests for the Canary sensor platform.""" import copy import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock from canary.api import SensorType -from homeassistant.components import canary as base_canary from homeassistant.components.canary import DATA_CANARY from homeassistant.components.sensor import canary from homeassistant.components.sensor.canary import CanarySensor @@ -39,16 +38,13 @@ class TestCanarySensorSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @patch('homeassistant.components.canary.CanaryData') - def test_setup_sensors(self, mock_canary): + def test_setup_sensors(self): """Test the sensor setup.""" - base_canary.setup(self.hass, self.config) - online_device_at_home = mock_device(20, "Dining Room", True) offline_device_at_home = mock_device(21, "Front Yard", False) online_device_at_work = mock_device(22, "Office", True) - self.hass.data[DATA_CANARY] = mock_canary() + self.hass.data[DATA_CANARY] = Mock() self.hass.data[DATA_CANARY].locations = [ mock_location("Home", True, devices=[online_device_at_home, offline_device_at_home]), diff --git a/tests/components/sensor/test_fido.py b/tests/components/sensor/test_fido.py new file mode 100644 index 00000000000..1eca7be7544 --- /dev/null +++ b/tests/components/sensor/test_fido.py @@ -0,0 +1,109 @@ +"""The test for the fido sensor platform.""" +import asyncio +import logging +import sys +from unittest.mock import MagicMock + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components.sensor import fido +from tests.common import assert_setup_component + + +CONTRACT = "123456789" + + +class FidoClientMock(): + """Fake Fido client.""" + + def __init__(self, username, password, timeout=None, httpsession=None): + """Fake Fido client init.""" + pass + + def get_phone_numbers(self): + """Return Phone numbers.""" + return ["1112223344"] + + def get_data(self): + """Return fake fido data.""" + return {"balance": 160.12, + "1112223344": {"data_remaining": 100.33}} + + @asyncio.coroutine + def fetch_data(self): + """Return fake fetching data.""" + pass + + +class FidoClientMockError(FidoClientMock): + """Fake Fido client error.""" + + @asyncio.coroutine + def fetch_data(self): + """Return fake fetching data.""" + raise PyFidoErrorMock("Fake Error") + + +class PyFidoErrorMock(Exception): + """Fake PyFido Error.""" + + +class PyFidoClientFakeModule(): + """Fake pyfido.client module.""" + + PyFidoError = PyFidoErrorMock + + +class PyFidoFakeModule(): + """Fake pyfido module.""" + + FidoClient = FidoClientMockError + + +def fake_async_add_devices(component, update_before_add=False): + """Fake async_add_devices function.""" + pass + + +@asyncio.coroutine +def test_fido_sensor(loop, hass): + """Test the Fido number sensor.""" + sys.modules['pyfido'] = MagicMock() + sys.modules['pyfido.client'] = MagicMock() + sys.modules['pyfido.client.PyFidoError'] = \ + PyFidoErrorMock + import pyfido.client + pyfido.FidoClient = FidoClientMock + pyfido.client.PyFidoError = PyFidoErrorMock + config = { + 'sensor': { + 'platform': 'fido', + 'name': 'fido', + 'username': 'myusername', + 'password': 'password', + 'monitored_variables': [ + 'balance', + 'data_remaining', + ], + } + } + with assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', config) + state = hass.states.get('sensor.fido_1112223344_balance') + assert state.state == "160.12" + assert state.attributes.get('number') == "1112223344" + state = hass.states.get('sensor.fido_1112223344_data_remaining') + assert state.state == "100.33" + + +@asyncio.coroutine +def test_error(hass, caplog): + """Test the Fido sensor errors.""" + caplog.set_level(logging.ERROR) + sys.modules['pyfido'] = PyFidoFakeModule() + sys.modules['pyfido.client'] = PyFidoClientFakeModule() + + config = {} + fake_async_add_devices = MagicMock() + yield from fido.async_setup_platform(hass, config, + fake_async_add_devices) + assert fake_async_add_devices.called is False diff --git a/tests/components/sensor/test_geo_rss_events.py b/tests/components/sensor/test_geo_rss_events.py index 557def8225b..f9ec83cc8be 100644 --- a/tests/components/sensor/test_geo_rss_events.py +++ b/tests/components/sensor/test_geo_rss_events.py @@ -1,6 +1,7 @@ """The test for the geo rss events sensor platform.""" import unittest from unittest import mock +import feedparser from homeassistant.setup import setup_component from tests.common import load_fixture, get_test_home_assistant @@ -33,7 +34,8 @@ class TestGeoRssServiceUpdater(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_setup_with_categories(self): + @mock.patch('feedparser.parse', return_value=feedparser.parse("")) + def test_setup_with_categories(self, mock_parse): """Test the general setup of this sensor.""" self.config = VALID_CONFIG_WITH_CATEGORIES self.assertTrue( @@ -43,7 +45,8 @@ class TestGeoRssServiceUpdater(unittest.TestCase): self.assertIsNotNone( self.hass.states.get('sensor.event_service_category_2')) - def test_setup_without_categories(self): + @mock.patch('feedparser.parse', return_value=feedparser.parse("")) + def test_setup_without_categories(self, mock_parse): """Test the general setup of this sensor.""" self.assertTrue( setup_component(self.hass, 'sensor', {'sensor': self.config})) diff --git a/tests/components/sensor/test_hydroquebec.py b/tests/components/sensor/test_hydroquebec.py new file mode 100644 index 00000000000..debd6ef6167 --- /dev/null +++ b/tests/components/sensor/test_hydroquebec.py @@ -0,0 +1,105 @@ +"""The test for the hydroquebec sensor platform.""" +import asyncio +import logging +import sys +from unittest.mock import MagicMock + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components.sensor import hydroquebec +from tests.common import assert_setup_component + + +CONTRACT = "123456789" + + +class HydroQuebecClientMock(): + """Fake Hydroquebec client.""" + + def __init__(self, username, password, contract=None, httpsession=None): + """Fake Hydroquebec client init.""" + pass + + def get_data(self, contract): + """Return fake hydroquebec data.""" + return {CONTRACT: {"balance": 160.12}} + + def get_contracts(self): + """Return fake hydroquebec contracts.""" + return [CONTRACT] + + @asyncio.coroutine + def fetch_data(self): + """Return fake fetching data.""" + pass + + +class HydroQuebecClientMockError(HydroQuebecClientMock): + """Fake Hydroquebec client error.""" + + def get_contracts(self): + """Return fake hydroquebec contracts.""" + return [] + + @asyncio.coroutine + def fetch_data(self): + """Return fake fetching data.""" + raise PyHydroQuebecErrorMock("Fake Error") + + +class PyHydroQuebecErrorMock(BaseException): + """Fake PyHydroquebec Error.""" + + +class PyHydroQuebecClientFakeModule(): + """Fake pyfido.client module.""" + + PyHydroQuebecError = PyHydroQuebecErrorMock + + +class PyHydroQuebecFakeModule(): + """Fake pyfido module.""" + + HydroQuebecClient = HydroQuebecClientMockError + + +@asyncio.coroutine +def test_hydroquebec_sensor(loop, hass): + """Test the Hydroquebec number sensor.""" + sys.modules['pyhydroquebec'] = MagicMock() + sys.modules['pyhydroquebec.client'] = MagicMock() + sys.modules['pyhydroquebec.client.PyHydroQuebecError'] = \ + PyHydroQuebecErrorMock + import pyhydroquebec.client + pyhydroquebec.HydroQuebecClient = HydroQuebecClientMock + pyhydroquebec.client.PyHydroQuebecError = PyHydroQuebecErrorMock + config = { + 'sensor': { + 'platform': 'hydroquebec', + 'name': 'hydro', + 'contract': CONTRACT, + 'username': 'myusername', + 'password': 'password', + 'monitored_variables': [ + 'balance', + ], + } + } + with assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', config) + state = hass.states.get('sensor.hydro_balance') + assert state.state == "160.12" + assert state.attributes.get('unit_of_measurement') == "CAD" + + +@asyncio.coroutine +def test_error(hass, caplog): + """Test the Hydroquebec sensor errors.""" + caplog.set_level(logging.ERROR) + sys.modules['pyhydroquebec'] = PyHydroQuebecFakeModule() + sys.modules['pyhydroquebec.client'] = PyHydroQuebecClientFakeModule() + + config = {} + fake_async_add_devices = MagicMock() + yield from hydroquebec.async_setup_platform(hass, config, + fake_async_add_devices) + assert fake_async_add_devices.called is False diff --git a/tests/components/sensor/test_imap_email_content.py b/tests/components/sensor/test_imap_email_content.py index 0bba3647c6c..cd5c079a431 100644 --- a/tests/components/sensor/test_imap_email_content.py +++ b/tests/components/sensor/test_imap_email_content.py @@ -60,7 +60,9 @@ class EmailContentSensor(unittest.TestCase): sensor.entity_id = 'sensor.emailtest' sensor.schedule_update_ha_state(True) self.hass.block_till_done() - self.assertEqual("Test Message", sensor.state) + self.assertEqual('Test', sensor.state) + self.assertEqual("Test Message", + sensor.device_state_attributes['body']) self.assertEqual('sender@test.com', sensor.device_state_attributes['from']) self.assertEqual('Test', sensor.device_state_attributes['subject']) @@ -89,13 +91,15 @@ class EmailContentSensor(unittest.TestCase): sensor.entity_id = "sensor.emailtest" sensor.schedule_update_ha_state(True) self.hass.block_till_done() - self.assertEqual("Test Message", sensor.state) + self.assertEqual('Link', sensor.state) + self.assertEqual("Test Message", + sensor.device_state_attributes['body']) def test_multi_part_only_html(self): """Test multi part emails with only HTML.""" msg = MIMEMultipart('alternative') - msg['Subject'] = "Link" - msg['From'] = "sender@test.com" + msg['Subject'] = 'Link' + msg['From'] = 'sender@test.com' html = "Test Message" @@ -113,9 +117,10 @@ class EmailContentSensor(unittest.TestCase): sensor.entity_id = 'sensor.emailtest' sensor.schedule_update_ha_state(True) self.hass.block_till_done() + self.assertEqual('Link', sensor.state) self.assertEqual( "Test Message", - sensor.state) + sensor.device_state_attributes['body']) def test_multi_part_only_other_text(self): """Test multi part emails with only other text.""" @@ -136,7 +141,9 @@ class EmailContentSensor(unittest.TestCase): sensor.entity_id = 'sensor.emailtest' sensor.schedule_update_ha_state(True) self.hass.block_till_done() - self.assertEqual("Test Message", sensor.state) + self.assertEqual('Link', sensor.state) + self.assertEqual("Test Message", + sensor.device_state_attributes['body']) def test_multiple_emails(self): """Test multiple emails.""" @@ -172,10 +179,11 @@ class EmailContentSensor(unittest.TestCase): sensor.schedule_update_ha_state(True) self.hass.block_till_done() - self.assertEqual("Test Message", states[0].state) - self.assertEqual("Test Message 2", states[1].state) + self.assertEqual("Test", states[0].state) + self.assertEqual("Test 2", states[1].state) - self.assertEqual("Test Message 2", sensor.state) + self.assertEqual("Test Message 2", + sensor.device_state_attributes['body']) def test_sender_not_allowed(self): """Test not whitelisted emails.""" diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 42136966e13..d5cfad407d5 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -7,7 +7,7 @@ from unittest.mock import patch import homeassistant.core as ha from homeassistant.setup import setup_component import homeassistant.components.sensor as sensor -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util from tests.common import mock_mqtt_component, fire_mqtt_message @@ -185,6 +185,121 @@ class TestSensorMQTT(unittest.TestCase): self.hass.block_till_done() self.assertEqual(2, len(events)) + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('sensor.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + def _send_time_changed(self, now): """Send a time changed event.""" self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) + + def test_setting_sensor_attribute_via_mqtt_json_message(self): + """Test the setting of attribute via MQTT with JSON playload.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual('100', + state.attributes.get('val')) + + @patch('homeassistant.components.sensor.mqtt._LOGGER') + def test_update_with_json_attrs_not_dict(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', '[ "list", "of", "things"]') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual(None, + state.attributes.get('val')) + self.assertTrue(mock_logger.warning.called) + + @patch('homeassistant.components.sensor.mqtt._LOGGER') + def test_update_with_json_attrs_bad_JSON(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', 'This is not JSON') + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertEqual(None, + state.attributes.get('val')) + self.assertTrue(mock_logger.warning.called) + self.assertTrue(mock_logger.debug.called) + + def test_update_with_json_attrs_and_template(self): + """Test attributes get extracted from a JSON result.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'value_template': '{{ value_json.val }}', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual('100', + state.attributes.get('val')) + self.assertEqual('100', state.state) diff --git a/tests/components/sensor/test_statistics.py b/tests/components/sensor/test_statistics.py index bfb8fb61f9b..48ebf720633 100644 --- a/tests/components/sensor/test_statistics.py +++ b/tests/components/sensor/test_statistics.py @@ -3,7 +3,8 @@ import unittest import statistics from homeassistant.setup import setup_component -from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN) from homeassistant.util import dt as dt_util from tests.common import get_test_home_assistant from unittest.mock import patch @@ -106,6 +107,38 @@ class TestStatisticsSensor(unittest.TestCase): self.assertEqual(3.8, state.attributes.get('min_value')) self.assertEqual(14, state.attributes.get('max_value')) + def test_sampling_size_1(self): + """Test validity of stats requiring only one sample.""" + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'statistics', + 'name': 'test', + 'entity_id': 'sensor.test_monitored', + 'sampling_size': 1, + } + }) + + for value in self.values[-3:]: # just the last 3 will do + self.hass.states.set('sensor.test_monitored', value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_mean') + + # require only one data point + self.assertEqual(self.values[-1], state.attributes.get('min_value')) + self.assertEqual(self.values[-1], state.attributes.get('max_value')) + self.assertEqual(self.values[-1], state.attributes.get('mean')) + self.assertEqual(self.values[-1], state.attributes.get('median')) + self.assertEqual(self.values[-1], state.attributes.get('total')) + self.assertEqual(0, state.attributes.get('change')) + self.assertEqual(0, state.attributes.get('average_change')) + + # require at least two data points + self.assertEqual(STATE_UNKNOWN, state.attributes.get('variance')) + self.assertEqual(STATE_UNKNOWN, + state.attributes.get('standard_deviation')) + def test_max_age(self): """Test value deprecation.""" mock_data = { diff --git a/tests/components/sensor/test_vultr.py b/tests/components/sensor/test_vultr.py index a4e5edc5800..c5222ab5543 100644 --- a/tests/components/sensor/test_vultr.py +++ b/tests/components/sensor/test_vultr.py @@ -1,6 +1,9 @@ """The tests for the Vultr sensor platform.""" -import pytest +import json import unittest +from unittest.mock import patch + +import pytest import requests_mock import voluptuous as vol @@ -59,11 +62,12 @@ class TestVultrSensorSetup(unittest.TestCase): 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - base_vultr.setup(self.hass, VALID_CONFIG) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + # Setup hub + base_vultr.setup(self.hass, VALID_CONFIG) for config in self.configs: setup = vultr.setup_platform(self.hass, @@ -146,11 +150,12 @@ class TestVultrSensorSetup(unittest.TestCase): 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - base_vultr.setup(self.hass, VALID_CONFIG) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + # Setup hub + base_vultr.setup(self.hass, VALID_CONFIG) bad_conf = { CONF_MONITORED_CONDITIONS: vultr.MONITORED_CONDITIONS, diff --git a/tests/components/switch/test_mochad.py b/tests/components/switch/test_mochad.py index 8011d85860e..a5e6e2c9ae6 100644 --- a/tests/components/switch/test_mochad.py +++ b/tests/components/switch/test_mochad.py @@ -16,6 +16,7 @@ def pymochad_mock(): """Mock pymochad.""" with mock.patch.dict('sys.modules', { 'pymochad': mock.MagicMock(), + 'pymochad.exceptions': mock.MagicMock(), }): yield diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 21ab1dd31f2..a3118f8ebf0 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -167,22 +167,22 @@ class TestSwitchMQTT(unittest.TestCase): 'availability_topic': 'availability_topic', 'payload_on': 1, 'payload_off': 0, - 'payload_available': 'online', - 'payload_not_available': 'offline' + 'payload_available': 'good', + 'payload_not_available': 'nogood' } }) state = self.hass.states.get('switch.test') self.assertEqual(STATE_UNAVAILABLE, state.state) - fire_mqtt_message(self.hass, 'availability_topic', 'online') + fire_mqtt_message(self.hass, 'availability_topic', 'good') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) - fire_mqtt_message(self.hass, 'availability_topic', 'offline') + fire_mqtt_message(self.hass, 'availability_topic', 'nogood') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -194,7 +194,7 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_UNAVAILABLE, state.state) - fire_mqtt_message(self.hass, 'availability_topic', 'online') + fire_mqtt_message(self.hass, 'availability_topic', 'good') self.hass.block_till_done() state = self.hass.states.get('switch.test') diff --git a/tests/components/switch/test_vultr.py b/tests/components/switch/test_vultr.py index 53bf6fbec85..222a044a523 100644 --- a/tests/components/switch/test_vultr.py +++ b/tests/components/switch/test_vultr.py @@ -1,5 +1,8 @@ """Test the Vultr switch platform.""" +import json import unittest +from unittest.mock import patch + import requests_mock import pytest import voluptuous as vol @@ -57,12 +60,12 @@ class TestVultrSwitchSetup(unittest.TestCase): 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - # Setup hub - base_vultr.setup(self.hass, VALID_CONFIG) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + # Setup hub + base_vultr.setup(self.hass, VALID_CONFIG) # Setup each of our test configs for config in self.configs: @@ -128,36 +131,30 @@ class TestVultrSwitchSetup(unittest.TestCase): @requests_mock.Mocker() def test_turn_on(self, mock): """Test turning a subscription on.""" - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads(load_fixture('vultr_server_list.json'))), \ + patch('vultr.Vultr.server_start') as mock_start: + for device in self.DEVICES: + if device.name == 'Failed Server': + device.turn_on() - mock.post( - 'https://api.vultr.com/v1/server/start?api_key=ABCDEFG1234567') - - for device in self.DEVICES: - if device.name == 'Failed Server': - device.turn_on() - - # Turn on, force date update - self.assertEqual(2, mock.call_count) + # Turn on + self.assertEqual(1, mock_start.call_count) @requests_mock.Mocker() def test_turn_off(self, mock): """Test turning a subscription off.""" - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads(load_fixture('vultr_server_list.json'))), \ + patch('vultr.Vultr.server_halt') as mock_halt: + for device in self.DEVICES: + if device.name == 'A Server': + device.turn_off() - mock.post( - 'https://api.vultr.com/v1/server/halt?api_key=ABCDEFG1234567') - - for device in self.DEVICES: - if device.name == 'A Server': - device.turn_off() - - # Turn off, force update - self.assertEqual(2, mock.call_count) + # Turn off + self.assertEqual(1, mock_halt.call_count) def test_invalid_switch_config(self): """Test config type failures.""" @@ -173,11 +170,12 @@ class TestVultrSwitchSetup(unittest.TestCase): 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - base_vultr.setup(self.hass, VALID_CONFIG) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + # Setup hub + base_vultr.setup(self.hass, VALID_CONFIG) bad_conf = {} # No subscription diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index 3704c486a2a..48443658fc4 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -7,6 +7,7 @@ import pytest from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component +from homeassistant.components.hassio import async_check_config from tests.common import mock_coro @@ -18,7 +19,12 @@ def hassio_env(): """Fixture to inject hassio env.""" with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ patch('homeassistant.components.hassio.HassIO.is_connected', - Mock(return_value=mock_coro(True))): + Mock(return_value=mock_coro( + {"result": "ok", "data": {}}))), \ + patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}), \ + patch('homeassistant.components.hassio.HassIO.' + 'get_homeassistant_info', + Mock(return_value=mock_coro(None))): yield @@ -26,7 +32,10 @@ def hassio_env(): def hassio_client(hassio_env, hass, test_client): """Create mock hassio http client.""" with patch('homeassistant.components.hassio.HassIO.update_hass_api', - Mock(return_value=mock_coro(True))): + Mock(return_value=mock_coro({"result": "ok"}))), \ + patch('homeassistant.components.hassio.HassIO.' + 'get_homeassistant_info', + Mock(return_value=mock_coro(None))): hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { 'http': { 'api_password': API_PASSWORD @@ -48,22 +57,29 @@ def test_fail_setup_cannot_connect(hass): """Fail setup if cannot connect.""" with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ patch('homeassistant.components.hassio.HassIO.is_connected', - Mock(return_value=mock_coro(False))): + Mock(return_value=mock_coro(None))): result = yield from async_setup_component(hass, 'hassio', {}) assert not result + assert not hass.components.hassio.is_hassio() + @asyncio.coroutine def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): result = yield from async_setup_component(hass, 'hassio', {}) assert result - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 + assert hass.components.hassio.get_homeassistant_version() == "10.0" + assert hass.components.hassio.is_hassio() @asyncio.coroutine @@ -71,6 +87,9 @@ def test_setup_api_push_api_data(hass, aioclient_mock): """Test setup with API push.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) @@ -84,10 +103,40 @@ def test_setup_api_push_api_data(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 2 - assert not aioclient_mock.mock_calls[-1][2]['ssl'] - assert aioclient_mock.mock_calls[-1][2]['password'] == "123456" - assert aioclient_mock.mock_calls[-1][2]['port'] == 9999 + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] == "123456" + assert aioclient_mock.mock_calls[1][2]['port'] == 9999 + assert aioclient_mock.mock_calls[1][2]['watchdog'] + + +@asyncio.coroutine +def test_setup_api_push_api_data_server_host(hass, aioclient_mock): + """Test setup with API push with active server host.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) + aioclient_mock.post( + "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) + + with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + result = yield from async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': "123456", + 'server_port': 9999, + 'server_host': "127.0.0.1" + }, + 'hassio': {} + }) + assert result + + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] == "123456" + assert aioclient_mock.mock_calls[1][2]['port'] == 9999 + assert not aioclient_mock.mock_calls[1][2]['watchdog'] @asyncio.coroutine @@ -95,6 +144,9 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): """Test setup with API push default data.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) @@ -105,10 +157,10 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 2 - assert not aioclient_mock.mock_calls[-1][2]['ssl'] - assert aioclient_mock.mock_calls[-1][2]['password'] is None - assert aioclient_mock.mock_calls[-1][2]['port'] == 8123 + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] is None + assert aioclient_mock.mock_calls[1][2]['port'] == 8123 @asyncio.coroutine @@ -116,6 +168,9 @@ def test_setup_core_push_timezone(hass, aioclient_mock): """Test setup with API push default data.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) aioclient_mock.post( "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) @@ -128,8 +183,8 @@ def test_setup_core_push_timezone(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 2 - assert aioclient_mock.mock_calls[-1][2]['timezone'] == "testzone" + assert aioclient_mock.call_count == 3 + assert aioclient_mock.mock_calls[1][2]['timezone'] == "testzone" @asyncio.coroutine @@ -137,14 +192,21 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): """Test setup with API push default data.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, }) assert result - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 + assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456" @asyncio.coroutine @@ -157,6 +219,11 @@ def test_service_register(hassio_env, hass): assert hass.services.has_service('hassio', 'addon_stdin') assert hass.services.has_service('hassio', 'host_shutdown') assert hass.services.has_service('hassio', 'host_reboot') + assert hass.services.has_service('hassio', 'host_reboot') + assert hass.services.has_service('hassio', 'snapshot_full') + assert hass.services.has_service('hassio', 'snapshot_partial') + assert hass.services.has_service('hassio', 'restore_full') + assert hass.services.has_service('hassio', 'restore_partial') @asyncio.coroutine @@ -176,6 +243,15 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "http://127.0.0.1/host/shutdown", json={'result': 'ok'}) aioclient_mock.post( "http://127.0.0.1/host/reboot", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/snapshots/new/full", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/snapshots/new/partial", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/snapshots/test/restore/full", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/snapshots/test/restore/partial", + json={'result': 'ok'}) yield from hass.services.async_call( 'hassio', 'addon_start', {'addon': 'test'}) @@ -196,6 +272,83 @@ def test_service_calls(hassio_env, hass, aioclient_mock): assert aioclient_mock.call_count == 6 + yield from hass.services.async_call('hassio', 'snapshot_full', {}) + yield from hass.services.async_call('hassio', 'snapshot_partial', { + 'addons': ['test'], + 'folders': ['ssl'], + }) + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 8 + assert aioclient_mock.mock_calls[-1][2] == { + 'addons': ['test'], 'folders': ['ssl']} + + yield from hass.services.async_call('hassio', 'restore_full', { + 'snapshot': 'test', + }) + yield from hass.services.async_call('hassio', 'restore_partial', { + 'snapshot': 'test', + 'homeassistant': False, + 'addons': ['test'], + 'folders': ['ssl'], + }) + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 10 + assert aioclient_mock.mock_calls[-1][2] == { + 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False} + + +@asyncio.coroutine +def test_service_calls_core(hassio_env, hass, aioclient_mock): + """Call core service and check the API calls behind that.""" + assert (yield from async_setup_component(hass, 'hassio', {})) + + aioclient_mock.post( + "http://127.0.0.1/homeassistant/restart", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/homeassistant/stop", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/homeassistant/check", json={'result': 'ok'}) + + yield from hass.services.async_call('homeassistant', 'stop') + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 1 + + yield from hass.services.async_call('homeassistant', 'check_config') + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + + yield from hass.services.async_call('homeassistant', 'restart') + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 4 + + +@asyncio.coroutine +def test_check_config_ok(hassio_env, hass, aioclient_mock): + """Check Config that is okay.""" + assert (yield from async_setup_component(hass, 'hassio', {})) + + aioclient_mock.post( + "http://127.0.0.1/homeassistant/check", json={'result': 'ok'}) + + assert (yield from async_check_config(hass)) is None + + +@asyncio.coroutine +def test_check_config_fail(hassio_env, hass, aioclient_mock): + """Check Config that is wrong.""" + assert (yield from async_setup_component(hass, 'hassio', {})) + + aioclient_mock.post( + "http://127.0.0.1/homeassistant/check", json={ + 'result': 'error', 'message': "Error"}) + + assert (yield from async_check_config(hass)) == "Error" + @asyncio.coroutine def test_forward_request(hassio_client): diff --git a/tests/components/test_input_text.py b/tests/components/test_input_text.py old mode 100755 new mode 100644 diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py new file mode 100644 index 00000000000..91a8b326bf9 --- /dev/null +++ b/tests/components/test_nuheat.py @@ -0,0 +1,46 @@ +"""NuHeat component tests.""" +import unittest + +from unittest.mock import patch +from tests.common import get_test_home_assistant, MockDependency + +from homeassistant.components import nuheat + +VALID_CONFIG = { + "nuheat": { + "username": "warm", + "password": "feet", + "devices": "thermostat123" + } +} + + +class TestNuHeat(unittest.TestCase): + """Test the NuHeat component.""" + + def setUp(self): # pylint: disable=invalid-name + """Initialize the values for this test class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): # pylint: disable=invalid-name + """Teardown this test class. Stop hass.""" + self.hass.stop() + + @MockDependency("nuheat") + @patch("homeassistant.helpers.discovery.load_platform") + def test_setup(self, mocked_nuheat, mocked_load): + """Test setting up the NuHeat component.""" + nuheat.setup(self.hass, self.config) + + mocked_nuheat.NuHeat.assert_called_with("warm", "feet") + self.assertIn(nuheat.DOMAIN, self.hass.data) + self.assertEquals(2, len(self.hass.data[nuheat.DOMAIN])) + self.assertIsInstance( + self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) + ) + self.assertEquals(self.hass.data[nuheat.DOMAIN][1], "thermostat123") + + mocked_load.assert_called_with( + self.hass, "climate", nuheat.DOMAIN, {}, self.config + ) diff --git a/tests/components/test_python_script.py b/tests/components/test_python_script.py index 8a7f94d7dcd..c0b7df158c5 100644 --- a/tests/components/test_python_script.py +++ b/tests/components/test_python_script.py @@ -236,6 +236,8 @@ def test_exposed_modules(hass, caplog): caplog.set_level(logging.ERROR) source = """ hass.states.set('module.time', time.strftime('%Y', time.gmtime(521276400))) +hass.states.set('module.time_strptime', + time.strftime('%H:%M', time.strptime('12:34', '%H:%M'))) hass.states.set('module.datetime', datetime.timedelta(minutes=1).total_seconds()) """ @@ -244,6 +246,7 @@ hass.states.set('module.datetime', yield from hass.async_block_till_done() assert hass.states.is_state('module.time', '1986') + assert hass.states.is_state('module.time_strptime', '12:34') assert hass.states.is_state('module.datetime', '60.0') # No errors logged = good diff --git a/tests/components/test_remember_the_milk.py b/tests/components/test_remember_the_milk.py index b59c840d765..1b6619aca9c 100644 --- a/tests/components/test_remember_the_milk.py +++ b/tests/components/test_remember_the_milk.py @@ -1,6 +1,7 @@ """Tests for the Remember The Milk component.""" import logging +import json import unittest from unittest.mock import patch, mock_open, Mock @@ -19,7 +20,16 @@ class TestConfiguration(unittest.TestCase): self.hass = get_test_home_assistant() self.profile = "myprofile" self.token = "mytoken" - self.json_string = '{"myprofile": {"token": "mytoken"}}' + self.json_string = json.dumps( + {"myprofile": { + "token": "mytoken", + "id_map": {"1234": { + "list_id": "0", + "timeseries_id": "1", + "task_id": "2" + }} + } + }) def tearDown(self): """Exit home assistant.""" @@ -28,7 +38,8 @@ class TestConfiguration(unittest.TestCase): def test_create_new(self): """Test creating a new config file.""" with patch("builtins.open", mock_open()), \ - patch("os.path.isfile", Mock(return_value=False)): + patch("os.path.isfile", Mock(return_value=False)), \ + patch.object(rtm.RememberTheMilkConfiguration, 'save_config'): config = rtm.RememberTheMilkConfiguration(self.hass) config.set_token(self.profile, self.token) self.assertEqual(config.get_token(self.profile), self.token) @@ -47,3 +58,30 @@ class TestConfiguration(unittest.TestCase): patch("os.path.isfile", Mock(return_value=True)): config = rtm.RememberTheMilkConfiguration(self.hass) self.assertIsNotNone(config) + + def test_id_map(self): + """Test the hass to rtm task is mapping.""" + hass_id = "hass-id-1234" + list_id = "mylist" + timeseries_id = "my_timeseries" + rtm_id = "rtm-id-4567" + with patch("builtins.open", mock_open()), \ + patch("os.path.isfile", Mock(return_value=False)), \ + patch.object(rtm.RememberTheMilkConfiguration, 'save_config'): + config = rtm.RememberTheMilkConfiguration(self.hass) + + self.assertEqual(None, config.get_rtm_id(self.profile, hass_id)) + config.set_rtm_id(self.profile, hass_id, list_id, timeseries_id, + rtm_id) + self.assertEqual((list_id, timeseries_id, rtm_id), + config.get_rtm_id(self.profile, hass_id)) + config.delete_rtm_id(self.profile, hass_id) + self.assertEqual(None, config.get_rtm_id(self.profile, hass_id)) + + def test_load_key_map(self): + """Test loading an existing key map from the file.""" + with patch("builtins.open", mock_open(read_data=self.json_string)), \ + patch("os.path.isfile", Mock(return_value=True)): + config = rtm.RememberTheMilkConfiguration(self.hass) + self.assertEqual(('0', '1', '2',), + config.get_rtm_id(self.profile, "1234")) diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index 3bdb6896394..6f993732c38 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -109,6 +109,7 @@ class TestShellCommand(unittest.TestCase): def test_template_render(self, mock_call): """Ensure shell_commands with templates get rendered properly.""" self.hass.states.set('sensor.test_state', 'Works') + mock_call.return_value = mock_process_creator(error=False) self.assertTrue( setup_component(self.hass, shell_command.DOMAIN, { shell_command.DOMAIN: { diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index a3e6fac0295..711d13dc341 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -1,41 +1,45 @@ """Test the Snips component.""" import asyncio +import json +from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component -from tests.common import async_fire_mqtt_message, async_mock_intent - -EXAMPLE_MSG = """ -{ - "input": "turn the lights green", - "intent": { - "intentName": "Lights", - "probability": 1 - }, - "slots": [ - { - "slotName": "light_color", - "value": { - "kind": "Custom", - "value": "green" - } - } - ] -} -""" +from tests.common import (async_fire_mqtt_message, async_mock_intent, + async_mock_service) +from homeassistant.components.snips import (SERVICE_SCHEMA_SAY, + SERVICE_SCHEMA_SAY_ACTION) @asyncio.coroutine -def test_snips_call_action(hass, mqtt_mock): - """Test calling action via Snips.""" +def test_snips_intent(hass, mqtt_mock): + """Test intent via Snips.""" result = yield from async_setup_component(hass, "snips", { "snips": {}, }) assert result + payload = """ + { + "input": "turn the lights green", + "intent": { + "intentName": "Lights", + "probability": 1 + }, + "slots": [ + { + "slotName": "light_color", + "value": { + "kind": "Custom", + "value": "green" + } + } + ] + } + """ intents = async_mock_intent(hass, 'Lights') - async_fire_mqtt_message(hass, 'hermes/intent/activateLights', - EXAMPLE_MSG) + async_fire_mqtt_message(hass, 'hermes/intent/Lights', + payload) yield from hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -43,3 +47,260 @@ def test_snips_call_action(hass, mqtt_mock): assert intent.intent_type == 'Lights' assert intent.slots == {'light_color': {'value': 'green'}} assert intent.text_input == 'turn the lights green' + + +@asyncio.coroutine +def test_snips_intent_with_duration(hass, mqtt_mock): + """Test intent with Snips duration.""" + result = yield from async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + payload = """ + { + "input": "set a timer of five minutes", + "intent": { + "intentName": "SetTimer" + }, + "slots": [ + { + "rawValue": "five minutes", + "value": { + "kind": "Duration", + "years": 0, + "quarters": 0, + "months": 0, + "weeks": 0, + "days": 0, + "hours": 0, + "minutes": 5, + "seconds": 0, + "precision": "Exact" + }, + "range": { + "start": 15, + "end": 27 + }, + "entity": "snips/duration", + "slotName": "timer_duration" + } + ] + } + """ + intents = async_mock_intent(hass, 'SetTimer') + + async_fire_mqtt_message(hass, 'hermes/intent/SetTimer', + payload) + yield from hass.async_block_till_done() + assert len(intents) == 1 + intent = intents[0] + assert intent.platform == 'snips' + assert intent.intent_type == 'SetTimer' + assert intent.slots == {'timer_duration': {'value': 300}} + + +@asyncio.coroutine +def test_intent_speech_response(hass, mqtt_mock): + """Test intent speech response via Snips.""" + event = 'call_service' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + result = yield from async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + result = yield from async_setup_component(hass, "intent_script", { + "intent_script": { + "spokenIntent": { + "speech": { + "type": "plain", + "text": "I am speaking to you" + } + } + } + }) + assert result + payload = """ + { + "input": "speak to me", + "sessionId": "abcdef0123456789", + "intent": { + "intentName": "spokenIntent" + }, + "slots": [] + } + """ + hass.bus.async_listen(event, record_event) + async_fire_mqtt_message(hass, 'hermes/intent/spokenIntent', + payload) + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data['domain'] == 'mqtt' + assert events[0].data['service'] == 'publish' + payload = json.loads(events[0].data['service_data']['payload']) + topic = events[0].data['service_data']['topic'] + assert payload['sessionId'] == 'abcdef0123456789' + assert payload['text'] == 'I am speaking to you' + assert topic == 'hermes/dialogueManager/endSession' + + +@asyncio.coroutine +def test_snips_unknown_intent(hass, mqtt_mock): + """Test calling unknown Intent via Snips.""" + event = 'call_service' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + result = yield from async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + payload = """ + { + "input": "what to do", + "intent": { + "intentName": "unknownIntent" + }, + "slots": [] + } + """ + intents = async_mock_intent(hass, 'knownIntent') + hass.bus.async_listen(event, record_event) + async_fire_mqtt_message(hass, 'hermes/intent/unknownIntent', + payload) + yield from hass.async_block_till_done() + + assert not intents + assert len(events) == 1 + assert events[0].data['domain'] == 'mqtt' + assert events[0].data['service'] == 'publish' + payload = json.loads(events[0].data['service_data']['payload']) + topic = events[0].data['service_data']['topic'] + assert payload['text'] == 'Unknown Intent' + assert topic == 'hermes/dialogueManager/endSession' + + +@asyncio.coroutine +def test_snips_intent_user(hass, mqtt_mock): + """Test intentName format user_XXX__intentName.""" + result = yield from async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + payload = """ + { + "input": "what to do", + "intent": { + "intentName": "user_ABCDEF123__Lights" + }, + "slots": [] + } + """ + intents = async_mock_intent(hass, 'Lights') + async_fire_mqtt_message(hass, 'hermes/intent/user_ABCDEF123__Lights', + payload) + yield from hass.async_block_till_done() + + assert len(intents) == 1 + intent = intents[0] + assert intent.platform == 'snips' + assert intent.intent_type == 'Lights' + + +@asyncio.coroutine +def test_snips_intent_username(hass, mqtt_mock): + """Test intentName format username:intentName.""" + result = yield from async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + payload = """ + { + "input": "what to do", + "intent": { + "intentName": "username:Lights" + }, + "slots": [] + } + """ + intents = async_mock_intent(hass, 'Lights') + async_fire_mqtt_message(hass, 'hermes/intent/username:Lights', + payload) + yield from hass.async_block_till_done() + + assert len(intents) == 1 + intent = intents[0] + assert intent.platform == 'snips' + assert intent.intent_type == 'Lights' + + +@asyncio.coroutine +def test_snips_say(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say', + SERVICE_SCHEMA_SAY) + + data = {'text': 'Hello'} + yield from hass.services.async_call('snips', 'say', data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'say' + assert calls[0].data['text'] == 'Hello' + + +@asyncio.coroutine +def test_snips_say_action(hass, caplog): + """Test snips say_action with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say_action', + SERVICE_SCHEMA_SAY_ACTION) + + data = {'text': 'Hello', 'intent_filter': ['myIntent']} + yield from hass.services.async_call('snips', 'say_action', data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'say_action' + assert calls[0].data['text'] == 'Hello' + assert calls[0].data['intent_filter'] == ['myIntent'] + + +@asyncio.coroutine +def test_snips_say_invalid_config(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say', + SERVICE_SCHEMA_SAY) + + data = {'text': 'Hello', 'badKey': 'boo'} + yield from hass.services.async_call('snips', 'say', data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text + + +@asyncio.coroutine +def test_snips_say_action_invalid_config(hass, caplog): + """Test snips say_action with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say_action', + SERVICE_SCHEMA_SAY_ACTION) + + data = {'text': 'Hello', 'can_be_enqueued': 'notabool'} + yield from hass.services.async_call('snips', 'say_action', data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py index d331b73849b..28ffcac2b13 100644 --- a/tests/components/test_updater.py +++ b/tests/components/test_updater.py @@ -3,13 +3,12 @@ import asyncio from datetime import timedelta from unittest.mock import patch, Mock -from freezegun import freeze_time import pytest from homeassistant.setup import async_setup_component from homeassistant.components import updater import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, mock_coro +from tests.common import async_fire_time_changed, mock_coro, mock_component NEW_VERSION = '10000.0' MOCK_VERSION = '10.0' @@ -39,7 +38,6 @@ def mock_get_uuid(): @asyncio.coroutine -@freeze_time("Mar 15th, 2017") def test_new_version_shows_entity_after_hour( hass, mock_get_uuid, mock_get_newest_version): """Test if new entity is created if new version is available.""" @@ -59,7 +57,6 @@ def test_new_version_shows_entity_after_hour( @asyncio.coroutine -@freeze_time("Mar 15th, 2017") def test_same_version_not_show_entity( hass, mock_get_uuid, mock_get_newest_version): """Test if new entity is created if new version is available.""" @@ -79,7 +76,6 @@ def test_same_version_not_show_entity( @asyncio.coroutine -@freeze_time("Mar 15th, 2017") def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): """Test if new entity is created if new version is available.""" mock_get_uuid.return_value = MOCK_HUUID @@ -178,3 +174,24 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): Mock(return_value=mock_coro({'fake': 'bla'}))): res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None + + +@asyncio.coroutine +def test_new_version_shows_entity_after_hour_hassio( + hass, mock_get_uuid, mock_get_newest_version): + """Test if new entity is created if new version is available / hass.io.""" + mock_get_uuid.return_value = MOCK_HUUID + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, '')) + mock_component(hass, 'hassio') + hass.data['hassio_hass_version'] = "999.0" + + res = yield from async_setup_component( + hass, updater.DOMAIN, {updater.DOMAIN: {}}) + assert res, 'Updater failed to setup' + + with patch('homeassistant.components.updater.current_version', + MOCK_VERSION): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + yield from hass.async_block_till_done() + + assert hass.states.is_state(updater.ENTITY_ID, "999.0") diff --git a/tests/components/test_vultr.py b/tests/components/test_vultr.py index b504c320dc8..725768f938b 100644 --- a/tests/components/test_vultr.py +++ b/tests/components/test_vultr.py @@ -1,8 +1,11 @@ """The tests for the Vultr component.""" +from copy import deepcopy +import json import unittest +from unittest.mock import patch + import requests_mock -from copy import deepcopy from homeassistant import setup import homeassistant.components.vultr as vultr @@ -31,14 +34,11 @@ class TestVultr(unittest.TestCase): @requests_mock.Mocker() def test_setup(self, mock): """Test successful setup.""" - mock.get( - 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567', - text=load_fixture('vultr_account_info.json')) - mock.get( - 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567', - text=load_fixture('vultr_server_list.json')) - - response = vultr.setup(self.hass, self.config) + with patch( + 'vultr.Vultr.server_list', + return_value=json.loads( + load_fixture('vultr_server_list.json'))): + response = vultr.setup(self.hass, self.config) self.assertTrue(response) def test_setup_no_api_key(self): diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index f4c63d63708..f81a5c849ec 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -6,7 +6,8 @@ from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, ATTR_FAN_SPEED, mqtt) from homeassistant.components.mqtt import CONF_COMMAND_TOPIC -from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME +from homeassistant.const import ( + CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) from homeassistant.setup import setup_component from tests.common import ( fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) @@ -197,3 +198,30 @@ class TestVacuumMQTT(unittest.TestCase): state = self.hass.states.get('vacuum.mqtttest') self.assertEqual(STATE_OFF, state.state) self.assertEqual("Stopped", state.attributes.get(ATTR_STATUS)) + + def test_custom_availability_payload(self): + """Test availability by custom payload with defined topic.""" + self.default_config.update({ + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + }) + + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: self.default_config, + })) + + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('vacuum.mqtttest') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_UNAVAILABLE, state.state) diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py new file mode 100644 index 00000000000..787aca2ca17 --- /dev/null +++ b/tests/components/weather/test_darksky.py @@ -0,0 +1,54 @@ +"""The tests for the Dark Sky weather component.""" +import re +import unittest +from unittest.mock import patch + +import forecastio +import requests_mock + +from homeassistant.components import weather +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.setup import setup_component + +from tests.common import load_fixture, get_test_home_assistant + + +class TestDarkSky(unittest.TestCase): + """Test the Dark Sky weather component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = METRIC_SYSTEM + self.lat = self.hass.config.latitude = 37.8267 + self.lon = self.hass.config.longitude = -122.423 + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker() + @patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast) + def test_setup(self, mock_req, mock_get_forecast): + """Test for successfully setting up the forecast.io platform.""" + uri = (r'https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/' + r'(-?\d+\.?\d*),(-?\d+\.?\d*)') + mock_req.get(re.compile(uri), + text=load_fixture('darksky.json')) + + self.assertTrue(setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'test', + 'platform': 'darksky', + 'api_key': 'foo', + } + })) + + self.assertTrue(mock_get_forecast.called) + self.assertEqual(mock_get_forecast.call_count, 1) + + state = self.hass.states.get('weather.test') + self.assertEqual(state.state, 'Clear') + self.assertEqual(state.attributes['daily_forecast_summary'], + 'No precipitation throughout the week, with ' + 'temperatures falling to 66°F on Thursday.') diff --git a/tests/components/weather/test_yweather.py b/tests/components/weather/test_yweather.py new file mode 100644 index 00000000000..3e5eff9dae7 --- /dev/null +++ b/tests/components/weather/test_yweather.py @@ -0,0 +1,169 @@ +"""The tests for the Yahoo weather component.""" +import json + +import unittest +from unittest.mock import patch + +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED) +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.setup import setup_component + +from tests.common import (get_test_home_assistant, load_fixture, + MockDependency) + + +def _yql_queryMock(yql): # pylint: disable=invalid-name + """Mock yahoo query language query.""" + return ('{"query": {"count": 1, "created": "2017-11-17T13:40:47Z", ' + '"lang": "en-US", "results": {"place": {"woeid": "23511632"}}}}') + + +def get_woeidMock(lat, lon): # pylint: disable=invalid-name + """Mock get woeid Where On Earth Identifiers.""" + return '23511632' + + +class YahooWeatherMock(): + """Mock class for the YahooWeather object.""" + + def __init__(self, woeid, temp_unit): + """Initialize Telnet object.""" + self.woeid = woeid + self.temp_unit = temp_unit + self._data = json.loads(load_fixture('yahooweather.json')) + + # pylint: disable=no-self-use + def updateWeather(self): # pylint: disable=invalid-name + """Return sample values.""" + return True + + @property + def RawData(self): # pylint: disable=invalid-name + """Raw Data.""" + if self.woeid == '12345': + return json.loads('[]') + return self._data + + @property + def Now(self): # pylint: disable=invalid-name + """Current weather data.""" + if self.woeid == '111': + raise ValueError + return self._data['query']['results']['channel']['item']['condition'] + + @property + def Atmosphere(self): # pylint: disable=invalid-name + """Atmosphere weather data.""" + return self._data['query']['results']['channel']['atmosphere'] + + @property + def Wind(self): # pylint: disable=invalid-name + """Wind weather data.""" + return self._data['query']['results']['channel']['wind'] + + @property + def Forecast(self): # pylint: disable=invalid-name + """Forecast data 0-5 Days.""" + if self.woeid == '123123': + raise ValueError + return self._data['query']['results']['channel']['item']['forecast'] + + +class TestWeather(unittest.TestCase): + """Test the Yahoo weather component.""" + + DEVICES = [] + + def add_devices(self, devices): + """Mock add devices.""" + for device in devices: + device.update() + self.DEVICES.append(device) + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = METRIC_SYSTEM + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + @MockDependency('yahooweather') + @patch('yahooweather._yql_query', new=_yql_queryMock) + @patch('yahooweather.get_woeid', new=get_woeidMock) + @patch('yahooweather.YahooWeather', new=YahooWeatherMock) + def test_setup(self, mock_yahooweather): + """Test for typical weather data attributes.""" + self.assertTrue( + setup_component(self.hass, 'weather', { + 'weather': { + 'platform': 'yweather', + } + })) + + state = self.hass.states.get('weather.yweather') + assert state is not None + + assert state.state == 'cloudy' + + data = state.attributes + self.assertEqual(data.get(ATTR_WEATHER_TEMPERATURE), 18.0) + self.assertEqual(data.get(ATTR_WEATHER_HUMIDITY), 71) + self.assertEqual(data.get(ATTR_WEATHER_PRESSURE), 1000.0) + self.assertEqual(data.get(ATTR_WEATHER_WIND_SPEED), 3.94) + self.assertEqual(data.get(ATTR_WEATHER_WIND_BEARING), 0) + self.assertEqual(state.attributes.get('friendly_name'), 'Yweather') + + @MockDependency('yahooweather') + @patch('yahooweather._yql_query', new=_yql_queryMock) + @patch('yahooweather.get_woeid', new=get_woeidMock) + @patch('yahooweather.YahooWeather', new=YahooWeatherMock) + def test_setup_no_data(self, mock_yahooweather): + """Test for note receiving data.""" + self.assertTrue( + setup_component(self.hass, 'weather', { + 'weather': { + 'platform': 'yweather', + 'woeid': '12345', + } + })) + + state = self.hass.states.get('weather.yweather') + assert state is not None + + @MockDependency('yahooweather') + @patch('yahooweather._yql_query', new=_yql_queryMock) + @patch('yahooweather.get_woeid', new=get_woeidMock) + @patch('yahooweather.YahooWeather', new=YahooWeatherMock) + def test_setup_bad_data(self, mock_yahooweather): + """Test for bad forecast data.""" + self.assertTrue( + setup_component(self.hass, 'weather', { + 'weather': { + 'platform': 'yweather', + 'woeid': '123123', + } + })) + + state = self.hass.states.get('weather.yweather') + assert state is None + + @MockDependency('yahooweather') + @patch('yahooweather._yql_query', new=_yql_queryMock) + @patch('yahooweather.get_woeid', new=get_woeidMock) + @patch('yahooweather.YahooWeather', new=YahooWeatherMock) + def test_setup_condition_error(self, mock_yahooweather): + """Test for bad forecast data.""" + self.assertTrue( + setup_component(self.hass, 'weather', { + 'weather': { + 'platform': 'yweather', + 'woeid': '111', + } + })) + + state = self.hass.states.get('weather.yweather') + assert state is None diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 5a940742e75..26262f50ac4 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -441,6 +441,27 @@ def test_datetime(): schema('2016-11-23T18:59:08') +def test_deprecated(caplog): + """Test deprecation log.""" + schema = vol.Schema({ + 'venus': cv.boolean, + 'mars': cv.boolean + }) + deprecated_schema = vol.All( + cv.deprecated('mars'), + schema + ) + + deprecated_schema({'venus': True}) + assert len(caplog.records) == 0 + + deprecated_schema({'mars': True}) + assert len(caplog.records) == 1 + assert caplog.records[0].name == __name__ + assert ("The 'mars' option (with value 'True') is deprecated, " + "please remove it from your configuration.") in caplog.text + + def test_key_dependency(): """Test key_dependency validator.""" schema = vol.Schema(cv.key_dependency('beer', 'soda')) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a4c8b03daa0..637644ca5b3 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -31,6 +31,14 @@ def test_generate_entity_id_given_keys(): 'test.another_entity']) == 'test.overwrite_hidden_true' +def test_generate_entity_id_with_nonlatin_name(): + """Test generate_entity_id given a name containing non-latin characters.""" + fmt = 'test.{}' + assert entity.generate_entity_id( + fmt, 'ホームアシスタント', current_ids=[] + ) == 'test.unnamed_device' + + def test_async_update_support(hass): """Test async update getting called.""" sync_update = [] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a5aa093bcd5..31d98633ef8 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1,4 +1,5 @@ """Test service helpers.""" +import asyncio from copy import deepcopy import unittest from unittest.mock import patch @@ -8,6 +9,7 @@ import homeassistant.components # noqa from homeassistant import core as ha, loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import service, template +from homeassistant.setup import async_setup_component import homeassistant.helpers.config_validation as cv from tests.common import get_test_home_assistant, mock_service @@ -135,3 +137,27 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual(['group.test'], service.extract_entity_ids( self.hass, call, expand_group=False)) + + +@asyncio.coroutine +def test_async_get_all_descriptions(hass): + """Test async_get_all_descriptions.""" + group = loader.get_component('group') + group_config = {group.DOMAIN: {}} + yield from async_setup_component(hass, group.DOMAIN, group_config) + descriptions = yield from service.async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert 'description' in descriptions['group']['reload'] + assert 'fields' in descriptions['group']['reload'] + + logger = loader.get_component('logger') + logger_config = {logger.DOMAIN: {}} + yield from async_setup_component(hass, logger.DOMAIN, logger_config) + descriptions = yield from service.async_get_all_descriptions(hass) + + assert len(descriptions) == 2 + + assert 'description' in descriptions[logger.DOMAIN]['set_level'] + assert 'fields' in descriptions[logger.DOMAIN]['set_level'] diff --git a/tests/test_config.py b/tests/test_config.py index 2c8edc32f82..377c650e91f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -531,7 +531,7 @@ class TestConfig(unittest.TestCase): """Check that restart propagates to stop.""" process_mock = mock.MagicMock() attrs = { - 'communicate.return_value': mock_coro(('output', 'error')), + 'communicate.return_value': mock_coro((b'output', None)), 'wait.return_value': mock_coro(0)} process_mock.configure_mock(**attrs) mock_create.return_value = mock_coro(process_mock) @@ -546,7 +546,7 @@ class TestConfig(unittest.TestCase): process_mock = mock.MagicMock() attrs = { 'communicate.return_value': - mock_coro(('\033[34mhello'.encode('utf-8'), 'error')), + mock_coro(('\033[34mhello'.encode('utf-8'), None)), 'wait.return_value': mock_coro(1)} process_mock.configure_mock(**attrs) mock_create.return_value = mock_coro(process_mock) diff --git a/tests/test_core.py b/tests/test_core.py index 09ddf721628..ea952a7c073 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -640,10 +640,7 @@ class TestServiceRegistry(unittest.TestCase): def test_services(self): """Test services.""" - expected = { - 'test_domain': {'test_service': {'description': '', 'fields': {}}} - } - self.assertEqual(expected, self.services.services) + assert len(self.services.services) == 1 def test_call_with_blocking_done_in_time(self): """Test call with blocking.""" @@ -800,8 +797,10 @@ class TestConfig(unittest.TestCase): def test_is_allowed_path(self): """Test is_allowed_path method.""" with TemporaryDirectory() as tmp_dir: + # The created dir is in /tmp. This is a symlink on OS X + # causing this test to fail unless we resolve path first. self.config.whitelist_external_dirs = set(( - tmp_dir, + os.path.realpath(tmp_dir), )) test_file = os.path.join(tmp_dir, "test.jpg") diff --git a/tests/test_setup.py b/tests/test_setup.py index 9a0f85874ad..afea30ddcd1 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -456,7 +456,7 @@ def test_component_warn_slow_setup(hass): hass, 'test_component1', {}) assert result assert mock_call.called - assert len(mock_call.mock_calls) == 2 + assert len(mock_call.mock_calls) == 3 timeout, logger_method = mock_call.mock_calls[0][1][:2] @@ -464,3 +464,17 @@ def test_component_warn_slow_setup(hass): assert logger_method == setup._LOGGER.warning assert mock_call().cancel.called + + +@asyncio.coroutine +def test_platform_no_warn_slow(hass): + """Do not warn for long entity setup time.""" + loader.set_component( + 'test_component1', + MockModule('test_component1', platform_schema=PLATFORM_SCHEMA)) + with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ + as mock_call: + result = yield from setup.async_setup_component( + hass, 'test_component1', {}) + assert result + assert not mock_call.called diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index ccd71e55d16..d11a71d541f 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -7,6 +7,8 @@ from unittest import mock from urllib.parse import urlparse, parse_qs import yarl +from aiohttp.client_exceptions import ClientResponseError + class AiohttpClientMocker: """Mock Aiohttp client requests.""" @@ -81,7 +83,7 @@ class AiohttpClientMocker: data = data or json for response in self._mocks: if response.match_request(method, url, params): - self.mock_calls.append((method, url, data)) + self.mock_calls.append((method, url, data, headers)) if response.exc: raise response.exc @@ -189,6 +191,12 @@ class AiohttpClientMockResponse: """Mock release.""" pass + def raise_for_status(self): + """Raise error if status is 400 or higher.""" + if self.status >= 400: + raise ClientResponseError( + None, None, code=self.status, headers=self.headers) + def close(self): """Mock close.""" pass diff --git a/tox.ini b/tox.ini index f3e58ce8889..70612658715 100644 --- a/tox.ini +++ b/tox.ini @@ -12,13 +12,13 @@ setenv = whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = - py.test --timeout=15 --duration=10 --cov --cov-report= {posargs} + py.test --timeout=9 --duration=10 --cov --cov-report= {posargs} deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt [testenv:pylint] -basepython = python3 +basepython = {env:PYTHON3_PATH:python3} ignore_errors = True deps = -r{toxinidir}/requirements_all.txt @@ -28,7 +28,7 @@ commands = pylint homeassistant [testenv:lint] -basepython = python3 +basepython = {env:PYTHON3_PATH:python3} deps = -r{toxinidir}/requirements_test.txt commands = @@ -37,7 +37,7 @@ commands = pydocstyle homeassistant tests [testenv:typing] -basepython = python3 +basepython = {env:PYTHON3_PATH:python3} deps = -r{toxinidir}/requirements_test.txt commands =