diff --git a/.coveragerc b/.coveragerc index e44a905a0ab..a63e0e48980 100644 --- a/.coveragerc +++ b/.coveragerc @@ -26,6 +26,9 @@ omit = homeassistant/components/zwave.py homeassistant/components/*/zwave.py + homeassistant/components/rfxtrx.py + homeassistant/components/*/rfxtrx.py + homeassistant/components/ifttt.py homeassistant/components/browser.py homeassistant/components/camera/* @@ -39,13 +42,16 @@ omit = homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tplink.py + homeassistant/components/device_tracker/snmp.py homeassistant/components/discovery.py homeassistant/components/downloader.py homeassistant/components/keyboard.py homeassistant/components/light/hue.py homeassistant/components/light/limitlessled.py + homeassistant/components/light/blinksticklight.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py + homeassistant/components/media_player/firetv.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py @@ -60,10 +66,12 @@ omit = homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py + homeassistant/components/notify/telegram.py homeassistant/components/notify/xmpp.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/command_sensor.py + homeassistant/components/sensor/cpuinfo.py homeassistant/components/sensor/dht.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/forecast.py @@ -71,7 +79,6 @@ omit = homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py - homeassistant/components/sensor/rfxtrx.py homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/swiss_public_transport.py diff --git a/MANIFEST.in b/MANIFEST.in index 53d3a9e22a9..8233015e646 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,5 @@ -recursive-exclude tests * -recursive-include homeassistant services.yaml +include README.md +include LICENSE +graft homeassistant +prune homeassistant/components/frontend/www_static/home-assistant-polymer +recursive-exclude * *.py[co] diff --git a/README.md b/README.md index 6b1b1353392..2fed012402c 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,12 @@ Check out [the website](https://home-assistant.io) for [a demo][demo], installat Examples of devices it can interface it: - * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) + * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT + * * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)) - * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) + * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)), and Amazon Fire TV (by way of [python-firetv](https://github.com/happyleavesaoc/python-firetv)) + * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) + * Interaction with [IFTTT](https://ifttt.com/) * Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org). * [See full list of supported devices](https://home-assistant.io/components/) @@ -29,8 +31,8 @@ Built home automation on top of your devices: * Turn on the lights when people get home after sun set * Turn on lights slowly during sun set to compensate for less light * Turn off all lights and devices when everybody leaves the house - * Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects - * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org) + * Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects like [OwnTracks](http://owntracks.org/) + * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), [Telegram](https://telegram.org/), and [Jabber (XMPP)](http://xmpp.org) The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html). diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index ae9959a193d..fae945b05e4 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -134,6 +134,9 @@ automation: service: light.turn_off entity_id: group.all_lights +# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc. +# Each sensor label should be unique or your sensors might not load correctly. + sensor: platform: systemmonitor resources: diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index b85305b6d18..d3289e08e62 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -8,7 +8,7 @@ import os from homeassistant.components import verisure from homeassistant.const import ( - ATTR_ENTITY_ID, + ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import Entity @@ -29,6 +29,7 @@ SERVICE_TO_METHOD = { SERVICE_ALARM_DISARM: 'alarm_disarm', SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', + SERVICE_ALARM_TRIGGER: 'alarm_trigger' } ATTR_CODE = 'code' @@ -53,9 +54,9 @@ def setup(hass, config): target_alarms = component.extract_from_service(service) if ATTR_CODE not in service.data: - return - - code = service.data[ATTR_CODE] + code = None + else: + code = service.data[ATTR_CODE] method = SERVICE_TO_METHOD[service.service] @@ -72,36 +73,50 @@ def setup(hass, config): return True -def alarm_disarm(hass, code, entity_id=None): +def alarm_disarm(hass, code=None, entity_id=None): """ Send the alarm the command for disarm. """ - data = {ATTR_CODE: code} - + data = {} + if code: + data[ATTR_CODE] = code if entity_id: data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) -def alarm_arm_home(hass, code, entity_id=None): +def alarm_arm_home(hass, code=None, entity_id=None): """ Send the alarm the command for arm home. """ - data = {ATTR_CODE: code} - + data = {} + if code: + data[ATTR_CODE] = code if entity_id: data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) -def alarm_arm_away(hass, code, entity_id=None): +def alarm_arm_away(hass, code=None, entity_id=None): """ Send the alarm the command for arm away. """ - data = {ATTR_CODE: code} - + data = {} + if code: + data[ATTR_CODE] = code if entity_id: data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) +def alarm_trigger(hass, code=None, entity_id=None): + """ Send the alarm the command for trigger. """ + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) + + # pylint: disable=no-self-use class AlarmControlPanel(Entity): """ ABC for alarm control devices. """ @@ -123,6 +138,10 @@ class AlarmControlPanel(Entity): """ Send arm away command. """ raise NotImplementedError() + def alarm_trigger(self, code=None): + """ Send alarm trigger command. """ + raise NotImplementedError() + @property def state_attributes(self): """ Return the state attributes. """ diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py new file mode 100644 index 00000000000..c379dca3199 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -0,0 +1,148 @@ +""" +homeassistant.components.alarm_control_panel.manual +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for manual alarms. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.manual.html +""" +import logging +import datetime +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.helpers.event import track_point_in_time +import homeassistant.util.dt as dt_util + +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = [] + +DEFAULT_ALARM_NAME = 'HA Alarm' +DEFAULT_PENDING_TIME = 60 +DEFAULT_TRIGGER_TIME = 120 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the manual alarm platform. """ + + add_devices([ManualAlarm( + hass, + config.get('name', DEFAULT_ALARM_NAME), + config.get('code'), + config.get('pending_time', DEFAULT_PENDING_TIME), + config.get('trigger_time', DEFAULT_TRIGGER_TIME), + )]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method +class ManualAlarm(alarm.AlarmControlPanel): + """ + Represents an alarm status. + + When armed, will be pending for 'pending_time', after that armed. + When triggered, will be pending for 'trigger_time'. After that will be + triggered for 'trigger_time', after that we return to disarmed. + """ + + def __init__(self, hass, name, code, pending_time, trigger_time): + self._state = STATE_ALARM_DISARMED + self._hass = hass + self._name = name + self._code = str(code) if code else None + self._pending_time = datetime.timedelta(seconds=pending_time) + self._trigger_time = datetime.timedelta(seconds=trigger_time) + self._state_ts = None + + @property + def should_poll(self): + """ No polling needed. """ + return False + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \ + self._pending_time and self._state_ts + self._pending_time > \ + dt_util.utcnow(): + return STATE_ALARM_PENDING + + if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state_ts + self._trigger_time > dt_util.utcnow(): + return STATE_ALARM_PENDING + elif dt_util.utcnow() >= self._state_ts + (2 * self._trigger_time): + return STATE_ALARM_DISARMED + + return self._state + + @property + def code_format(self): + """ One or more characters. """ + return None if self._code is None else '.+' + + def alarm_disarm(self, code=None): + """ Send disarm command. """ + if not self._validate_code(code, STATE_ALARM_DISARMED): + return + + self._state = STATE_ALARM_DISARMED + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + def alarm_arm_home(self, code=None): + """ Send arm home command. """ + if not self._validate_code(code, STATE_ALARM_ARMED_HOME): + return + + self._state = STATE_ALARM_ARMED_HOME + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._pending_time) + + def alarm_arm_away(self, code=None): + """ Send arm away command. """ + if not self._validate_code(code, STATE_ALARM_ARMED_AWAY): + return + + self._state = STATE_ALARM_ARMED_AWAY + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._pending_time) + + def alarm_trigger(self, code=None): + """ Send alarm trigger command. No code needed. """ + self._state = STATE_ALARM_TRIGGERED + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + if self._trigger_time: + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._trigger_time) + + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + 2 * self._trigger_time) + + def _validate_code(self, code, state): + """ Validate given code. """ + check = self._code is None or code == self._code + if not check: + _LOGGER.warning('Invalid code given for %s', state) + return check diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index c04c8ee6031..e070babd080 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -1,68 +1,18 @@ """ homeassistant.components.alarm_control_panel.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This platform enables the possibility to control a MQTT alarm. -In this platform, 'state_topic' and 'command_topic' are required. -The alarm will only change state after receiving the a new state -from 'state_topic'. If these messages are published with RETAIN flag, -the MQTT alarm will receive an instant state update after subscription -and will start with correct state. Otherwise, the initial state will -be 'unknown'. -Configuration: - -alarm_control_panel: - platform: mqtt - name: "MQTT Alarm" - state_topic: "home/alarm" - command_topic: "home/alarm/set" - qos: 0 - payload_disarm: "DISARM" - payload_arm_home: "ARM_HOME" - payload_arm_away: "ARM_AWAY" - code: "mySecretCode" - -Variables: - -name -*Optional -The name of the alarm. Default is 'MQTT Alarm'. - -state_topic -*Required -The MQTT topic subscribed to receive state updates. - -command_topic -*Required -The MQTT topic to publish commands to change the alarm state. - -qos -*Optional -The maximum QoS level of the state topic. Default is 0. -This QoS will also be used to publishing messages. - -payload_disarm -*Optional -The payload do disarm alarm. Default is "DISARM". - -payload_arm_home -*Optional -The payload to set armed-home mode. Default is "ARM_HOME". - -payload_arm_away -*Optional -The payload to set armed-away mode. Default is "ARM_AWAY". - -code -*Optional -If defined, specifies a code to enable or disable the alarm in the frontend. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.mqtt.html """ import logging import homeassistant.components.mqtt as mqtt import homeassistant.components.alarm_control_panel as alarm -from homeassistant.const import (STATE_UNKNOWN) +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) @@ -99,6 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method class MqttAlarm(alarm.AlarmControlPanel): """ represents a MQTT alarm status within home assistant. """ @@ -113,10 +64,15 @@ class MqttAlarm(alarm.AlarmControlPanel): self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away - self._code = code + self._code = str(code) if code else None def message_received(topic, payload, qos): """ A new MQTT message has been received. """ + if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED): + _LOGGER.warning('Received unexpected payload: %s', payload) + return self._state = payload self.update_ha_state() @@ -144,24 +100,28 @@ class MqttAlarm(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """ Send disarm command. """ - if code == str(self._code) or self.code_format is None: - mqtt.publish(self.hass, self._command_topic, - self._payload_disarm, self._qos) - else: - _LOGGER.warning("Wrong code entered while disarming!") + if not self._validate_code(code, 'disarming'): + return + mqtt.publish(self.hass, self._command_topic, + self._payload_disarm, self._qos) def alarm_arm_home(self, code=None): """ Send arm home command. """ - if code == str(self._code) or self.code_format is None: - mqtt.publish(self.hass, self._command_topic, - self._payload_arm_home, self._qos) - else: - _LOGGER.warning("Wrong code entered while arming home!") + if not self._validate_code(code, 'arming home'): + return + mqtt.publish(self.hass, self._command_topic, + self._payload_arm_home, self._qos) def alarm_arm_away(self, code=None): """ Send arm away command. """ - if code == str(self._code) or self.code_format is None: - mqtt.publish(self.hass, self._command_topic, - self._payload_arm_away, self._qos) - else: - _LOGGER.warning("Wrong code entered while arming away!") + if not self._validate_code(code, 'arming away'): + return + mqtt.publish(self.hass, self._command_topic, + self._payload_arm_away, self._qos) + + def _validate_code(self, code, state): + """ Validate given code. """ + check = self._code is None or code == self._code + if not check: + _LOGGER.warning('Wrong code entered for %s', state) + return check diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index c7c24a60c4a..9e0475592bd 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -33,6 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(alarms) +# pylint: disable=abstract-method class VerisureAlarm(alarm.AlarmControlPanel): """ Represents a Verisure alarm status. """ diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index c5b0ee47923..c172b8e0e11 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.event ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers event listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#event-trigger """ import logging diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 3f85792f907..706d97824b4 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers MQTT listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#mqtt-trigger """ import logging diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 7e014213d62..1ddfb91a334 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.numeric_state ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers numeric state listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#numeric-state-trigger """ import logging diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 8baa0a01d46..52379355d6b 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.state ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers state listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#state-trigger """ import logging @@ -28,6 +30,11 @@ def trigger(hass, config, action): from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL + if isinstance(from_state, bool) or isinstance(to_state, bool): + logging.getLogger(__name__).error( + 'Config error. Surround to/from values with quotes.') + return False + def state_automation_listener(entity, from_s, to_s): """ Listens for state changes and calls action. """ action() diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 103df6c9b39..c72474ae4dd 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.sun ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers sun based automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#sun-trigger """ import logging from datetime import timedelta diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 1d97ccc135d..2f05c6f4390 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers time listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#time-trigger """ import logging diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index f62aec8bf2a..28d1c8456f0 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.zone ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers zone automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#zone-trigger """ import logging diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 78fd0f4d2e1..24a42cbf883 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -3,42 +3,6 @@ homeassistant.components.camera.foscam ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This component provides basic support for Foscam IP cameras. -As part of the basic support the following features will be provided: --MJPEG video streaming - -To use this component, add the following to your configuration.yaml file. - -camera: - platform: foscam - name: Door Camera - ip: 192.168.0.123 - port: 88 - username: YOUR_USERNAME - password: YOUR_PASSWORD - -Variables: - -ip -*Required -The IP address of your Foscam device. - -username -*Required -The username of a visitor or operator of your camera. Oddly admin accounts -don't seem to have access to take snapshots. - -password -*Required -The password for accessing your camera. - -name -*Optional -This parameter allows you to override the name of your camera in homeassistant. - -port -*Optional -The port that the camera is running on. The default is 88. - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.foscam.html """ diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 7e4a24ffdfe..74d2d0102d3 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -3,43 +3,8 @@ homeassistant.components.camera.generic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for IP Cameras. -This component provides basic support for IP cameras. For the basic support to -work you camera must support accessing a JPEG snapshot via a URL and you will -need to specify the "still_image_url" parameter which should be the location of -the JPEG image. - -As part of the basic support the following features will be provided: -- MJPEG video streaming -- Saving a snapshot -- Recording(JPEG frame capture) - -To use this component, add the following to your configuration.yaml file. - -camera: - platform: generic - name: Door Camera - username: YOUR_USERNAME - password: YOUR_PASSWORD - still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg - -Variables: - -still_image_url -*Required -The URL your camera serves the image on, eg. http://192.168.1.21:2112/ - -name -*Optional -This parameter allows you to override the name of your camera in Home -Assistant. - -username -*Optional -The username for accessing your camera. - -password -*Optional -The password for accessing your camera. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.generic.html """ import logging from requests.auth import HTTPBasicAuth diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index f22135ec5bc..388a869ae0c 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -63,6 +63,14 @@ def setup(hass, config): 'still_image_url': 'http://home-assistant.io/demo/webcam.jpg', }}) + # Setup alarm_control_panel + bootstrap.setup_component( + hass, 'alarm_control_panel', + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'Test Alarm', + }}) + # Setup scripts bootstrap.setup_component( hass, 'script', diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 55359b393ec..9fe18585418 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -114,6 +114,8 @@ def setup(hass, config): os.remove(csv_path) conf = config.get(DOMAIN, {}) + if isinstance(conf, list): + conf = conf[0] consider_home = timedelta( seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int, DEFAULT_CONSIDER_HOME)) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index f926b182983..a2f94f34c3a 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -4,41 +4,8 @@ homeassistant.components.device_tracker.actiontec Device tracker platform that supports scanning an Actiontec MI424WR (Verizon FIOS) router for device presence. -This device tracker needs telnet to be enabled on the router. - -Configuration: - -To use the Actiontec tracker you will need to add something like the -following to your configuration.yaml file. If you experience disconnects -you can modify the home_interval variable. - -device_tracker: - platform: actiontec - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - # optional: - home_interval: 10 - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. - -home_interval -*Optional -If the home_interval is set then the component will not let a device -be AWAY if it has been HOME in the last home_interval minutes. This is -in addition to the 3 minute wait built into the device_tracker component. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.actiontec.html """ import logging from datetime import timedelta @@ -56,7 +23,7 @@ from homeassistant.components.device_tracker import DOMAIN # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) -# interval in minutes to exclude devices from a scan while they are home +# Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index 68ff8390216..d46264fa264 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -4,33 +4,8 @@ homeassistant.components.device_tracker.aruba Device tracker platform that supports scanning a Aruba Access Point for device presence. -This device tracker needs telnet to be enabled on the router. - -Configuration: - -To use the Aruba tracker you will need to add something like the following -to your configuration.yaml file. You also need to enable Telnet in the -configuration page of your router. - -device_tracker: - platform: aruba - host: YOUR_ACCESS_POINT_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.aruba.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 1e3ac20b6f2..5284d45835b 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -161,9 +161,10 @@ class AsusWrtDeviceScanner(object): # For leases where the client doesn't set a hostname, ensure # it is blank and not '*', which breaks the entity_id down # the line - host = match.group('host') - if host == '*': - host = '' + if match: + host = match.group('host') + if host == '*': + host = '' devices[match.group('ip')] = { 'host': host, @@ -174,6 +175,6 @@ class AsusWrtDeviceScanner(object): for neighbor in neighbors: match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) - if match.group('ip') in devices: + if match and match.group('ip') in devices: devices[match.group('ip')]['status'] = match.group('status') return devices diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index a9a4ac8e3f5..d8734a55a17 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -4,30 +4,8 @@ homeassistant.components.device_tracker.ddwrt Device tracker platform that supports scanning a DD-WRT router for device presence. -Configuration: - -To use the DD-WRT tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: ddwrt - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ddwrt.html """ import logging from datetime import timedelta @@ -46,6 +24,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') +_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') # pylint: disable=unused-argument @@ -77,7 +56,7 @@ class DdWrtDeviceScanner(object): self.last_results = {} - self.mac2name = None + self.mac2name = {} # Test the router is accessible url = 'http://{}/Status_Wireless.live.asp'.format(self.host) @@ -98,30 +77,33 @@ class DdWrtDeviceScanner(object): with self.lock: # if not initialised and not already scanned and not found - if self.mac2name is None or device not in self.mac2name: + if device not in self.mac2name: url = 'http://{}/Status_Lan.live.asp'.format(self.host) data = self.get_ddwrt_data(url) if not data: - return + return None dhcp_leases = data.get('dhcp_leases', None) - if dhcp_leases: - # remove leading and trailing single quotes - cleaned_str = dhcp_leases.strip().strip('"') - elements = cleaned_str.split('","') - num_clients = int(len(elements)/5) - self.mac2name = {} - for idx in range(0, num_clients): - # this is stupid but the data is a single array - # every 5 elements represents one hosts, the MAC - # is the third element and the name is the first - mac_index = (idx * 5) + 2 - if mac_index < len(elements): - mac = elements[mac_index] - self.mac2name[mac] = elements[idx * 5] - return self.mac2name.get(device, None) + if not dhcp_leases: + return None + + # remove leading and trailing single quotes + cleaned_str = dhcp_leases.strip().strip('"') + elements = cleaned_str.split('","') + num_clients = int(len(elements)/5) + self.mac2name = {} + for idx in range(0, num_clients): + # this is stupid but the data is a single array + # every 5 elements represents one hosts, the MAC + # is the third element and the name is the first + mac_index = (idx * 5) + 2 + if mac_index < len(elements): + mac = elements[mac_index] + self.mac2name[mac] = elements[idx * 5] + + return self.mac2name.get(device) @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): @@ -141,29 +123,25 @@ class DdWrtDeviceScanner(object): if not data: return False - if data: - self.last_results = [] - active_clients = data.get('active_wireless', None) - if active_clients: - # This is really lame, instead of using JSON the DD-WRT UI - # uses its own data format for some reason and then - # regex's out values so I guess I have to do the same, - # LAME!!! + self.last_results = [] - # remove leading and trailing single quotes - clean_str = active_clients.strip().strip("'") - elements = clean_str.split("','") + active_clients = data.get('active_wireless', None) + if not active_clients: + return False - num_clients = int(len(elements)/9) - for idx in range(0, num_clients): - # get every 9th element which is the MAC address - index = idx * 9 - if index < len(elements): - self.last_results.append(elements[index]) + # This is really lame, instead of using JSON the DD-WRT UI + # uses its own data format for some reason and then + # regex's out values so I guess I have to do the same, + # LAME!!! - return True + # remove leading and trailing single quotes + clean_str = active_clients.strip().strip("'") + elements = clean_str.split("','") - return False + self.last_results.extend(item for item in elements + if _MAC_REGEX.match(item)) + + return True def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result. """ diff --git a/homeassistant/components/device_tracker/geofancy.py b/homeassistant/components/device_tracker/geofancy.py new file mode 100644 index 00000000000..91d3978326b --- /dev/null +++ b/homeassistant/components/device_tracker/geofancy.py @@ -0,0 +1,71 @@ +""" +homeassistant.components.device_tracker.geofancy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Geofancy platform for the device tracker. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.geofancy.html +""" + +from homeassistant.const import ( + HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) + +DEPENDENCIES = ['http'] + +_SEE = 0 + +URL_API_GEOFANCY_ENDPOINT = "/api/geofancy" + + +def setup_scanner(hass, config, see): + """ Set up an endpoint for the Geofancy app. """ + + # Use a global variable to keep setup_scanner compact when using a callback + global _SEE + _SEE = see + + # POST would be semantically better, but that currently does not work + # since Geofancy sends the data as key1=value1&key2=value2 + # in the request body, while Home Assistant expects json there. + + hass.http.register_path( + 'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy) + + return True + + +def _handle_get_api_geofancy(handler, path_match, data): + """ Geofancy message received. """ + + if not isinstance(data, dict): + handler.write_json_message( + "Error while parsing Geofancy message.", + HTTP_INTERNAL_SERVER_ERROR) + return + if 'latitude' not in data or 'longitude' not in data: + handler.write_json_message( + "Location not specified.", + HTTP_UNPROCESSABLE_ENTITY) + return + if 'device' not in data or 'id' not in data: + handler.write_json_message( + "Device id or location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + return + + try: + gps_coords = (float(data['latitude']), float(data['longitude'])) + except ValueError: + # If invalid latitude / longitude format + handler.write_json_message( + "Invalid latitude / longitude format.", + HTTP_UNPROCESSABLE_ENTITY) + return + + # entity id's in Home Assistant must be alphanumerical + device_uuid = data['device'] + device_entity_id = device_uuid.replace('-', '') + + _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) + + handler.write_json_message("Geofancy message processed") diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 4cbc6a2d492..2ce032f90fd 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -4,33 +4,8 @@ homeassistant.components.device_tracker.luci Device tracker platform that supports scanning a OpenWRT router for device presence. -It's required that the luci RPC package is installed on the OpenWRT router: -# opkg install luci-mod-rpc - -Configuration: - -To use the Luci tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: luci - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.luci.html """ import logging import json diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 34cee8f6733..f78cb3420f5 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -1,15 +1,10 @@ """ homeassistant.components.device_tracker.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - MQTT platform for the device tracker. -device_tracker: - platform: mqtt - qos: 1 - devices: - paulus_oneplus: /location/paulus - annetherese_n4: /location/annetherese +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.mqtt.html """ import logging from homeassistant import util diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 46c515dcb1f..2d138cf5c70 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -4,30 +4,8 @@ homeassistant.components.device_tracker.netgear Device tracker platform that supports scanning a Netgear router for device presence. -Configuration: - -To use the Netgear tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: netgear - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.netgear.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 8d9c2e72c20..fe6b814b96f 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -3,26 +3,8 @@ homeassistant.components.device_tracker.nmap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Device tracker platform that supports scanning a network with nmap. -Configuration: - -To use the nmap tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: nmap_tracker - hosts: 192.168.1.1/24 - -Variables: - -hosts -*Required -The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or -the range notation (192.168.1.1-255). - -home_interval -*Optional -Number of minutes it will not scan devices that it found in previous results. -This is to save battery. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.nmap_scanner.html """ import logging from datetime import timedelta @@ -117,15 +99,18 @@ class NmapDeviceScanner(object): scanner = PortScanner() options = "-F --host-timeout 5" - exclude_targets = set() + if self.home_interval: - now = dt_util.now() - for host in self.last_results: - if host.last_update + self.home_interval > now: - exclude_targets.add(host) - if len(exclude_targets) > 0: - target_list = [t.ip for t in exclude_targets] - options += " --exclude {}".format(",".join(target_list)) + boundary = dt_util.now() - self.home_interval + last_results = [device for device in self.last_results + if device.last_update > boundary] + if last_results: + # Pylint is confused here. + # pylint: disable=no-member + options += " --exclude {}".format(",".join(device.ip for device + in last_results)) + else: + last_results = [] try: result = scanner.scan(hosts=self.hosts, arguments=options) @@ -133,18 +118,17 @@ class NmapDeviceScanner(object): return False now = dt_util.now() - self.last_results = [] for ipv4, info in result['scan'].items(): if info['status']['state'] != 'up': continue - name = info['hostnames'][0] if info['hostnames'] else ipv4 + name = info['hostnames'][0]['name'] if info['hostnames'] else ipv4 # Mac address only returned if nmap ran as root mac = info['addresses'].get('mac') or _arp(ipv4) if mac is None: continue - device = Device(mac.upper(), name, ipv4, now) - self.last_results.append(device) - self.last_results.extend(exclude_targets) + last_results.append(Device(mac.upper(), name, ipv4, now)) + + self.last_results = last_results _LOGGER.info("nmap scan successful") return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 505fd6b7ad2..78fd42f1566 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,11 +1,10 @@ """ homeassistant.components.device_tracker.owntracks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - OwnTracks platform for the device tracker. -device_tracker: - platform: owntracks +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.owntracks.html """ import json import logging diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py new file mode 100644 index 00000000000..21bcdfb2a93 --- /dev/null +++ b/homeassistant/components/device_tracker/snmp.py @@ -0,0 +1,119 @@ +""" +homeassistant.components.device_tracker.snmp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports fetching WiFi associations +through SNMP. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.snmp.html +""" +import logging +from datetime import timedelta +import threading +import binascii + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pysnmp==4.2.5'] + +CONF_COMMUNITY = "community" +CONF_BASEOID = "baseoid" + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns an snmp scanner """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_COMMUNITY, CONF_BASEOID]}, + _LOGGER): + return None + + scanner = SnmpScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class SnmpScanner(object): + """ + This class queries any SNMP capable Acces Point for connected devices. + """ + def __init__(self, config): + self.host = config[CONF_HOST] + self.community = config[CONF_COMMUNITY] + self.baseoid = config[CONF_BASEOID] + + self.lock = threading.Lock() + + self.last_results = [] + + # Test the router is accessible + data = self.get_snmp_data() + self.success_init = data is not None + + def scan_devices(self): + """ + Scans for new devices and return a list containing found device IDs. + """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + # Supressing no-self-use warning + # pylint: disable=R0201 + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + # We have no names + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensures the information from the WAP is up to date. + Returns boolean if scanning successful. + """ + if not self.success_init: + return False + + with self.lock: + data = self.get_snmp_data() + if not data: + return False + + self.last_results = data + return True + + def get_snmp_data(self): + """ Fetch mac addresses from WAP via SNMP. """ + from pysnmp.entity.rfc3413.oneliner import cmdgen + + devices = [] + + snmp = cmdgen.CommandGenerator() + errindication, errstatus, errindex, restable = snmp.nextCmd( + cmdgen.CommunityData(self.community), + cmdgen.UdpTransportTarget((self.host, 161)), + cmdgen.MibVariable(self.baseoid) + ) + + if errindication: + _LOGGER.error("SNMPLIB error: %s", errindication) + return + if errstatus: + _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), + errindex and restable[-1][int(errindex)-1] + or '?') + return + + for resrow in restable: + for _, val in resrow: + mac = binascii.hexlify(val.asOctets()).decode('utf-8') + mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)]) + devices.append({'mac': mac}) + return devices diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py index 408daa94d81..c6679e6c320 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -4,32 +4,8 @@ homeassistant.components.device_tracker.thomson Device tracker platform that supports scanning a THOMSON router for device presence. -This device tracker needs telnet to be enabled on the router. - -Configuration: - -To use the THOMSON tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: thomson - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.thomson.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index a23b7b80ff0..df0c9c8d93d 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -4,36 +4,8 @@ homeassistant.components.device_tracker.tomato Device tracker platform that supports scanning a Tomato router for device presence. -Configuration: - -To use the Tomato tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: tomato - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - http_id: ABCDEFG - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. - -http_id -*Required -The value can be obtained by logging in to the Tomato admin interface and -search for http_id in the page source code. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.tomato.html """ import logging import json diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 6b12000cf45..3769229f101 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -4,30 +4,8 @@ homeassistant.components.device_tracker.tplink Device tracker platform that supports scanning a TP-Link router for device presence. -Configuration: - -To use the TP-Link tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: tplink - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.tplink.html """ import base64 import logging @@ -54,10 +32,13 @@ def get_scanner(hass, config): _LOGGER): return None - scanner = Tplink2DeviceScanner(config[DOMAIN]) + scanner = Tplink3DeviceScanner(config[DOMAIN]) if not scanner.success_init: - scanner = TplinkDeviceScanner(config[DOMAIN]) + scanner = Tplink2DeviceScanner(config[DOMAIN]) + + if not scanner.success_init: + scanner = TplinkDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -156,7 +137,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): with self.lock: _LOGGER.info("Loading wireless clients...") - url = 'http://{}/data/map_access_wireless_client_grid.json'\ + url = 'http://{}/data/map_access_wireless_client_grid.json' \ .format(self.host) referer = 'http://{}'.format(self.host) @@ -166,7 +147,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): b64_encoded_username_password = base64.b64encode( username_password.encode('ascii') ).decode('ascii') - cookie = 'Authorization=Basic {}'\ + cookie = 'Authorization=Basic {}' \ .format(b64_encoded_username_password) response = requests.post(url, headers={'referer': referer, @@ -183,7 +164,119 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): self.last_results = { device['mac_addr'].replace('-', ':'): device['name'] for device in result - } + } + return True + + return False + + +class Tplink3DeviceScanner(TplinkDeviceScanner): + """ + This class queries the Archer C9 router running version 150811 or higher + of TP-Link firmware for connected devices. + """ + + def __init__(self, config): + self.stok = '' + self.sysauth = '' + super(Tplink3DeviceScanner, self).__init__(config) + + def scan_devices(self): + """ + Scans for new devices and return a list containing found device ids. + """ + + self._update_info() + return self.last_results.keys() + + # pylint: disable=no-self-use + def get_device_name(self, device): + """ + The TP-Link firmware doesn't save the name of the wireless device. + We are forced to use the MAC address as name here. + """ + + return self.last_results.get(device) + + def _get_auth_tokens(self): + """ + Retrieves auth tokens from the router. + """ + + _LOGGER.info("Retrieving auth tokens...") + + url = 'http://{}/cgi-bin/luci/;stok=/login?form=login' \ + .format(self.host) + referer = 'http://{}/webpages/login.html'.format(self.host) + + # if possible implement rsa encryption of password here + + response = requests.post(url, + params={'operation': 'login', + 'username': self.username, + 'password': self.password}, + headers={'referer': referer}) + + try: + self.stok = response.json().get('data').get('stok') + _LOGGER.info(self.stok) + regex_result = re.search('sysauth=(.*);', + response.headers['set-cookie']) + self.sysauth = regex_result.group(1) + _LOGGER.info(self.sysauth) + return True + except ValueError: + _LOGGER.error("Couldn't fetch auth tokens!") + return False + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensures the information from the TP-Link router is up to date. + Returns boolean if scanning successful. + """ + + with self.lock: + if (self.stok == '') or (self.sysauth == ''): + self._get_auth_tokens() + + _LOGGER.info("Loading wireless clients...") + + url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \ + .format(self.host, self.stok) + referer = 'http://{}/webpages/index.html'.format(self.host) + + response = requests.post(url, + params={'operation': 'load'}, + headers={'referer': referer}, + cookies={'sysauth': self.sysauth}) + + try: + json_response = response.json() + + if json_response.get('success'): + result = response.json().get('data') + else: + if json_response.get('errorcode') == 'timeout': + _LOGGER.info("Token timed out. " + "Relogging on next scan.") + self.stok = '' + self.sysauth = '' + return False + else: + _LOGGER.error("An unknown error happened " + "while fetching data.") + return False + except ValueError: + _LOGGER.error("Router didn't respond with JSON. " + "Check if credentials are correct.") + return False + + if result: + self.last_results = { + device['mac'].replace('-', ':'): device['mac'] + for device in result + } return True return False diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 419e48d55b5..b327e510cd8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -11,6 +11,7 @@ import logging from . import version import homeassistant.util as util from homeassistant.const import URL_ROOT, HTTP_OK +from homeassistant.config import get_default_config_dir DOMAIN = 'frontend' DEPENDENCIES = ['api'] @@ -19,7 +20,6 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template') _LOGGER = logging.getLogger(__name__) - FRONTEND_URLS = [ URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', '/devEvent'] @@ -44,6 +44,9 @@ def setup(hass, config): hass.http.register_path( 'HEAD', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'), _handle_get_static, False) + hass.http.register_path( + 'GET', re.compile(r'/local/(?P[a-zA-Z\._\-0-9/]+)'), + _handle_get_local, False) return True @@ -84,3 +87,16 @@ def _handle_get_static(handler, path_match, data): path = os.path.join(os.path.dirname(__file__), 'www_static', req_file) handler.write_file(path) + + +def _handle_get_local(handler, path_match, data): + """ + Returns a static file from the hass.config.path/www for the frontend. + """ + req_file = util.sanitize_path(path_match.group('file')) + + path = os.path.join(get_default_config_dir(), 'www', req_file) + if not os.path.isfile(path): + return False + + handler.write_file(path) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index abf0c498b1a..98deab3f447 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "c4722afa376379bc4457d54bb9a38cee" +VERSION = "90c41bfbaa56f9a1c88db27a54f7d36b" diff --git a/homeassistant/components/frontend/www_static/__init__.py b/homeassistant/components/frontend/www_static/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 73fdb905114..18bb5c557f9 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -4010,6 +4010,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN transition: border .3s ease-in-out; } .label-badge .value { + font-size: 90%; overflow: hidden; text-overflow: ellipsis; } @@ -4060,7 +4061,11 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN } .blue { - --ha-label-badge-color: #039be5; + --ha-label-badge-color: #039be5; + } + + .green { + --ha-label-badge-color: #0DA035; } .grey { @@ -4269,7 +4274,12 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN .current { color: var(--secondary-text-color); - } \ No newline at end of file +console.group&&(console.groupCollapsed("Dispatch: %s",t),console.group("payload"),console.debug(e),console.groupEnd())},e.dispatchError=function(t){console.group&&(console.debug("Dispatch error: "+t),console.groupEnd())},e.storeHandled=function(t,e,n){console.group&&e!==n&&console.debug("Store "+t+" handled action")},e.dispatchEnd=function(t){console.group&&(console.debug("Dispatch done, new state: ",t.toJS()),console.groupEnd())}},function(t,e,n){function r(t,e){this.__prevState=t,this.__evaluator=e,this.__prevValues=i.Map(),this.__observers=[]}var i=n(2),o=n(7),u=n(8);Object.defineProperty(r.prototype,"notifyObservers",{writable:!0,configurable:!0,value:function(t){if(this.__observers.length>0){var e=i.Map();this.__observers.forEach(function(n){var r,i=n.getter,a=o(i),s=this.__prevState;this.__prevValues.has(a)?r=this.__prevValues.get(a):(r=this.__evaluator.evaluate(s,i),this.__prevValues=this.__prevValues.set(a,r));var c=this.__evaluator.evaluate(t,i);u(r,c)||(n.handler.call(null,c),e=e.set(a,c))}.bind(this)),this.__prevValues=e}this.__prevState=t}}),Object.defineProperty(r.prototype,"onChange",{writable:!0,configurable:!0,value:function(t,e){var n={getter:t,handler:e};return this.__observers.push(n),function(){var t=this.__observers.indexOf(n);t>-1&&this.__observers.splice(t,1)}.bind(this)}}),Object.defineProperty(r.prototype,"reset",{writable:!0,configurable:!0,value:function(t){this.__prevState=t,this.__prevValues=i.Map(),this.__observers=[]}}),t.exports=r},function(t,e,n){var r=n(2);t.exports=function(t,e){if(t.hasOwnProperty("__hashCode"))return t.__hashCode;var n=r.fromJS(t).hashCode();return e||(Object.defineProperty(t,"__hashCode",{enumerable:!1,configurable:!1,writable:!1,value:n}),Object.freeze(t)),n}},function(t,e,n){var r=n(2);t.exports=function(t,e){return r.is(t,e)}},function(t,e,n){function r(t){return s(t)&&a(t[t.length-1])}function i(t){return t[t.length-1]}function o(t){return t.slice(0,t.length-1)}function u(t){if(!c(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,l]}var a=n(3).isFunction,s=n(3).isArray,c=n(10).isKeyPath,l=function(t){return t};t.exports={isGetter:r,getComputeFn:i,getDeps:o,fromKeyPath:u}},function(t,e,n){var r=n(3).isArray,i=n(3).isFunction;e.isKeyPath=function(t){return r(t)&&!i(t[t.length-1])}},function(t,e,n){function r(){this.__cachedGetters=i.Map({})}var i=n(2),o=n(1).toImmutable,u=n(7),a=n(8),s=n(9).getComputeFn,c=n(9).getDeps,l=n(10).isKeyPath,f=n(9).isGetter,d=!1;Object.defineProperty(r.prototype,"evaluate",{writable:!0,configurable:!0,value:function(t,e){if(l(e))return t.getIn(e);if(!f(e))throw new Error("evaluate must be passed a keyPath or Getter");var n=u(e);if(this.__isCached(t,e))return this.__cachedGetters.getIn([n,"value"]);var r=c(e).map(function(e){return this.evaluate(t,e)}.bind(this));if(this.__hasStaleValue(t,e)){var i=this.__cachedGetters.getIn([n,"args"]);if(a(i,o(r))){var p=this.__cachedGetters.getIn([n,"value"]);return this.__cacheValue(t,e,i,p),p}}if(d===!0)throw d=!1,new Error("Evaluate may not be called within a Getters computeFn");var h;d=!0;try{h=s(e).apply(null,r),d=!1}catch(v){throw d=!1,v}return this.__cacheValue(t,e,r,h),h}}),Object.defineProperty(r.prototype,"__hasStaleValue",{writable:!0,configurable:!0,value:function(t,e){var n=u(e),r=this.__cachedGetters;return r.has(n)&&r.getIn([n,"stateHashCode"])!==t.hashCode()}}),Object.defineProperty(r.prototype,"__cacheValue",{writable:!0,configurable:!0,value:function(t,e,n,r){var a=u(e);this.__cachedGetters=this.__cachedGetters.set(a,i.Map({value:r,args:o(n),stateHashCode:t.hashCode()}))}}),Object.defineProperty(r.prototype,"__isCached",{writable:!0,configurable:!0,value:function(t,e){var n=u(e);return this.__cachedGetters.hasIn([n,"value"])&&this.__cachedGetters.getIn([n,"stateHashCode"])===t.hashCode()}}),Object.defineProperty(r.prototype,"untrack",{writable:!0,configurable:!0,value:function(t){}}),Object.defineProperty(r.prototype,"reset",{writable:!0,configurable:!0,value:function(){this.__cachedGetters=i.Map({})}}),t.exports=r},function(t,e,n){function r(t,e){var n={};return i(e,function(e,r){n[r]=t.evaluate(e)}),n}var i=n(3).each;t.exports=function(t){return{getInitialState:function(){return r(t,this.getDataBindings())},componentDidMount:function(){var e=this;e.__unwatchFns=[],i(this.getDataBindings(),function(n,r){var i=t.observe(n,function(t){var n={};n[r]=t,e.setState(n)});e.__unwatchFns.push(i)})},componentWillUnmount:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}},function(t,e,n){function r(t){return this instanceof r?(this.__handlers=o({}),t&&u(this,t),void this.initialize()):new r(t)}function i(t){return t instanceof r}var o=n(2).Map,u=n(3).extend,a=n(1).toJS,s=n(1).toImmutable;Object.defineProperty(r.prototype,"initialize",{writable:!0,configurable:!0,value:function(){}}),Object.defineProperty(r.prototype,"getInitialState",{writable:!0,configurable:!0,value:function(){return o()}}),Object.defineProperty(r.prototype,"handle",{writable:!0,configurable:!0,value:function(t,e,n){var r=this.__handlers.get(e);return"function"==typeof r?r.call(this,t,n,e):t}}),Object.defineProperty(r.prototype,"handleReset",{writable:!0,configurable:!0,value:function(t){return this.getInitialState()}}),Object.defineProperty(r.prototype,"on",{writable:!0,configurable:!0,value:function(t,e){this.__handlers=this.__handlers.set(t,e)}}),Object.defineProperty(r.prototype,"serialize",{writable:!0,configurable:!0,value:function(t){return a(t)}}),Object.defineProperty(r.prototype,"deserialize",{writable:!0,configurable:!0,value:function(t){return s(t)}}),t.exports=r,t.exports.isStore=i}])})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(179),u=r(o);e["default"]=u["default"](i.reactor),t.exports=e["default"]},function(t,e){"use strict";var n=function(t){var e,n={};if(!(t instanceof Object)||Array.isArray(t))throw new Error("keyMirror(...): Argument must be an object.");for(e in t)t.hasOwnProperty(e)&&(n[e]=e);return n};t.exports=n},function(t,e){"use strict";function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(188),o=r(i);e.callApi=o["default"]},function(t,e,n){"use strict";function r(t){return i(t)?t:Object(t)}var i=n(6);t.exports=r},function(t,e,n){"use strict";var r=n(20),i=n(12),o=n(13),u="[object Array]",a=Object.prototype,s=a.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==u};t.exports=l},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(204),u=i(o),a=n(205),s=r(a),c=u["default"];e.actions=c;var l=s;e.getters=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){t.registerStores({restApiCache:l["default"]})}function o(t){return[["restApiCache",t.entity],function(t){return!!t}]}function u(t){return[["restApiCache",t.entity],function(t){return t||s.toImmutable({})}]}function a(t){return function(e){return["restApiCache",t.entity,e]}}Object.defineProperty(e,"__esModule",{value:!0}),e.register=i,e.createHasDataGetter=o,e.createEntityMapGetter=u,e.createByIdGetter=a;var s=n(3),c=n(228),l=r(c),f=n(227),d=r(f);e.createApiActions=d["default"]},function(t,e){"use strict";function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e){"use strict";function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(138),n(59),e["default"]=new o["default"]({is:"state-info",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"partial-base",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1}},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},toggleMenu:function(){this.fire("open-menu")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({ENTITY_HISTORY_DATE_SELECTED:null,ENTITY_HISTORY_FETCH_START:null,ENTITY_HISTORY_FETCH_ERROR:null,ENTITY_HISTORY_FETCH_SUCCESS:null,RECENT_ENTITY_HISTORY_FETCH_START:null,RECENT_ENTITY_HISTORY_FETCH_ERROR:null,RECENT_ENTITY_HISTORY_FETCH_SUCCESS:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return null!=t&&o(i(t))}var i=n(50),o=n(12);t.exports=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({LOGBOOK_DATE_SELECTED:null,LOGBOOK_ENTRIES_FETCH_START:null,LOGBOOK_ENTRIES_FETCH_ERROR:null,LOGBOOK_ENTRIES_FETCH_SUCCESS:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(229),u=i(o),a=n(79),s=r(a),c=u["default"];e.actions=c;var l=s;e.getters=l},function(t,e,n){"use strict";function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(126);t.exports=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({authAttempt:a["default"],authCurrent:c["default"],rememberAuth:f["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(191),a=i(u),s=n(192),c=i(s),l=n(193),f=i(l),d=n(189),p=r(d),h=n(190),v=r(h),_=p;e.actions=_;var y=v;e.getters=y},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var u=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(s){i=!0,o=s}finally{try{!r&&a["return"]&&a["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),a=function(){function t(t,e){for(var n=0;n-1&&t%1==0&&e>t}var r=/^\d+$/,i=9007199254740991;t.exports=n},function(t,e,n){"use strict";function r(t,e,n){if(!u(n))return!1;var r=typeof e;if("number"==r?i(n)&&o(e,n.length):"string"==r&&e in n){var a=n[e];return t===t?t===a:a!==a}return!1}var i=n(17),o=n(25),u=n(6);t.exports=r},function(t,e,n){"use strict";function r(t){return o(t)&&i(t)&&a.call(t,"callee")&&!s.call(t,"callee")}var i=n(17),o=n(13),u=Object.prototype,a=u.hasOwnProperty,s=u.propertyIsEnumerable;t.exports=r},function(t,e,n){"use strict";var r=n(20),i=n(17),o=n(6),u=n(123),a=r(Object,"keys"),s=a?function(t){var e=null==t?void 0:t.constructor;return"function"==typeof e&&e.prototype===t||"function"!=typeof t&&i(t)?u(t):o(t)?a(t):[]}:u;t.exports=s},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(62),a=r(u);e["default"]=new o["default"]({is:"domain-icon",properties:{domain:{type:String,value:""},state:{type:String,value:""}},computeIcon:function(t,e){return a["default"](t,e)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-label-badge",properties:{value:{type:String},icon:{type:String},label:{type:String},description:{type:String},image:{type:String,observe:"imageChanged"}},computeClasses:function(t){return t&&t.length>5?"value big":"value"}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"loading-box"}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(180),a=r(u);n(61),n(178),n(177),n(174),n(176),n(175),e["default"]=new o["default"]({is:"state-card-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"}},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=a["default"](t);if(e&&a["default"](e)===r)n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("state-card-"+r);i.stateObj=t,n.appendChild(i)}}}),t.exports=e["default"]},function(t,e){"use strict";function n(t,e){return t?e.map(function(e){return e in t.attributes?"has-"+e:""}).join(" "):""}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return i.reactor.evaluate(i.serviceGetters.canToggleEntity(t))}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=r;var i=n(2);t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e,n){function r(){y&&clearTimeout(y),p&&clearTimeout(p),g=0,p=y=m=void 0}function s(e,n){n&&clearTimeout(n),p=y=m=void 0,e&&(g=o(),h=t.apply(_,d),y||p||(d=_=void 0))}function c(){var t=e-(o()-v);0>=t||t>e?s(m,p):y=setTimeout(c,t)}function l(){s(O,y)}function f(){if(d=arguments,v=o(),_=this,m=O&&(y||!w),b===!1)var n=w&&!y;else{p||w||(g=v);var r=b-(v-g),i=0>=r||r>b;i?(p&&(p=clearTimeout(p)),g=v,h=t.apply(_,d)):p||(p=setTimeout(l,r))}return i&&y?y=clearTimeout(y):y||e===b||(y=setTimeout(c,e)),n&&(i=!0,h=t.apply(_,d)),!i||y||p||(d=_=void 0),h}var d,p,h,v,_,y,m,g=0,b=!1,O=!0;if("function"!=typeof t)throw new TypeError(u);if(e=0>e?0:+e||0,n===!0){var w=!0;O=!1}else i(n)&&(w=!!n.leading,b="maxWait"in n&&a(+n.maxWait||0,e),O="trailing"in n?!!n.trailing:O);return f.cancel=r,f}var i=n(68),o=n(184),u="Expected a function",a=Math.max;t.exports=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({SERVER_CONFIG_LOADED:null,COMPONENT_LOADED:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({serverComponent:a["default"],serverConfig:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(196),a=i(u),s=n(197),c=i(s),l=n(194),f=r(l),d=n(195),p=r(d),h=f;e.actions=h;var v=p;e.getters=v},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(206),u=i(o),a=n(207),s=r(a),c=u["default"];e.actions=c;var l=s;e.getters=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({NAVIGATE:null,SHOW_SIDEBAR:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({notifications:a["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(224),a=i(u),s=n(222),c=r(s),l=n(223),f=r(l),d=c;e.actions=d;var p=f;e.getters=p},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({streamStatus:a["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(236),a=i(u),s=n(232),c=r(s),l=n(233),f=r(l),d=c;e.actions=d;var p=f;e.getters=p},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({isFetchingData:a["default"],isSyncScheduled:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(238),a=i(u),s=n(239),c=i(s),l=n(237),f=r(l),d=n(82),p=r(d),h=f;e.actions=h;var v=p;e.getters=v},function(t,e){"use strict";function n(t){return t.getFullYear()+"-"+(t.getMonth()+1)+"-"+t.getDate()}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e){"use strict";function n(t){var e=t.split(" "),n=r(e,2),i=n[0],o=n[1],u=i.split(":"),a=r(u,3),s=a[0],c=a[1],l=a[2],f=o.split("-"),d=r(f,3),p=d[0],h=d[1],v=d[2];return new Date(Date.UTC(v,parseInt(h,10)-1,p,s,c,l))}Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(s){i=!0,o=s}finally{try{!r&&a["return"]&&a["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}();e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e,n){if(null!=t){void 0!==n&&n in i(t)&&(e=[n]);for(var r=0,o=e.length;null!=t&&o>r;)t=t[e[r++]];return r&&r==o?t:void 0}}var i=n(8);t.exports=r},function(t,e,n){"use strict";function r(t,e,n,a,s,c){return t===e?!0:null==t||null==e||!o(t)&&!u(e)?t!==t&&e!==e:i(t,e,r,n,a,s,c)}var i=n(102),o=n(6),u=n(13);t.exports=r},function(t,e,n){"use strict";function r(t,e){var n=-1,r=o(t)?Array(t.length):[];return i(t,function(t,i,o){r[++n]=e(t,i,o)}),r}var i=n(97),o=n(17);t.exports=r},function(t,e){"use strict";function n(t){return function(e){return null==e?void 0:e[t]}}t.exports=n},function(t,e,n){"use strict";var r=n(49),i=r("length");t.exports=i},function(t,e,n){"use strict";function r(t,e){var n=typeof t;if("string"==n&&a.test(t)||"number"==n)return!0;if(i(t))return!1;var r=!u.test(t);return r||null!=e&&t in o(e)}var i=n(9),o=n(8),u=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,a=/^\w*$/;t.exports=r},function(t,e,n){"use strict";function r(t){return t===t&&!i(t)}var i=n(6);t.exports=r},function(t,e,n){"use strict";function r(t){if(o(t))return t;var e=[];return i(t).replace(u,function(t,n,r,i){e.push(r?i.replace(a,"$1"):n||t)}),e}var i=n(109),o=n(9),u=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,a=/\\(\\)?/g;t.exports=r},function(t,e){"use strict";function n(t){return t}t.exports=n},function(t,e,n){"use strict";function r(t){return u(t)?i(t):o(t)}var i=n(49),o=n(106),u=n(51);t.exports=r},function(t,e,n){(function(t){"use strict";!function(e,n){t.exports=n()}(void 0,function(){function e(){return Ln.apply(null,arguments)}function n(t){Ln=t}function r(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,r=[];for(n=0;n0)for(n in Rn)r=Rn[n],i=e[r],"undefined"!=typeof i&&(t[r]=i);return t}function h(t){p(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),zn===!1&&(zn=!0,e.updateOffset(this),zn=!1)}function v(t){return t instanceof h||null!=t&&null!=t._isAMomentObject}function _(t){return 0>t?Math.ceil(t):Math.floor(t)}function y(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=_(e)),n}function m(t,e,n){var r,i=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),u=0;for(r=0;i>r;r++)(n&&t[r]!==e[r]||!n&&y(t[r])!==y(e[r]))&&u++;return u+o}function g(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function O(t){for(var e,n,r,i,o=0;o0;){if(r=w(i.slice(0,e).join("-")))return r;if(n&&n.length>=e&&m(i,n,!0)>=e-1)break;e--}o++}return null}function w(e){var n=null;if(!Hn[e]&&"undefined"!=typeof t&&t&&t.exports)try{n=Nn._abbr,!function(){var t=new Error('Cannot find module "./locale"');throw t.code="MODULE_NOT_FOUND",t}(),S(n)}catch(r){}return Hn[e]}function S(t,e){var n;return t&&(n="undefined"==typeof e?j(t):M(t,e),n&&(Nn=n)),Nn._abbr}function M(t,e){return null!==e?(e.abbr=t,Hn[t]=Hn[t]||new g,Hn[t].set(e),S(t),Hn[t]):(delete Hn[t],null)}function j(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Nn;if(!r(t)){if(e=w(t))return e;t=[t]}return O(t)}function T(t,e){var n=t.toLowerCase();Yn[n]=Yn[n+"s"]=Yn[e]=t}function E(t){return"string"==typeof t?Yn[t]||Yn[t.toLowerCase()]:void 0}function I(t){var e,n,r={};for(n in t)u(t,n)&&(e=E(n),e&&(r[e]=t[n]));return r}function P(t,n){return function(r){return null!=r?(C(this,t,r),e.updateOffset(this,n),this):D(this,t)}}function D(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function C(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function A(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=E(t),"function"==typeof this[t])return this[t](e);return this}function x(t,e,n){var r=""+Math.abs(t),i=e-r.length,o=t>=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+r}function k(t,e,n,r){var i=r;"string"==typeof r&&(i=function(){return this[r]()}),t&&(Bn[t]=i),e&&(Bn[e[0]]=function(){return x(i.apply(this,arguments),e[1],e[2])}),n&&(Bn[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),t)})}function L(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function N(t){var e,n,r=t.match(Gn);for(e=0,n=r.length;n>e;e++)Bn[r[e]]?r[e]=Bn[r[e]]:r[e]=L(r[e]);return function(i){var o="";for(e=0;n>e;e++)o+=r[e]instanceof Function?r[e].call(i,t):r[e];return o}}function R(t,e){return t.isValid()?(e=z(e,t.localeData()),Un[e]=Un[e]||N(e),Un[e](t)):t.localeData().invalidDate()}function z(t,e){function n(t){return e.longDateFormat(t)||t}var r=5;for(Fn.lastIndex=0;r>=0&&Fn.test(t);)t=t.replace(Fn,n),Fn.lastIndex=0,r-=1;return t}function H(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function Y(t,e,n){or[t]=H(e)?e:function(t){return t&&n?n:e}}function G(t,e){return u(or,t)?or[t](e._strict,e._locale):new RegExp(F(t))}function F(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,r,i){return e||n||r||i}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function U(t,e){var n,r=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(r=function(t,n){n[e]=y(t)}),n=0;nr;r++){if(i=s([2e3,r]),n&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[r]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[r]||(o="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[r]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[r].test(t))return r;if(n&&"MMM"===e&&this._shortMonthsParse[r].test(t))return r;if(!n&&this._monthsParse[r].test(t))return r}}function $(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),q(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function Z(t){return null!=t?($(this,t),e.updateOffset(this,!0),this):D(this,"Month")}function X(){return q(this.year(),this.month())}function Q(t){var e,n=t._a;return n&&-2===l(t).overflow&&(e=n[sr]<0||n[sr]>11?sr:n[cr]<1||n[cr]>q(n[ar],n[sr])?cr:n[lr]<0||n[lr]>24||24===n[lr]&&(0!==n[fr]||0!==n[dr]||0!==n[pr])?lr:n[fr]<0||n[fr]>59?fr:n[dr]<0||n[dr]>59?dr:n[pr]<0||n[pr]>999?pr:-1,l(t)._overflowDayOfYear&&(ar>e||e>cr)&&(e=cr),l(t).overflow=e),t}function tt(t){e.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function et(t,e){var n=!0;return a(function(){return n&&(tt(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function nt(t,e){_r[t]||(tt(e),_r[t]=!0)}function rt(t){var e,n,r=t._i,i=yr.exec(r);if(i){for(l(t).iso=!0,e=0,n=mr.length;n>e;e++)if(mr[e][1].exec(r)){t._f=mr[e][0];break}for(e=0,n=gr.length;n>e;e++)if(gr[e][1].exec(r)){t._f+=(i[6]||" ")+gr[e][0];break}r.match(nr)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(t){var n=br.exec(t._i);return null!==n?void(t._d=new Date(+n[1])):(rt(t),void(t._isValid===!1&&(delete t._isValid,e.createFromInputFallback(t))))}function ot(t,e,n,r,i,o,u){var a=new Date(t,e,n,r,i,o,u);return 1970>t&&a.setFullYear(t),a}function ut(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function at(t){return st(t)?366:365}function st(t){return t%4===0&&t%100!==0||t%400===0}function ct(){return st(this.year())}function lt(t,e,n){var r,i=n-e,o=n-t.day();return o>i&&(o-=7),i-7>o&&(o+=7),r=Dt(t).add(o,"d"),{week:Math.ceil(r.dayOfYear()/7),year:r.year()}}function ft(t){return lt(t,this._week.dow,this._week.doy).week}function dt(){return this._week.dow}function pt(){return this._week.doy}function ht(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function vt(t){var e=lt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function _t(t,e,n,r,i){var o,u=6+i-r,a=ut(t,0,1+u),s=a.getUTCDay();return i>s&&(s+=7),n=null!=n?1*n:i,o=1+u+7*(e-1)-s+n,{year:o>0?t:t-1,dayOfYear:o>0?o:at(t-1)+o}}function yt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function mt(t,e,n){return null!=t?t:null!=e?e:n}function gt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,r,i,o=[];if(!t._d){for(r=gt(t),t._w&&null==t._a[cr]&&null==t._a[sr]&&Ot(t),t._dayOfYear&&(i=mt(t._a[ar],r[ar]),t._dayOfYear>at(i)&&(l(t)._overflowDayOfYear=!0), +n=ut(i,0,t._dayOfYear),t._a[sr]=n.getUTCMonth(),t._a[cr]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=r[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[lr]&&0===t._a[fr]&&0===t._a[dr]&&0===t._a[pr]&&(t._nextDay=!0,t._a[lr]=0),t._d=(t._useUTC?ut:ot).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[lr]=24)}}function Ot(t){var e,n,r,i,o,u,a;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,u=4,n=mt(e.GG,t._a[ar],lt(Dt(),1,4).year),r=mt(e.W,1),i=mt(e.E,1)):(o=t._locale._week.dow,u=t._locale._week.doy,n=mt(e.gg,t._a[ar],lt(Dt(),o,u).year),r=mt(e.w,1),null!=e.d?(i=e.d,o>i&&++r):i=null!=e.e?e.e+o:o),a=_t(n,r,i,u,o),t._a[ar]=a.year,t._dayOfYear=a.dayOfYear}function wt(t){if(t._f===e.ISO_8601)return void rt(t);t._a=[],l(t).empty=!0;var n,r,i,o,u,a=""+t._i,s=a.length,c=0;for(i=z(t._f,t._locale).match(Gn)||[],n=0;n0&&l(t).unusedInput.push(u),a=a.slice(a.indexOf(r)+r.length),c+=r.length),Bn[o]?(r?l(t).empty=!1:l(t).unusedTokens.push(o),V(o,r,t)):t._strict&&!r&&l(t).unusedTokens.push(o);l(t).charsLeftOver=s-c,a.length>0&&l(t).unusedInput.push(a),l(t).bigHour===!0&&t._a[lr]<=12&&t._a[lr]>0&&(l(t).bigHour=void 0),t._a[lr]=St(t._locale,t._a[lr],t._meridiem),bt(t),Q(t)}function St(t,e,n){var r;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(r=t.isPM(n),r&&12>e&&(e+=12),r||12!==e||(e=0),e):e}function Mt(t){var e,n,r,i,o;if(0===t._f.length)return l(t).invalidFormat=!0,void(t._d=new Date(NaN));for(i=0;io)&&(r=o,n=e));a(t,n||e)}function jt(t){if(!t._d){var e=I(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function Tt(t){var e=new h(Q(Et(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Et(t){var e=t._i,n=t._f;return t._locale=t._locale||j(t._l),null===e||void 0===n&&""===e?d({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),v(e)?new h(Q(e)):(r(n)?Mt(t):n?wt(t):i(e)?t._d=e:It(t),t))}function It(t){var n=t._i;void 0===n?t._d=new Date:i(n)?t._d=new Date(+n):"string"==typeof n?it(t):r(n)?(t._a=o(n.slice(0),function(t){return parseInt(t,10)}),bt(t)):"object"==typeof n?jt(t):"number"==typeof n?t._d=new Date(n):e.createFromInputFallback(t)}function Pt(t,e,n,r,i){var o={};return"boolean"==typeof n&&(r=n,n=void 0),o._isAMomentObject=!0,o._useUTC=o._isUTC=i,o._l=n,o._i=t,o._f=e,o._strict=r,Tt(o)}function Dt(t,e,n,r){return Pt(t,e,n,r,!1)}function Ct(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Dt();for(n=e[0],i=1;it&&(t=-t,n="-"),n+x(~~(t/60),2)+e+x(~~t%60,2)})}function Rt(t){var e=(t||"").match(nr)||[],n=e[e.length-1]||[],r=(n+"").match(jr)||["-",0,0],i=+(60*r[1])+y(r[2]);return"+"===r[0]?i:-i}function zt(t,n){var r,o;return n._isUTC?(r=n.clone(),o=(v(t)||i(t)?+t:+Dt(t))-+r,r._d.setTime(+r._d+o),e.updateOffset(r,!1),r):Dt(t).local()}function Ht(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Yt(t,n){var r,i=this._offset||0;return null!=t?("string"==typeof t&&(t=Rt(t)),Math.abs(t)<16&&(t=60*t),!this._isUTC&&n&&(r=Ht(this)),this._offset=t,this._isUTC=!0,null!=r&&this.add(r,"m"),i!==t&&(!n||this._changeInProgress?ne(this,Zt(t-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?i:Ht(this)}function Gt(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Ft(t){return this.utcOffset(0,t)}function Ut(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ht(this),"m")),this}function Bt(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Rt(this._i)),this}function Vt(t){return t=t?Dt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function qt(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Wt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(p(t,this),t=Et(t),t._a){var e=t._isUTC?s(t._a):Dt(t._a);this._isDSTShifted=this.isValid()&&m(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Kt(){return!this._isUTC}function Jt(){return this._isUTC}function $t(){return this._isUTC&&0===this._offset}function Zt(t,e){var n,r,i,o=t,a=null;return Lt(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(a=Tr.exec(t))?(n="-"===a[1]?-1:1,o={y:0,d:y(a[cr])*n,h:y(a[lr])*n,m:y(a[fr])*n,s:y(a[dr])*n,ms:y(a[pr])*n}):(a=Er.exec(t))?(n="-"===a[1]?-1:1,o={y:Xt(a[2],n),M:Xt(a[3],n),d:Xt(a[4],n),h:Xt(a[5],n),m:Xt(a[6],n),s:Xt(a[7],n),w:Xt(a[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(i=te(Dt(o.from),Dt(o.to)),o={},o.ms=i.milliseconds,o.M=i.months),r=new kt(o),Lt(t)&&u(t,"_locale")&&(r._locale=t._locale),r}function Xt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Qt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function te(t,e){var n;return e=zt(e,t),t.isBefore(e)?n=Qt(t,e):(n=Qt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function ee(t,e){return function(n,r){var i,o;return null===r||isNaN(+r)||(nt(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),o=n,n=r,r=o),n="string"==typeof n?+n:n,i=Zt(n,r),ne(this,i,t),this}}function ne(t,n,r,i){var o=n._milliseconds,u=n._days,a=n._months;i=null==i?!0:i,o&&t._d.setTime(+t._d+o*r),u&&C(t,"Date",D(t,"Date")+u*r),a&&$(t,D(t,"Month")+a*r),i&&e.updateOffset(t,u||a)}function re(t,e){var n=t||Dt(),r=zt(n,this).startOf("day"),i=this.diff(r,"days",!0),o=-6>i?"sameElse":-1>i?"lastWeek":0>i?"lastDay":1>i?"sameDay":2>i?"nextDay":7>i?"nextWeek":"sameElse";return this.format(e&&e[o]||this.localeData().calendar(o,this,Dt(n)))}function ie(){return new h(this)}function oe(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=v(t)?t:Dt(t),+this>+t):(n=v(t)?+t:+Dt(t),n<+this.clone().startOf(e))}function ue(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=v(t)?t:Dt(t),+t>+this):(n=v(t)?+t:+Dt(t),+this.clone().endOf(e)e-o?(n=t.clone().add(i-1,"months"),r=(e-o)/(o-n)):(n=t.clone().add(i+1,"months"),r=(e-o)/(n-o)),-(i+r)}function fe(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function de(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=Dt([2e3,1]).day(e),r="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(r.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Fe(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=Re(t,this.localeData()),this.add(t-e,"d")):e}function Ue(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Be(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function Ve(t,e){k(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qe(t,e){return e._meridiemParse}function We(t){return"p"===(t+"").toLowerCase().charAt(0)}function Ke(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Je(t,e){e[pr]=y(1e3*("0."+t))}function $e(){return this._isUTC?"UTC":""}function Ze(){return this._isUTC?"Coordinated Universal Time":""}function Xe(t){return Dt(1e3*t)}function Qe(){return Dt.apply(null,arguments).parseZone()}function tn(t,e,n){var r=this._calendar[t];return"function"==typeof r?r.call(e,n):r}function en(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function nn(){return this._invalidDate}function rn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function un(t,e,n,r){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,r):i.replace(/%d/i,t)}function an(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function sn(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function cn(t,e,n,r){var i=j(),o=s().set(r,e);return i[n](o,t)}function ln(t,e,n,r,i){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return cn(t,e,n,i);var o,u=[];for(o=0;r>o;o++)u[o]=cn(t,o,n,i);return u}function fn(t,e){return ln(t,e,"months",12,"month")}function dn(t,e){return ln(t,e,"monthsShort",12,"month")}function pn(t,e){return ln(t,e,"weekdays",7,"day")}function hn(t,e){return ln(t,e,"weekdaysShort",7,"day")}function vn(t,e){return ln(t,e,"weekdaysMin",7,"day")}function _n(){var t=this._data;return this._milliseconds=$r(this._milliseconds),this._days=$r(this._days),this._months=$r(this._months),t.milliseconds=$r(t.milliseconds),t.seconds=$r(t.seconds),t.minutes=$r(t.minutes),t.hours=$r(t.hours),t.months=$r(t.months),t.years=$r(t.years),this}function yn(t,e,n,r){var i=Zt(e,n);return t._milliseconds+=r*i._milliseconds,t._days+=r*i._days,t._months+=r*i._months,t._bubble()}function mn(t,e){return yn(this,t,e,1)}function gn(t,e){return yn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function On(){var t,e,n,r,i,o=this._milliseconds,u=this._days,a=this._months,s=this._data;return o>=0&&u>=0&&a>=0||0>=o&&0>=u&&0>=a||(o+=864e5*bn(Sn(a)+u),u=0,a=0),s.milliseconds=o%1e3,t=_(o/1e3),s.seconds=t%60,e=_(t/60),s.minutes=e%60,n=_(e/60),s.hours=n%24,u+=_(n/24),i=_(wn(u)),a+=i,u-=bn(Sn(i)),r=_(a/12),a%=12,s.days=u,s.months=a,s.years=r,this}function wn(t){return 4800*t/146097}function Sn(t){return 146097*t/4800}function Mn(t){var e,n,r=this._milliseconds;if(t=E(t),"month"===t||"year"===t)return e=this._days+r/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(Sn(this._months)),t){case"week":return e/7+r/6048e5;case"day":return e+r/864e5;case"hour":return 24*e+r/36e5;case"minute":return 1440*e+r/6e4;case"second":return 86400*e+r/1e3;case"millisecond":return Math.floor(864e5*e)+r;default:throw new Error("Unknown unit "+t)}}function jn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*y(this._months/12)}function Tn(t){return function(){return this.as(t)}}function En(t){return t=E(t),this[t+"s"]()}function In(t){return function(){return this._data[t]}}function Pn(){return _(this.days()/7)}function Dn(t,e,n,r,i){return i.relativeTime(e||1,!!n,t,r)}function Cn(t,e,n){var r=Zt(t).abs(),i=di(r.as("s")),o=di(r.as("m")),u=di(r.as("h")),a=di(r.as("d")),s=di(r.as("M")),c=di(r.as("y")),l=i0,l[4]=n,Dn.apply(null,l)}function An(t,e){return void 0===pi[t]?!1:void 0===e?pi[t]:(pi[t]=e,!0)}function xn(t){var e=this.localeData(),n=Cn(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function kn(){var t,e,n,r=hi(this._milliseconds)/1e3,i=hi(this._days),o=hi(this._months);t=_(r/60),e=_(t/60),r%=60,t%=60,n=_(o/12),o%=12;var u=n,a=o,s=i,c=e,l=t,f=r,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(u?u+"Y":"")+(a?a+"M":"")+(s?s+"D":"")+(c||l||f?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(f?f+"S":""):"P0D"}var Ln,Nn,Rn=e.momentProperties=[],zn=!1,Hn={},Yn={},Gn=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Fn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Un={},Bn={},Vn=/\d/,qn=/\d\d/,Wn=/\d{3}/,Kn=/\d{4}/,Jn=/[+-]?\d{6}/,$n=/\d\d?/,Zn=/\d{1,3}/,Xn=/\d{1,4}/,Qn=/[+-]?\d{1,6}/,tr=/\d+/,er=/[+-]?\d+/,nr=/Z|[+-]\d\d:?\d\d/gi,rr=/[+-]?\d+(\.\d{1,3})?/,ir=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,or={},ur={},ar=0,sr=1,cr=2,lr=3,fr=4,dr=5,pr=6;k("M",["MM",2],"Mo",function(){return this.month()+1}),k("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),k("MMMM",0,0,function(t){return this.localeData().months(this,t)}),T("month","M"),Y("M",$n),Y("MM",$n,qn),Y("MMM",ir),Y("MMMM",ir),U(["M","MM"],function(t,e){e[sr]=y(t)-1}),U(["MMM","MMMM"],function(t,e,n,r){var i=n._locale.monthsParse(t,r,n._strict);null!=i?e[sr]=i:l(n).invalidMonth=t});var hr="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),vr="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),_r={};e.suppressDeprecationWarnings=!1;var yr=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mr=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],gr=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],br=/^\/?Date\((\-?\d+)/i;e.createFromInputFallback=et("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),k(0,["YY",2],0,function(){return this.year()%100}),k(0,["YYYY",4],0,"year"),k(0,["YYYYY",5],0,"year"),k(0,["YYYYYY",6,!0],0,"year"),T("year","y"),Y("Y",er),Y("YY",$n,qn),Y("YYYY",Xn,Kn),Y("YYYYY",Qn,Jn),Y("YYYYYY",Qn,Jn),U(["YYYYY","YYYYYY"],ar),U("YYYY",function(t,n){n[ar]=2===t.length?e.parseTwoDigitYear(t):y(t)}),U("YY",function(t,n){n[ar]=e.parseTwoDigitYear(t)}),e.parseTwoDigitYear=function(t){return y(t)+(y(t)>68?1900:2e3)};var Or=P("FullYear",!1);k("w",["ww",2],"wo","week"),k("W",["WW",2],"Wo","isoWeek"),T("week","w"),T("isoWeek","W"),Y("w",$n),Y("ww",$n,qn),Y("W",$n),Y("WW",$n,qn),B(["w","ww","W","WW"],function(t,e,n,r){e[r.substr(0,1)]=y(t)});var wr={dow:0,doy:6};k("DDD",["DDDD",3],"DDDo","dayOfYear"),T("dayOfYear","DDD"),Y("DDD",Zn),Y("DDDD",Wn),U(["DDD","DDDD"],function(t,e,n){n._dayOfYear=y(t)}),e.ISO_8601=function(){};var Sr=et("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=Dt.apply(null,arguments);return this>t?this:t}),Mr=et("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=Dt.apply(null,arguments);return t>this?this:t});Nt("Z",":"),Nt("ZZ",""),Y("Z",nr),Y("ZZ",nr),U(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Rt(t)});var jr=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Tr=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Er=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Zt.fn=kt.prototype;var Ir=ee(1,"add"),Pr=ee(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var Dr=et("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});k(0,["gg",2],0,function(){return this.weekYear()%100}),k(0,["GG",2],0,function(){return this.isoWeekYear()%100}),De("gggg","weekYear"),De("ggggg","weekYear"),De("GGGG","isoWeekYear"),De("GGGGG","isoWeekYear"),T("weekYear","gg"),T("isoWeekYear","GG"),Y("G",er),Y("g",er),Y("GG",$n,qn),Y("gg",$n,qn),Y("GGGG",Xn,Kn),Y("gggg",Xn,Kn),Y("GGGGG",Qn,Jn),Y("ggggg",Qn,Jn),B(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,r){e[r.substr(0,2)]=y(t)}),B(["gg","GG"],function(t,n,r,i){n[i]=e.parseTwoDigitYear(t)}),k("Q",0,0,"quarter"),T("quarter","Q"),Y("Q",Vn),U("Q",function(t,e){e[sr]=3*(y(t)-1)}),k("D",["DD",2],"Do","date"),T("date","D"),Y("D",$n),Y("DD",$n,qn),Y("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),U(["D","DD"],cr),U("Do",function(t,e){e[cr]=y(t.match($n)[0],10)});var Cr=P("Date",!0);k("d",0,"do","day"),k("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),k("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),k("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),k("e",0,0,"weekday"),k("E",0,0,"isoWeekday"),T("day","d"),T("weekday","e"),T("isoWeekday","E"),Y("d",$n),Y("e",$n),Y("E",$n),Y("dd",ir),Y("ddd",ir),Y("dddd",ir),B(["dd","ddd","dddd"],function(t,e,n){var r=n._locale.weekdaysParse(t);null!=r?e.d=r:l(n).invalidWeekday=t}),B(["d","e","E"],function(t,e,n,r){e[r]=y(t)});var Ar="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),xr="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),kr="Su_Mo_Tu_We_Th_Fr_Sa".split("_");k("H",["HH",2],0,"hour"),k("h",["hh",2],0,function(){return this.hours()%12||12}),Ve("a",!0),Ve("A",!1),T("hour","h"),Y("a",qe),Y("A",qe),Y("H",$n),Y("h",$n),Y("HH",$n,qn),Y("hh",$n,qn),U(["H","HH"],lr),U(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),U(["h","hh"],function(t,e,n){e[lr]=y(t),l(n).bigHour=!0});var Lr=/[ap]\.?m?\.?/i,Nr=P("Hours",!0);k("m",["mm",2],0,"minute"),T("minute","m"),Y("m",$n),Y("mm",$n,qn),U(["m","mm"],fr);var Rr=P("Minutes",!1);k("s",["ss",2],0,"second"),T("second","s"),Y("s",$n),Y("ss",$n,qn),U(["s","ss"],dr);var zr=P("Seconds",!1);k("S",0,0,function(){return~~(this.millisecond()/100)}),k(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),k(0,["SSS",3],0,"millisecond"),k(0,["SSSS",4],0,function(){return 10*this.millisecond()}),k(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),k(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),k(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),k(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),k(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),T("millisecond","ms"),Y("S",Zn,Vn),Y("SS",Zn,qn),Y("SSS",Zn,Wn);var Hr;for(Hr="SSSS";Hr.length<=9;Hr+="S")Y(Hr,tr);for(Hr="S";Hr.length<=9;Hr+="S")U(Hr,Je);var Yr=P("Milliseconds",!1);k("z",0,0,"zoneAbbr"),k("zz",0,0,"zoneName");var Gr=h.prototype;Gr.add=Ir,Gr.calendar=re,Gr.clone=ie,Gr.diff=ce,Gr.endOf=Oe,Gr.format=pe,Gr.from=he,Gr.fromNow=ve,Gr.to=_e,Gr.toNow=ye,Gr.get=A,Gr.invalidAt=Pe,Gr.isAfter=oe,Gr.isBefore=ue,Gr.isBetween=ae,Gr.isSame=se,Gr.isValid=Ee,Gr.lang=Dr,Gr.locale=me,Gr.localeData=ge,Gr.max=Mr,Gr.min=Sr,Gr.parsingFlags=Ie,Gr.set=A,Gr.startOf=be,Gr.subtract=Pr,Gr.toArray=je,Gr.toObject=Te,Gr.toDate=Me,Gr.toISOString=de,Gr.toJSON=de,Gr.toString=fe,Gr.unix=Se,Gr.valueOf=we,Gr.year=Or,Gr.isLeapYear=ct,Gr.weekYear=Ae,Gr.isoWeekYear=xe,Gr.quarter=Gr.quarters=Ne,Gr.month=Z,Gr.daysInMonth=X,Gr.week=Gr.weeks=ht,Gr.isoWeek=Gr.isoWeeks=vt,Gr.weeksInYear=Le,Gr.isoWeeksInYear=ke,Gr.date=Cr,Gr.day=Gr.days=Fe,Gr.weekday=Ue,Gr.isoWeekday=Be,Gr.dayOfYear=yt,Gr.hour=Gr.hours=Nr,Gr.minute=Gr.minutes=Rr,Gr.second=Gr.seconds=zr,Gr.millisecond=Gr.milliseconds=Yr,Gr.utcOffset=Yt,Gr.utc=Ft,Gr.local=Ut,Gr.parseZone=Bt,Gr.hasAlignedHourOffset=Vt,Gr.isDST=qt,Gr.isDSTShifted=Wt,Gr.isLocal=Kt,Gr.isUtcOffset=Jt,Gr.isUtc=$t,Gr.isUTC=$t,Gr.zoneAbbr=$e,Gr.zoneName=Ze,Gr.dates=et("dates accessor is deprecated. Use date instead.",Cr),Gr.months=et("months accessor is deprecated. Use month instead",Z),Gr.years=et("years accessor is deprecated. Use year instead",Or),Gr.zone=et("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Gt);var Fr=Gr,Ur={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Br={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Vr="Invalid date",qr="%d",Wr=/\d{1,2}/,Kr={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Jr=g.prototype;Jr._calendar=Ur,Jr.calendar=tn,Jr._longDateFormat=Br,Jr.longDateFormat=en,Jr._invalidDate=Vr,Jr.invalidDate=nn,Jr._ordinal=qr,Jr.ordinal=rn,Jr._ordinalParse=Wr,Jr.preparse=on,Jr.postformat=on,Jr._relativeTime=Kr,Jr.relativeTime=un,Jr.pastFuture=an,Jr.set=sn,Jr.months=W,Jr._months=hr,Jr.monthsShort=K,Jr._monthsShort=vr,Jr.monthsParse=J,Jr.week=ft,Jr._week=wr,Jr.firstDayOfYear=pt,Jr.firstDayOfWeek=dt,Jr.weekdays=ze,Jr._weekdays=Ar,Jr.weekdaysMin=Ye,Jr._weekdaysMin=kr,Jr.weekdaysShort=He,Jr._weekdaysShort=xr,Jr.weekdaysParse=Ge,Jr.isPM=We,Jr._meridiemParse=Lr,Jr.meridiem=Ke,S("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===y(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),e.lang=et("moment.lang is deprecated. Use moment.locale instead.",S),e.langData=et("moment.langData is deprecated. Use moment.localeData instead.",j);var $r=Math.abs,Zr=Tn("ms"),Xr=Tn("s"),Qr=Tn("m"),ti=Tn("h"),ei=Tn("d"),ni=Tn("w"),ri=Tn("M"),ii=Tn("y"),oi=In("milliseconds"),ui=In("seconds"),ai=In("minutes"),si=In("hours"),ci=In("days"),li=In("months"),fi=In("years"),di=Math.round,pi={s:45,m:45,h:22,d:26,M:11},hi=Math.abs,vi=kt.prototype;vi.abs=_n,vi.add=mn,vi.subtract=gn,vi.as=Mn,vi.asMilliseconds=Zr,vi.asSeconds=Xr,vi.asMinutes=Qr,vi.asHours=ti,vi.asDays=ei,vi.asWeeks=ni,vi.asMonths=ri,vi.asYears=ii,vi.valueOf=jn,vi._bubble=On,vi.get=En,vi.milliseconds=oi,vi.seconds=ui,vi.minutes=ai,vi.hours=si,vi.days=ci,vi.weeks=Pn,vi.months=li,vi.years=fi,vi.humanize=xn,vi.toISOString=kn,vi.toString=kn,vi.toJSON=kn,vi.locale=me,vi.localeData=ge,vi.toIsoString=et("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",kn),vi.lang=Dr,k("X",0,0,"unix"),k("x",0,0,"valueOf"),Y("x",er),Y("X",rr),U("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),U("x",function(t,e,n){n._d=new Date(y(t))}),e.version="2.10.6",n(Dt),e.fn=Fr,e.min=At,e.max=xt,e.utc=s,e.unix=Xe,e.months=fn,e.isDate=i,e.locale=S,e.invalid=d,e.duration=Zt,e.isMoment=v,e.weekdays=pn,e.parseZone=Qe,e.localeData=j,e.isDuration=Lt,e.monthsShort=dn,e.weekdaysMin=vn,e.defineLocale=M,e.weekdaysShort=hn,e.normalizeUnits=E,e.relativeTimeThreshold=An;var _i=e;return _i})}).call(e,n(130)(t))},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);e["default"]=new u["default"]({is:"ha-entity-toggle",properties:{stateObj:{type:Object,observer:"stateObjChanged"},toggleChecked:{type:Boolean,value:!1}},ready:function(){this.forceStateChange()},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.state?this.turn_on():e||"off"===this.stateObj.state||this.turn_off()},stateObjChanged:function(t){t&&this.updateToggle(t)},updateToggle:function(t){this.toggleChecked=t&&"off"!==t.state},forceStateChange:function(){this.updateToggle(this.stateObj)},turn_on:function(){var t=this;i.serviceActions.callTurnOn(this.stateObj.entityId).then(function(){return t.forceStateChange()})},turn_off:function(){var t=this;i.serviceActions.callTurnOff(this.stateObj.entityId).then(function(){return t.forceStateChange()})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-card",properties:{title:{type:String},header:{type:String}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(56),o=r(i),u=n(2),a=n(1),s=r(a),c=6e4,l=u.util.parseDateTime;e["default"]=new s["default"]({is:"relative-ha-datetime",properties:{datetime:{type:String,observer:"datetimeChanged"},datetimeObj:{type:Object,observer:"datetimeObjChanged"},parsedDateTime:{type:Object},relativeTime:{type:String,value:"not set"}},created:function(){this.updateRelative=this.updateRelative.bind(this)},attached:function(){this._interval=setInterval(this.updateRelative,c)},detached:function(){clearInterval(this._interval)},datetimeChanged:function(t){this.parsedDateTime=t?l(t):null,this.updateRelative()},datetimeObjChanged:function(t){this.parsedDateTime=t,this.updateRelative()},updateRelative:function(){this.relativeTime=this.parsedDateTime?o["default"](this.parsedDateTime).fromNow():""}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(31),n(149),n(148),e["default"]=new o["default"]({is:"state-history-charts",properties:{stateHistory:{type:Object},isLoadingData:{type:Boolean,value:!1},apiLoaded:{type:Boolean,value:!1},isLoading:{type:Boolean,computed:"computeIsLoading(isLoadingData, apiLoaded)"},groupedStateHistory:{type:Object,computed:"computeGroupedStateHistory(isLoading, stateHistory)"},isSingleDevice:{type:Boolean,computed:"computeIsSingleDevice(stateHistory)"}},computeIsSingleDevice:function(t){return t&&1===t.size},computeGroupedStateHistory:function(t,e){if(t||!e)return{line:[],timeline:[]};var n={},r=[];e.forEach(function(t){if(t&&0!==t.size){var e=t.find(function(t){return"unit_of_measurement"in t.attributes}),i=e?e.attributes.unit_of_measurement:!1;i?i in n?n[i].push(t.toArray()):n[i]=[t.toArray()]:r.push(t.toArray())}}),r=r.length>0&&r;var i=Object.keys(n).map(function(t){return[t,n[t]]});return{line:i,timeline:r}},googleApiLoaded:function(){var t=this;google.load("visualization","1",{packages:["timeline","corechart"],callback:function(){return t.apiLoaded=!0}})},computeContentClasses:function(t){return t?"loading":""},computeIsLoading:function(t,e){return t||!e},computeIsEmpty:function(t){return t&&0===t.size},extractUnit:function(t){return t[0]},extractData:function(t){return t[1]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(14),e["default"]=new o["default"]({is:"state-card-display",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e){"use strict";function n(t,e){switch(t){case"homeassistant":return"home";case"group":return"homeassistant-24:group";case"device_tracker":return"social:person";case"switch":return"image:flash-on";case"alarm_control_panel":return e&&"disarmed"===e?"icons:lock-open":"icons:lock";case"media_player":var n="hardware:cast";return e&&"off"!==e&&"idle"!==e&&(n+="-connected"),n;case"sun":return"image:wb-sunny";case"light":return"image:wb-incandescent";case"simple_alarm":return"social:notifications";case"notify":return"announcement";case"thermostat":return"homeassistant-100:thermostat";case"sensor":return"visibility";case"configurator":return"settings";case"conversation":return"av:hearing";case"script":return"description";case"scene":return"social:pages";case"updater":return"update_available"===e?"icons:cloud-download":"icons:cloud-done";default:return"bookmark"}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return u["default"](t).format("LT")}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(56),u=r(o);t.exports=e["default"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e["default"]=function(t,e){r.authActions.validate(t,{rememberAuth:e,useStreaming:r.localStoragePreferences.useStreaming})},t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(187);t.exports=r},function(t,e){"use strict";function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){"use strict";function r(t){return i(t)&&a.call(t)==o}var i=n(68),o="[object Function]",u=Object.prototype,a=u.toString; +t.exports=r},function(t,e){"use strict";function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=["isLoadingEntityHistory"];e.isLoadingEntityHistory=i;var o=["currentEntityHistoryDate"];e.currentDate=o;var u=["entityHistory"];e.entityHistoryMap=u;var a=[o,u,function(t,e){return e.get(t)||r.toImmutable({})}];e.entityHistoryForCurrentDate=a;var s=[o,u,function(t,e){return!!e.get(t)}];e.hasDataForCurrentDate=s;var c=["recentEntityHistory"];e.recentEntityHistoryMap=c;var l=["recentEntityHistory"];e.recentEntityHistoryUpdatedMap=l},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({currentEntityHistoryDate:a["default"],entityHistory:c["default"],isLoadingEntityHistory:f["default"],recentEntityHistory:p["default"],recentEntityHistoryUpdated:v["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(199),a=i(u),s=n(200),c=i(s),l=n(201),f=i(l),d=n(202),p=i(d),h=n(203),v=i(h),_=n(198),y=r(_),m=n(69),g=r(m),b=y;e.actions=b;var O=g;e.getters=O},function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(t,e){for(var n=0;n6e4}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var u=n(225),a=n(245),s=i(a),c=n(247),l=i(c),f=n(249),d=i(f),p=n(22),h=r(p),v=n(37),_=r(v),y=n(10),m=r(y),g=n(70),b=r(g),O=n(38),w=r(O),S=n(210),M=r(S),j=n(73),T=r(j),E=n(76),I=r(E),P=n(40),D=r(P),C=n(19),A=r(C),x=n(41),k=r(x),L=n(43),N=r(L),R=n(242),z=r(R),H=n(11),Y=r(H),G=function F(){o(this,F);var t=s["default"]();Object.defineProperties(this,{demo:{value:!1,enumerable:!0},localStoragePreferences:{value:u.localStoragePreferences,enumerable:!0},reactor:{value:t,enumerable:!0},util:{value:d["default"],enumerable:!0},startLocalStoragePreferencesSync:{value:u.localStoragePreferences.startSync.bind(u.localStoragePreferences,t)},startUrlSync:{value:I.urlSync.startSync.bind(null,t)},stopUrlSync:{value:I.urlSync.stopSync.bind(null,t)}}),l["default"](this,t,{auth:h,config:_,entity:m,entityHistory:b,event:w,logbook:M,moreInfo:T,navigation:I,notification:D,service:A,stream:k,sync:N,voice:z,restApi:Y})};e["default"]=G,t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e,n){var r=t?t.length:0;return n&&o(t,e,n)&&(e=!1),r?i(t,e):[]}var i=n(98),o=n(26);t.exports=r},function(t,e){"use strict";function n(t){var e=t?t.length:0;return e?t[e-1]:void 0}t.exports=n},function(t,e,n){"use strict";function r(t,e,n,r){var s=t?t.length:0;return s?(null!=e&&"boolean"!=typeof e&&(r=n,n=u(t,e,r)?void 0:e,e=!1),n=null==n?n:i(n,r,3),e?a(t,n):o(t,n)):[]}var i=n(24),o=n(110),u=n(26),a=n(124);t.exports=r},function(t,e,n){"use strict";function r(t,e,n){var r=a(t)?i:u;return e=o(e,n,3),r(t,e)}var i=n(93),o=n(24),u=n(48),a=n(9);t.exports=r},function(t,e,n){"use strict";function r(t,e){return i(t,o(e))}var i=n(89),o=n(55);t.exports=r},function(t,e,n){"use strict";function r(t,e,n){if(null==t)return[];n&&s(t,e,n)&&(e=void 0);var r=-1;e=i(e,n,3);var c=o(t,function(t,n,i){return{criteria:e(t,n,i),index:++r,value:t}});return u(c,a)}var i=n(24),o=n(48),u=n(108),a=n(114),s=n(26);t.exports=r},function(t,e,n){(function(e){"use strict";function r(t){var e=t?t.length:0;for(this.data={hash:a(null),set:new u};e--;)this.push(t[e])}var i=n(113),o=n(20),u=o(e,"Set"),a=o(Object,"create");r.prototype.push=i,t.exports=r}).call(e,function(){return this}())},function(t,e){"use strict";function n(t,e){for(var n=-1,r=t.length,i=Array(r);++ne&&!o||!i||n&&!u&&a||r&&a)return 1;if(e>t&&!n||!a||o&&!r&&i||u&&i)return-1}return 0}t.exports=n},function(t,e,n){"use strict";var r=n(100),i=n(115),o=i(r);t.exports=o},function(t,e,n){"use strict";function r(t,e,n,c){c||(c=[]);for(var l=-1,f=t.length;++le&&(e=-e>i?0:i+e),n=void 0===n||n>i?i:+n||0,0>n&&(n+=i),i=e>n?0:n-e>>>0,e>>>=0;for(var o=Array(i);++r=a,f=l?u():null,d=[];f?(r=o,c=!1):(l=!1,f=e?[]:d);t:for(;++nc))return!1;for(;++s0;++r1}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(58),e["default"]=new o["default"]({is:"ha-introduction-card",properties:{showInstallInstruction:{type:Boolean,value:!1},showHideInstruction:{type:Boolean,value:!0}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(63),a=r(u);e["default"]=new o["default"]({is:"display-time",properties:{dateObj:{type:Object}},computeTime:function(t){return t?a["default"](t):""}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);e["default"]=new u["default"]({is:"entity-list",behaviors:[s["default"]],properties:{entities:{type:Array,bindNuclear:[i.entityGetters.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.entityId}).toArray()}]}},entitySelected:function(t){t.preventDefault(),this.fire("entity-selected",{entityId:t.model.entity.entityId})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(2);n(30),e["default"]=new o["default"]({is:"ha-entity-marker",properties:{entityId:{type:String,value:""},state:{type:Object,computed:"computeState(entityId)"},icon:{type:Object,computed:"computeIcon(state)"},image:{type:Object,computed:"computeImage(state)"},value:{type:String,computed:"computeValue(state)"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;t.stopPropagation(),this.entityId&&this.async(function(){return u.moreInfoActions.selectEntity(e.entityId)},1)},computeState:function(t){return t&&u.reactor.evaluate(u.entityGetters.byId(t))},computeIcon:function(t){return!t&&"home"},computeImage:function(t){return t&&t.attributes.entity_picture},computeValue:function(t){return t&&t.entityDisplay.split(" ").map(function(t){return t.substr(0,1)}).join("")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(2),a=n(62),s=r(a),c=n(34),l=r(c);n(30),e["default"]=new o["default"]({is:"ha-state-label-badge",properties:{state:{type:Object,observer:"stateChanged"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;return t.stopPropagation(),l["default"](this.state.entityId)?void("scene"===this.state.domain?u.serviceActions.callTurnOn(this.state.entityId):"off"===this.state.state?u.serviceActions.callTurnOn(this.state.entityId):u.serviceActions.callTurnOff(this.state.entityId)):void this.async(function(){return u.moreInfoActions.selectEntity(e.state.entityId)},1)},computeClasses:function(t){switch(t.domain){case"scene":return"green";case"script":return"on"===t.state?"blue":"grey";default:return""}},computeGlow:function(t){switch(t.domain){case"scene":case"script":return"on"===t.state;default:return!1}},computeValue:function(t){switch(t.domain){case"device_tracker":case"sun":case"scene":case"script":case"alarm_control_panel":return void 0;case"sensor":return t.attributes.unit_of_measurement&&t.state;default:return t.state}},computeIcon:function(t){switch(t.domain){case"device_tracker":case"alarm_control_panel":case"scene":case"script":return s["default"](t.domain,t.state);case"sensor":return!t.attributes.unit_of_measurement&&s["default"](t.domain);case"sun":return"above_horizon"===t.state?"image:wb-sunny":"image:brightness-3";default:return void 0}},computeImage:function(t){return t.attributes.entity_picture},computeLabel:function(t){switch(t.domain){case"scene":case"script":return t.domain;case"sensor":return t.attributes.unit_of_measurement||t.state;case"device_tracker":return"not_home"===t.state?"Away":t.state;case"alarm_control_panel":return t.state;default:return t.attributes.unit_of_measurement}},computeDescription:function(t){return t.entityDisplay},stateChanged:function(){this.updateStyles()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(182),a=r(u);n(29),e["default"]=new o["default"]({is:"state-badge",properties:{stateObj:{type:Object,observer:"updateIconColor"}},updateIconColor:function(t){if("light"===t.domain&&"on"===t.state&&t.attributes.brightness&&t.attributes.xy_color){var e=a["default"](t.attributes.xy_color[0],t.attributes.xy_color[1],t.attributes.brightness);this.$.icon.style.color="rgb("+e.map(Math.floor).join(",")+")"}else this.$.icon.style.color=null}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);e["default"]=new u["default"]({is:"events-list",behaviors:[s["default"]],properties:{events:{type:Array,bindNuclear:[i.eventGetters.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.event}).toArray()}]}},eventSelected:function(t){t.preventDefault(),this.fire("event-selected",{eventType:t.model.event.event})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){var e=t.toString(16);return 1===e.length?"0"+e:e}function o(t){return"#"+i(t.r)+i(t.g)+i(t.b)}Object.defineProperty(e,"__esModule",{value:!0});var u=n(1),a=r(u);e["default"]=new a["default"]({is:"ha-color-picker",properties:{width:{type:Number,value:300},height:{type:Number,value:300},color:{type:Object}},listeners:{mousedown:"onMouseDown",mouseup:"onMouseUp",touchstart:"onTouchStart",touchend:"onTouchEnd",tap:"onTap"},onMouseDown:function(t){this.onMouseMove(t),this.addEventListener("mousemove",this.onMouseMove)},onMouseUp:function(){this.removeEventListener("mousemove",this.onMouseMove)},onTouchStart:function(t){this.onTouchMove(t),this.addEventListener("touchmove",this.onTouchMove)},onTouchEnd:function(){this.removeEventListener("touchmove",this.onTouchMove)},onTap:function(t){t.stopPropagation()},onTouchMove:function(t){var e=t.touches[0];this.onColorSelect(t,{x:e.clientX,y:e.clientY})},onMouseMove:function(t){var e=this;t.preventDefault(),this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.onColorSelect(t),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},onColorSelect:function(t,e){if(this.context){var n=e||this.relativeMouseCoordinates(t),r=this.context.getImageData(n.x,n.y,1,1).data;this.setColor({r:r[0],g:r[1],b:r[2]})}},setColor:function(t){this.color={hex:o(t),rgb:t},this.fire("colorselected",{rgb:this.color.rgb,hex:this.color.hex})},relativeMouseCoordinates:function(t){var e=0,n=0;if(this.canvas){var r=this.canvas.getBoundingClientRect();e=t.clientX-r.left,n=t.clientY-r.top}return{x:e,y:n}},ready:function(){this.setColor=this.setColor.bind(this),this.mouseMoveIsThrottled=!0,this.canvas=this.children[0],this.context=this.canvas.getContext("2d");var t=this.context.createLinearGradient(0,0,this.width,0);t.addColorStop(0,"rgb(255,0,0)"),t.addColorStop(.16,"rgb(255,0,255)"),t.addColorStop(.32,"rgb(0,0,255)"),t.addColorStop(.48,"rgb(0,255,255)"),t.addColorStop(.64,"rgb(0,255,0)"),t.addColorStop(.8,"rgb(255,255,0)"),t.addColorStop(1,"rgb(255,0,0)"),this.context.fillStyle=t,this.context.fillRect(0,0,this.width,this.height);var e=this.context.createLinearGradient(0,0,0,this.height);e.addColorStop(0,"rgba(255,255,255,1)"),e.addColorStop(.5,"rgba(255,255,255,0)"),e.addColorStop(.5,"rgba(0,0,0,0)"),e.addColorStop(1,"rgba(0,0,0,1)"),this.context.fillStyle=e,this.context.fillRect(0,0,this.width,this.height)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(30),e["default"]=new o["default"]({is:"ha-demo-badge"}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(146),e["default"]=new o["default"]({is:"ha-logbook",properties:{entries:{type:Object,value:[]}},noEntries:function(t){return!t.length}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(150),e["default"]=new u["default"]({is:"ha-sidebar",behaviors:[s["default"]],properties:{menuShown:{type:Boolean},menuSelected:{type:String},selected:{type:String,bindNuclear:i.navigationGetters.activePane,observer:"selectedChanged"},hasHistoryComponent:{type:Boolean,bindNuclear:i.configGetters.isComponentLoaded("history")},hasLogbookComponent:{type:Boolean,bindNuclear:i.configGetters.isComponentLoaded("logbook")}},selectedChanged:function(t){for(var e=this.querySelectorAll(".menu [data-panel]"),n=0;nd;d++)f._columns[d]=[];var p=0;return n&&u(),c.keySeq().sortBy(function(t){return i(t)}).forEach(function(t){if("a"===t)return void(f._demo=!0);var n=i(t);n>=0&&10>n?f._badges.push.apply(f._badges,r(c.get(t)).sortBy(o).toArray()):"group"===t?c.get(t).filter(function(t){return!t.attributes.auto}).sortBy(o).forEach(function(t){var n=s.util.expandGroup(t,e);n.forEach(function(t){return l[t.entityId]=!0}),a(t.entityDisplay,n.toArray(),t)}):a(t,r(c.get(t)).sortBy(o).toArray())}),f},computeShouldRenderColumn:function(t,e){return 0===t||e.length},computeShowIntroduction:function(t,e,n){return 0===t&&(e||n._demo)},computeShowHideInstruction:function(t,e){return t.size>0&&!0&&!e._demo},computeGroupEntityOfCard:function(t,e){return t[e].groupEntity},computeStatesOfCard:function(t,e){return t[e].entities}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(29),n(134),n(59),e["default"]=new u["default"]({is:"logbook-entry",entityClicked:function(t){t.preventDefault(),i.moreInfoActions.selectEntity(this.entryObj.entityId)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(29),e["default"]=new u["default"]({is:"services-list",behaviors:[s["default"]],properties:{serviceDomains:{type:Array,bindNuclear:i.serviceGetters.entityMap}},computeDomains:function(t){return t.valueSeq().map(function(t){return t.domain}).sort().toJS()},computeServices:function(t,e){return t.get(e).get("services").keySeq().toArray()},serviceClicked:function(t){t.preventDefault(),this.fire("service-selected",{domain:t.model.domain,service:t.model.service})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(90),o=r(i),u=n(86),a=r(u),s=n(88),c=r(s),l=n(91),f=r(l),d=n(1),p=r(d);e["default"]=new p["default"]({is:"state-history-chart-line",properties:{data:{type:Object,observer:"dataChanged"},unit:{type:String},isSingleDevice:{type:Boolean,value:!1},isAttached:{type:Boolean,value:!1,observer:"dataChanged"}},created:function(){this.style.display="block"},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){if(this.isAttached){for(var t=p["default"].dom(this),e=this.unit,n=this.data;t.lastChild;)t.removeChild(t.lastChild);if(0!==n.length){var r=new google.visualization.LineChart(this),i=new google.visualization.DataTable;i.addColumn({type:"datetime",id:"Time"});var u={legend:{position:"top"},titlePosition:"none",vAxes:{0:{title:e}},hAxis:{format:"H:mm"},lineWidth:1,chartArea:{left:"60",width:"95%"},explorer:{actions:["dragToZoom","rightClickToReset","dragToPan"],keepInBounds:!0,axis:"horizontal",maxZoomIn:.1}};this.isSingleDevice&&(u.legend.position="none",u.vAxes[0].title=null,u.chartArea.left=40,u.chartArea.height="80%",u.chartArea.top=5,u.enableInteractivity=!1);var s=o["default"](a["default"](n),"lastChangedAsDate");s=f["default"](c["default"](s,function(t){return t.getTime()}));for(var l=[],d=new Array(n.length),h=0;hnew Date&&(a=new Date);var s=0;n.forEach(function(e){if(0!==e.length){var n=e[0].entityDisplay,r=void 0,i=null,o=null;e.forEach(function(e){null!==i&&e.state!==i?(r=e.lastChangedAsDate,t(n,i,o,r),i=e.state,o=r):null===i&&(i=e.state,o=e.lastChangedAsDate)}),t(n,i,o,a),s++}}),r.draw(i,{height:55+42*s,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);e["default"]=new u["default"]({is:"stream-status",behaviors:[s["default"]],properties:{isStreaming:{type:Boolean,bindNuclear:i.streamGetters.isStreamingEvents},hasError:{type:Boolean,bindNuclear:i.streamGetters.hasStreamingEventsError}},toggleChanged:function(){this.isStreaming?i.streamActions.stop():i.streamActions.start()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(32),n(60),n(165);var c=["camera","configurator","scene"];e["default"]=new u["default"]({is:"more-info-dialog",behaviors:[s["default"]],properties:{stateObj:{type:Object,bindNuclear:i.moreInfoGetters.currentEntity,observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:[i.moreInfoGetters.currentEntityHistory,function(t){return t?[t]:!1}]},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(_delayedDialogOpen, _isLoadingHistoryData)"},_isLoadingHistoryData:{type:Boolean,bindNuclear:i.entityHistoryGetters.isLoadingEntityHistory},hasHistoryComponent:{type:Boolean,bindNuclear:i.configGetters.isComponentLoaded("history"),observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:i.moreInfoGetters.isCurrentEntityHistoryStale,observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},_delayedDialogOpen:{type:Boolean,value:!1},_boundOnBackdropTap:{type:Function,value:function(){return this._onBackdropTap.bind(this)}}},computeIsLoadingHistoryData:function(t,e){return!t||e},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&i.entityHistoryActions.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(t){var e=this;return t?(this.showHistoryComponent=this.hasHistoryComponent&&-1===c.indexOf(this.stateObj.domain),void this.async(function(){e.fetchHistoryData(),e.dialogOpen=!0},10)):void(this.dialogOpen=!1)},dialogOpenChanged:function(t){var e=this;t?(this.$.dialog.backdropElement.addEventListener("click",this._boundOnBackdropTap),this.async(function(){return e._delayedDialogOpen=!0},10)):!t&&this.stateObj&&(i.moreInfoActions.deselectEntity(),this._delayedDialogOpen=!1)},_onBackdropTap:function(){this.$.dialog.backdropElement.removeEventListener("click",this._boundOnBackdropTap),this.dialogOpen=!1}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(4),u=r(o);n(143),n(160),n(158),n(157),n(159),n(154),n(155),n(156),n(161),n(151),e["default"]=new Polymer({is:"home-assistant-main",behaviors:[u["default"]],properties:{narrow:{type:Boolean,value:!1},activePane:{type:String,bindNuclear:i.navigationGetters.activePane,observer:"activePaneChanged"},isSelectedStates:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("states")},isSelectedHistory:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("history")},isSelectedMap:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("map")},isSelectedLogbook:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("logbook")},isSelectedDevEvent:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("devEvent")},isSelectedDevState:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("devState")},isSelectedDevService:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("devService")},showSidebar:{type:Boolean,bindNuclear:i.navigationGetters.showSidebar}},listeners:{"open-menu":"openMenu","close-menu":"closeMenu"},openMenu:function(){this.narrow?this.$.drawer.openDrawer():i.navigationActions.showSidebar(!0)},closeMenu:function(){this.$.drawer.closeDrawer(),this.showSidebar&&i.navigationActions.showSidebar(!1)},activePaneChanged:function(){this.narrow&&this.$.drawer.closeDrawer()},attached:function(){i.startUrlSync()},computeForceNarrow:function(t,e){return t||!e},detached:function(){i.stopUrlSync()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(2),a=n(4),s=r(a),c=n(64),l=r(c);e["default"]=new o["default"]({is:"login-form",behaviors:[s["default"]],properties:{isValidating:{type:Boolean,observer:"isValidatingChanged",bindNuclear:u.authGetters.isValidating},isInvalid:{type:Boolean,bindNuclear:u.authGetters.isInvalidAttempt},errorMessage:{type:String,bindNuclear:u.authGetters.attemptErrorMessage}},listeners:{keydown:"passwordKeyDown","loginButton.click":"validatePassword"},observers:["validatingChanged(isValidating, isInvalid)"],validatingChanged:function(t,e){t||e||(this.$.passwordInput.value="")},isValidatingChanged:function(t){var e=this;t||this.async(function(){return e.$.passwordInput.focus()},10)},passwordKeyDown:function(t){13===t.keyCode?(this.validatePassword(),t.preventDefault()):this.isInvalid&&(this.isInvalid=!1)},validatePassword:function(){this.$.hideKeyboardOnFocus.focus(),l["default"](this.$.passwordInput.value,this.$.rememberLogin.checked)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(15),n(147),e["default"]=new u["default"]({is:"partial-dev-call-service",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},domain:{type:String,value:""},service:{type:String,value:""},serviceData:{type:String,value:""},description:{type:String,computed:"computeDescription(domain, service)"}},computeDescription:function(t,e){return i.reactor.evaluate([i.serviceGetters.entityMap,function(n){return n.has(t)&&n.get(t).get("services").has(e)?JSON.stringify(n.get(t).get("services").get(e).toJS(),null,2):"No description available"}])},serviceSelected:function(t){this.domain=t.detail.domain,this.service=t.detail.service},callService:function(){var t=void 0;try{t=this.serviceData?JSON.parse(this.serviceData):{}}catch(e){return void alert("Error parsing JSON: "+e)}i.serviceActions.callService(this.domain,this.service,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(15),n(139),e["default"]=new u["default"]({is:"partial-dev-fire-event",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(t){this.eventType=t.detail.eventType},fireEvent:function(){var t=void 0;try{t=this.eventData?JSON.parse(this.eventData):{}}catch(e){return void alert("Error parsing JSON: "+e)}i.eventActions.fireEvent(this.eventType,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(15),n(135),e["default"]=new u["default"]({is:"partial-dev-set-state",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(t){var e=t?JSON.stringify(t,null," "):"";this.$.inputData.value=e,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(t){var e=i.reactor.evaluate(i.entityGetters.byId(t.detail.entityId));this.entityId=e.entityId,this.state=e.state,this.stateAttributes=JSON.stringify(e.attributes,null," ")},handleSetState:function(){var t=void 0;try{t=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(e){return void alert("Error parsing JSON: "+e)}i.entityActions.save({entityId:this.entityId,state:this.state,attributes:t})},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(15),n(60),e["default"]=new u["default"]({is:"partial-history",behaviors:[s["default"]],properties:{narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:i.entityHistoryGetters.hasDataForCurrentDate,observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:i.entityHistoryGetters.entityHistoryForCurrentDate},isLoadingData:{type:Boolean,bindNuclear:i.entityHistoryGetters.isLoadingEntityHistory},selectedDate:{type:String,value:null,bindNuclear:i.entityHistoryGetters.currentDate}},isDataLoadedChanged:function(t){t||this.async(function(){return i.entityHistoryActions.fetchSelectedDate()},1)},handleRefreshClick:function(){i.entityHistoryActions.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new Pikaday({field:this.$.datePicker.inputElement,onSelect:i.entityHistoryActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(t){return"flex content "+(t?"narrow":"wide")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(15),n(142),n(31),e["default"]=new u["default"]({is:"partial-logbook",behaviors:[s["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:i.logbookGetters.currentDate},isLoading:{type:Boolean,bindNuclear:i.logbookGetters.isLoadingEntries},isStale:{type:Boolean,bindNuclear:i.logbookGetters.isCurrentStale,observer:"isStaleChanged"},entries:{type:Array,bindNuclear:[i.logbookGetters.currentEntries,function(t){return t.reverse().toArray()}]},datePicker:{type:Object}},isStaleChanged:function(t){var e=this;t&&this.async(function(){return i.logbookActions.fetchDate(e.selectedDate)},1)},handleRefresh:function(){i.logbookActions.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new Pikaday({field:this.$.datePicker.inputElement,onSelect:i.logbookActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(136),L.Icon.Default.imagePath="/static/images/leaflet",e["default"]=new u["default"]({is:"partial-map",behaviors:[s["default"]],properties:{locationGPS:{type:Number,bindNuclear:i.configGetters.locationGPS},locationName:{type:String,bindNuclear:i.configGetters.locationName},locationEntities:{type:Array,bindNuclear:[i.entityGetters.visibleEntityMap,function(t){return t.valueSeq().filter(function(t){return t.attributes.latitude&&"home"!==t.state}).toArray()}]},zoneEntities:{type:Array,bindNuclear:[i.entityGetters.entityMap,function(t){return t.valueSeq().filter(function(t){return"zone"===t.domain}).toArray()}]},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1}},attached:function(){var t=this;L.Browser.mobileWebkit&&this.async(function(){var e=t.$.map,n=e.style.display;e.style.display="none",t.async(function(){e.style.display=n},1)},1)},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},toggleMenu:function(){this.fire("open-menu")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(15),n(144),n(145),e["default"]=new u["default"]({is:"partial-zone",behaviors:[s["default"]],properties:{narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:i.syncGetters.isFetching},isStreaming:{type:Boolean,bindNuclear:i.streamGetters.isStreamingEvents},canListen:{type:Boolean,bindNuclear:[i.voiceGetters.isVoiceSupported,i.configGetters.isComponentLoaded("conversation"),function(t,e){return t&&e}]},isListening:{type:Boolean,bindNuclear:i.voiceGetters.isListening},showListenInterface:{type:Boolean,bindNuclear:[i.voiceGetters.isListening,i.voiceGetters.isTransmitting,function(t,e){return t||e}]},introductionLoaded:{type:Boolean,bindNuclear:i.configGetters.isComponentLoaded("introduction")},locationName:{type:String,bindNuclear:i.configGetters.locationName},showMenu:{type:Boolean,value:!1,observer:"windowChange"},states:{type:Object,bindNuclear:i.entityGetters.visibleEntityMap},columns:{type:Number}},created:function(){var t=this;this.windowChange=this.windowChange.bind(this);for(var e=[],n=0;5>n;n++)e.push(278+278*n);this.mqls=e.map(function(e){var n=window.matchMedia("(min-width: "+e+"px)");return n.addListener(t.windowChange),n})},detached:function(){var t=this;this.mqls.forEach(function(e){return e.removeListener(t.windowChange)})},windowChange:function(){var t=this.mqls.reduce(function(t,e){return t+e.matches},0);this.columns=Math.max(1,t-this.showMenu)},handleRefresh:function(){i.syncActions.fetchAll()},handleListenClick:function(){this.isListening?i.voiceActions.stop():i.voiceActions.listen()},computeDomains:function(t){return t.keySeq().toArray()},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},computeStatesOfDomain:function(t,e){return t.get(e).toArray()},computeListenButtonIcon:function(t){return t?"av:mic-off":"av:mic"},computeRefreshButtonClass:function(t){return t?"ha-spin":void 0},computeShowIntroduction:function(t,e){return t||0===e.size},toggleMenu:function(){this.fire("open-menu")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);e["default"]=new u["default"]({is:"notification-manager",behaviors:[s["default"]],properties:{text:{type:String,bindNuclear:i.notificationGetters.lastNotificationMessage,observer:"showNotification"}},showNotification:function(t){t&&this.$.toast.show()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);e["default"]=new u["default"]({is:"more-info-alarm_control_panel",handleDisarmTap:function(){this.callService("alarm_disarm",{code:this.enteredCode})},handleHomeTap:function(){this.callService("alarm_arm_home",{code:this.enteredCode})},handleAwayTap:function(){this.callService("alarm_arm_away",{code:this.enteredCode})},properties:{stateObj:{type:Object,observer:"stateObjChanged"},enteredCode:{type:String,value:""},disarmButtonVisible:{type:Boolean,value:!1},armHomeButtonVisible:{type:Boolean,value:!1},armAwayButtonVisible:{type:Boolean,value:!1},codeInputVisible:{type:Boolean,value:!1},codeInputEnabled:{type:Boolean,value:!1},codeFormat:{type:String,value:""},codeValid:{type:Boolean,computed:"validateCode(enteredCode, codeFormat)"}},validateCode:function(t,e){var n=new RegExp(e);return null===e?!0:n.test(t)},stateObjChanged:function(t){var e=this;t&&(this.codeFormat=t.attributes.code_format,this.codeInputVisible=null!==this.codeFormat,this.codeInputEnabled="armed_home"===t.state||"armed_away"===t.state||"disarmed"===t.state||"pending"===t.state||"triggered"===t.state,this.disarmButtonVisible="armed_home"===t.state||"armed_away"===t.state||"pending"===t.state||"triggered"===t.state,this.armHomeButtonVisible="disarmed"===t.state,this.armAwayButtonVisible="disarmed"===t.state),this.async(function(){return e.fire("iron-resize")},500)},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,i.serviceActions.callService("alarm_control_panel",t,n)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-camera",properties:{stateObj:{type:Object},dialogOpen:{type:Boolean}},imageLoaded:function(){this.fire("iron-resize")},computeCameraImageUrl:function(t){return t?"/api/camera_proxy_stream/"+this.stateObj.entityId:"data:image/gif;base64,R0lGODlhAQABAAAAACw="}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(31),e["default"]=new u["default"]({is:"more-info-configurator",behaviors:[s["default"]],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:i.streamGetters.isStreamingEvents},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"}},computeIsConfigurable:function(t){return"configure"===t.state},computeSubmitCaption:function(t){return t.attributes.submit_caption||"Set configuration"},submitClicked:function(){var t=this;this.isConfiguring=!0;var e={configure_id:this.stateObj.attributes.configure_id};i.serviceActions.callService("configurator","configure",e).then(function(){t.isConfiguring=!1,t.isStreaming||i.syncActions.fetchAll()},function(){t.isConfiguring=!1})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(181),a=r(u);n(166),n(167),n(171),n(164),n(172),n(170),n(168),n(169),n(163),n(173),n(162),e["default"]=new o["default"]({is:"more-info-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"}},dialogOpenChanged:function(t){var e=o["default"].dom(this);e.lastChild&&(e.lastChild.dialogOpen=t)},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=a["default"](t);if(e&&a["default"](e)===r)n.lastChild.dialogOpen=this.dialogOpen,n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("more-info-"+r);i.stateObj=t,i.dialogOpen=this.dialogOpen,n.appendChild(i)}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=["entity_picture","friendly_name","unit_of_measurement"];e["default"]=new o["default"]({is:"more-info-default",properties:{stateObj:{type:Object}},computeDisplayAttributes:function(t){return t?Object.keys(t.attributes).filter(function(t){return-1===u.indexOf(t)}):[]},getAttributeValue:function(t,e){return t.attributes[e]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(32),e["default"]=new u["default"]({is:"more-info-group",behaviors:[s["default"]],properties:{stateObj:{type:Object},states:{type:Array,bindNuclear:[i.moreInfoGetters.currentEntity,i.entityGetters.entityMap,function(t,e){return t?t.attributes.entity_id.map(e.get.bind(e)):[]}]}},updateStates:function(){this.states=this.stateObj&&this.stateObj.attributes.entity_id?stateStore.gets(this.stateObj.attributes.entity_id).toArray():[]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(33),s=r(a);n(140);var c=["brightness","xy_color"];e["default"]=new u["default"]({is:"more-info-light",properties:{stateObj:{type:Object,observer:"stateObjChanged"},brightnessSliderValue:{type:Number,value:0}},stateObjChanged:function(t){var e=this;t&&"on"===t.state&&(this.brightnessSliderValue=t.attributes.brightness),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return s["default"](t,c)},brightnessSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||(0===e?i.serviceActions.callTurnOff(this.stateObj.entityId):i.serviceActions.callService("light","turn_on",{entity_id:this.stateObj.entityId,brightness:e}))},colorPicked:function(t){var e=t.detail.rgb;i.serviceActions.callService("light","turn_on",{entity_id:this.stateObj.entityId,rgb_color:[e.r,e.g,e.b]})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(33),s=r(a),c=["volume_level"];e["default"]=new u["default"]({is:"more-info-media_player",properties:{stateObj:{type:Object,observer:"stateObjChanged"},isOff:{type:Boolean,value:!1},isPlaying:{type:Boolean,value:!1},isMuted:{type:Boolean,value:!1},volumeSliderValue:{type:Number,value:0},supportsPause:{type:Boolean,value:!1},supportsVolumeSet:{type:Boolean,value:!1},supportsVolumeMute:{type:Boolean,value:!1},supportsPreviousTrack:{type:Boolean,value:!1},supportsNextTrack:{type:Boolean,value:!1},supportsTurnOn:{type:Boolean,value:!1},supportsTurnOff:{type:Boolean,value:!1}},stateObjChanged:function(t){var e=this;t&&(this.isOff="off"===t.state,this.isPlaying="playing"===t.state,this.volumeSliderValue=100*t.attributes.volume_level,this.isMuted=t.attributes.is_volume_muted,this.supportsPause=0!==(1&t.attributes.supported_media_commands),this.supportsVolumeSet=0!==(4&t.attributes.supported_media_commands),this.supportsVolumeMute=0!==(8&t.attributes.supported_media_commands),this.supportsPreviousTrack=0!==(16&t.attributes.supported_media_commands),this.supportsNextTrack=0!==(32&t.attributes.supported_media_commands),this.supportsTurnOn=0!==(128&t.attributes.supported_media_commands),this.supportsTurnOff=0!==(256&t.attributes.supported_media_commands)),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return s["default"](t,c)},computeIsOff:function(t){return"off"===t.state},computeMuteVolumeIcon:function(t){return t?"av:volume-off":"av:volume-up"},computePlaybackControlIcon:function(){return this.isPlaying?this.supportsPause?"av:pause":"av:stop":"av:play-arrow"},computeHidePowerButton:function(t,e,n){return t?!e:!n},handleTogglePower:function(){this.callService(this.isOff?"turn_on":"turn_off")},handlePrevious:function(){this.callService("media_previous_track")},handlePlaybackControl:function(){this.callService("media_play_pause")},handleNext:function(){this.callService("media_next_track")},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},volumeSliderChanged:function(t){var e=parseFloat(t.target.value),n=e>0?e/100:0;this.callService("volume_set",{volume_level:n})},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,i.serviceActions.callService("media_player",t,n)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-script",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(63),u=r(o),a=i.util.parseDateTime;e["default"]=new Polymer({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object,computed:"computeSetting(stateObj)"}},computeRising:function(t){return a(t.attributes.next_rising)},computeSetting:function(t){return a(t.attributes.next_setting)},computeOrder:function(t,e){return t>e?["set","ris"]:["ris","set"]},itemCaption:function(t){return"ris"===t?"Rising ":"Setting "},itemDate:function(t){return"ris"===t?this.risingDate:this.settingDate},itemValue:function(t){return u["default"](this.itemDate(t))}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(33),s=r(a),c=["away_mode"];e["default"]=new u["default"]({is:"more-info-thermostat",properties:{stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(t){this.targetTemperatureSliderValue=t.state,this.awayToggleChecked="on"===t.attributes.away_mode,this.tempMin=t.attributes.min_temp,this.tempMax=t.attributes.max_temp},computeClassNames:function(t){return s["default"](t,c)},targetTemperatureSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||i.serviceActions.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:e})},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):e||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(t){var e=this;i.serviceActions.callService("thermostat","set_away_mode",{away_mode:t,entity_id:this.stateObj.entityId}).then(function(){return e.stateObjChanged(e.stateObj)})}}),t.exports=e["default"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e["default"]=new Polymer({is:"more-info-updater",properties:{stateObj:{type:Object}},updateTapped:function(){r.serviceActions.callService("updater","update",{})},linkTapped:function(){window.open(this.stateObj.attributes.link,"_blank")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(14),n(61),e["default"]=new o["default"]({is:"state-card-configurator",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict"; +function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(14);var u=["playing","paused"];e["default"]=new o["default"]({is:"state-card-media_player",properties:{stateObj:{type:Object},isPlaying:{type:Boolean,computed:"computeIsPlaying(stateObj)"}},computeIsPlaying:function(t){return-1!==u.indexOf(t.state)},computePrimaryText:function(t,e){return e?t.attributes.media_title:t.stateDisplay},computeSecondaryText:function(t){var e=void 0;return"music"===t.attributes.media_content_type?t.attributes.media_artist:"tvshow"===t.attributes.media_content_type?(e=t.attributes.media_series_title,t.attributes.media_season&&t.attributes.media_episode&&(e+=" S"+t.attributes.media_season+"E"+t.attributes.media_episode),e):t.attributes.app_name?t.attributes.app_name:""}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(2);n(14),e["default"]=new o["default"]({is:"state-card-scene",properties:{stateObj:{type:Object}},activateScene:function(t){u.serviceActions.callTurnOn(this.stateObj.entityId)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(14),e["default"]=new o["default"]({is:"state-card-thermostat",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(14),n(57),e["default"]=new o["default"]({is:"state-card-toggle"}),t.exports=e["default"]},function(t,e){"use strict";function n(t){return{attached:function(){var e=this;this.__unwatchFns=Object.keys(this.properties).reduce(function(n,r){if(!("bindNuclear"in e.properties[r]))return n;var i=e.properties[r].bindNuclear;if(!i)throw new Error("Undefined getter specified for key "+r);return e[r]=t.evaluate(i),n.concat(t.observe(i,function(t){e[r]=t}))},[])},detached:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return-1!==a.indexOf(t.domain)?t.domain:u["default"](t.entityId)?"toggle":"display"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(34),u=r(o),a=["thermostat","configurator","scene","media_player"];t.exports=e["default"]},function(t,e){"use strict";function n(t){return-1!==r.indexOf(t.domain)?t.domain:"default"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n;var r=["light","group","sun","configurator","thermostat","script","media_player","camera","updater","alarm_control_panel"];t.exports=e["default"]},function(t,e){"use strict";function n(t,e,n){var r=1-t-e,i=n/255,o=i/e*t,u=i/e*r,a=1.612*o-.203*i-.302*u,s=.509*-o+1.412*i+.066*u,c=.026*o-.072*i+.962*u;a=.0031308>=a?12.92*a:1.055*Math.pow(a,1/2.4)-.055,s=.0031308>=s?12.92*s:1.055*Math.pow(s,1/2.4)-.055,c=.0031308>=c?12.92*c:1.055*Math.pow(c,1/2.4)-.055;var l=Math.max(a,s,c);return a/=l,s/=l,c/=l,a=255*a,0>a&&(a=255),s=255*s,0>s&&(s=255),c=255*c,0>c&&(c=255),[a,s,c]}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){var r;(function(t,i,o){"use strict";(function(){function u(t){return"function"==typeof t||"object"==typeof t&&null!==t}function a(t){return"function"==typeof t}function s(t){return"object"==typeof t&&null!==t}function c(t){W=t}function l(t){Z=t}function f(){return function(){t.nextTick(_)}}function d(){return function(){q(_)}}function p(){var t=0,e=new tt(_),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function h(){var t=new MessageChannel;return t.port1.onmessage=_,function(){t.port2.postMessage(0)}}function v(){return function(){setTimeout(_,1)}}function _(){for(var t=0;$>t;t+=2){var e=rt[t],n=rt[t+1];e(n),rt[t]=void 0,rt[t+1]=void 0}$=0}function y(){try{var t=n(254);return q=t.runOnLoop||t.runOnContext,d()}catch(e){return v()}}function m(){}function g(){return new TypeError("You cannot resolve a promise with itself")}function b(){return new TypeError("A promises callback cannot return that same promise.")}function O(t){try{return t.then}catch(e){return at.error=e,at}}function w(t,e,n,r){try{t.call(e,n,r)}catch(i){return i}}function S(t,e,n){Z(function(t){var r=!1,i=w(n,e,function(n){r||(r=!0,e!==n?T(t,n):I(t,n))},function(e){r||(r=!0,P(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&i&&(r=!0,P(t,i))},t)}function M(t,e){e._state===ot?I(t,e._result):e._state===ut?P(t,e._result):D(e,void 0,function(e){T(t,e)},function(e){P(t,e)})}function j(t,e){if(e.constructor===t.constructor)M(t,e);else{var n=O(e);n===at?P(t,at.error):void 0===n?I(t,e):a(n)?S(t,e,n):I(t,e)}}function T(t,e){t===e?P(t,g()):u(e)?j(t,e):I(t,e)}function E(t){t._onerror&&t._onerror(t._result),C(t)}function I(t,e){t._state===it&&(t._result=e,t._state=ot,0!==t._subscribers.length&&Z(C,t))}function P(t,e){t._state===it&&(t._state=ut,t._result=e,Z(E,t))}function D(t,e,n,r){var i=t._subscribers,o=i.length;t._onerror=null,i[o]=e,i[o+ot]=n,i[o+ut]=r,0===o&&t._state&&Z(C,t)}function C(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,i,o=t._result,u=0;uu;u++)D(r.resolve(t[u]),void 0,e,n);return i}function H(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(m);return T(n,t),n}function Y(t){var e=this,n=new e(m);return P(n,t),n}function G(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function F(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function U(t){this._id=ht++,this._state=void 0,this._result=void 0,this._subscribers=[],m!==t&&(a(t)||G(),this instanceof U||F(),L(this,t))}function B(){var t;if("undefined"!=typeof i)t=i;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=vt)}var V;V=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var q,W,K,J=V,$=0,Z=({}.toString,function(t,e){rt[$]=t,rt[$+1]=e,$+=2,2===$&&(W?W(_):K())}),X="undefined"!=typeof window?window:void 0,Q=X||{},tt=Q.MutationObserver||Q.WebKitMutationObserver,et="undefined"!=typeof t&&"[object process]"==={}.toString.call(t),nt="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,rt=new Array(1e3);K=et?f():tt?p():nt?h():void 0===X?y():v();var it=void 0,ot=1,ut=2,at=new A,st=new A;N.prototype._validateInput=function(t){return J(t)},N.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},N.prototype._init=function(){this._result=new Array(this.length)};var ct=N;N.prototype._enumerate=function(){for(var t=this,e=t.length,n=t.promise,r=t._input,i=0;n._state===it&&e>i;i++)t._eachEntry(r[i],i)},N.prototype._eachEntry=function(t,e){var n=this,r=n._instanceConstructor;s(t)?t.constructor===r&&t._state!==it?(t._onerror=null,n._settledAt(t._state,e,t._result)):n._willSettleAt(r.resolve(t),e):(n._remaining--,n._result[e]=t)},N.prototype._settledAt=function(t,e,n){var r=this,i=r.promise;i._state===it&&(r._remaining--,t===ut?P(i,n):r._result[e]=n),0===r._remaining&&I(i,r._result)},N.prototype._willSettleAt=function(t,e){var n=this;D(t,void 0,function(t){n._settledAt(ot,e,t)},function(t){n._settledAt(ut,e,t)})};var lt=R,ft=z,dt=H,pt=Y,ht=0,vt=U;U.all=lt,U.race=ft,U.resolve=dt,U.reject=pt,U._setScheduler=c,U._setAsap=l,U._asap=Z,U.prototype={constructor:U,then:function(t,e){var n=this,r=n._state;if(r===ot&&!t||r===ut&&!e)return this;var i=new this.constructor(m),o=n._result;if(r){var u=arguments[r-1];Z(function(){k(r,i,u,o)})}else D(n,i,t,e);return i},"catch":function(t){return this.then(null,t)}};var _t=B,yt={Promise:vt,polyfill:_t};n(253).amd?(r=function(){return yt}.call(e,n,e,o),!(void 0!==r&&(o.exports=r))):"undefined"!=typeof o&&o.exports?o.exports=yt:"undefined"!=typeof this&&(this.ES6Promise=yt),_t()}).call(void 0)}).call(e,n(250),function(){return this}(),n(251)(t))},function(t,e,n){"use strict";var r=n(65),i=r(Date,"now"),o=i||function(){return(new Date).getTime()};t.exports=o},function(t,e){"use strict";function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e,n){"use strict";var r=n(65),i=n(185),o=n(66),u="[object Array]",a=Object.prototype,s=a.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==u};t.exports=l},function(t,e,n){"use strict";function r(t){return null==t?!1:i(t)?l.test(s.call(t)):o(t)&&u.test(t)}var i=n(67),o=n(66),u=/^\[object .+?Constructor\]$/,a=Object.prototype,s=Function.prototype.toString,c=a.hasOwnProperty,l=RegExp("^"+s.call(c).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(183),i=n(22),o=function(t,e,n){var o=arguments.length<=3||void 0===arguments[3]?null:arguments[3],u=t.evaluate(i.getters.authInfo),a=u.host+"/api/"+n;return new r.Promise(function(t,n){var r=new XMLHttpRequest;r.open(e,a,!0),r.setRequestHeader("X-HA-access",u.authToken),r.onload=function(){if(r.status>199&&r.status<300)t(JSON.parse(r.responseText));else try{n(JSON.parse(r.responseText))}catch(e){n({})}},r.onerror=function(){return n({})},o?r.send(JSON.stringify(o)):r.send()})};e["default"]=o,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(s.getters.isSupported):r,o=n.rememberAuth,u=void 0===o?!1:o,f=n.host,d=void 0===f?"":f;t.dispatch(a["default"].VALIDATING_AUTH_TOKEN,{authToken:e,host:d}),c.actions.fetchAll(t).then(function(){t.dispatch(a["default"].VALID_AUTH_TOKEN,{authToken:e,host:d,rememberAuth:u}),i?s.actions.start(t,{syncOnInitialConnect:!1}):c.actions.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?l:n;t.dispatch(a["default"].INVALID_AUTH_TOKEN,{errorMessage:r})})}function o(t){t.dispatch(a["default"].LOG_OUT,{})}Object.defineProperty(e,"__esModule",{value:!0}),e.validate=i,e.logOut=o;var u=n(21),a=r(u),s=n(41),c=n(43),l="Unexpected result from API"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=[["authAttempt","isValidating"],function(t){return!!t}];e.isValidating=n;var r=[["authAttempt","isInvalid"],function(t){return!!t}];e.isInvalidAttempt=r;var i=["authAttempt","errorMessage"];e.attemptErrorMessage=i;var o=["rememberAuth"];e.rememberAuth=o;var u=[["authAttempt","authToken"],["authAttempt","host"],function(t,e){return{authToken:t,host:e}}];e.attemptAuthInfo=u;var a=["authCurrent","authToken"];e.currentAuthToken=a;var s=[a,["authCurrent","host"],function(t,e){return{authToken:t,host:e}}];e.currentAuthInfo=s;var c=[n,["authAttempt","authToken"],["authCurrent","authToken"],function(t,e,n){return t?e:n}];e.authToken=c;var l=[n,u,s,function(t,e,n){return t?e:n}];e.authInfo=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(null==t)throw new TypeError("Cannot destructure undefined")}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function u(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function a(t,e){var n=e.authToken,r=e.host;return d.toImmutable({authToken:n,host:r,isValidating:"true",isInvalid:!1,errorMessage:""})}function s(t,e){return i(e),_.getInitialState()}function c(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid","true").set("errorMessage",n)})}Object.defineProperty(e,"__esModule",{value:!0});var l=function(){function t(t,e){for(var n=0;n1&&t.set(p,r)})}function a(){return v.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var s=function(){function t(t,e){for(var n=0;no}Object.defineProperty(e,"__esModule",{value:!0});var i=n(3),o=6e4,u=["currentLogbookDate"];e.currentDate=u;var a=[u,["logbookEntriesUpdated"],function(t,e){return r(e.get(t))}];e.isCurrentStale=a;var s=[u,["logbookEntries"],function(t,e){return e.get(t)||i.toImmutable([])}];e.currentEntries=s;var c=["isLoadingLogbookEntries"];e.isLoadingEntries=c},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({currentLogbookDate:a["default"],isLoadingLogbookEntries:c["default"],logbookEntries:f["default"],logbookEntriesUpdated:p["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(212),a=i(u),s=n(213),c=i(s),l=n(214),f=i(l),d=n(215),p=i(d),h=n(208),v=r(h),_=n(209),y=r(_),m=v;e.actions=m;var g=y;e.getters=g},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var u=function(){function t(t,e){for(var n=0;n1)for(var n=1;n \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index c8d99bc3ea2..c91fcccf29c 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit c8d99bc3ea21cdd7bfb39e7700f92ed09f4b9efd +Subproject commit c91fcccf29c977bb0f8e1143fb26fc75613b6a0f diff --git a/homeassistant/components/frontend/www_static/images/__init__.py b/homeassistant/components/frontend/www_static/images/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 8d09910093b..c1b1579b4b5 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -246,6 +246,7 @@ def setup(hass, config): rgb_color = dat.get(ATTR_RGB_COLOR) if len(rgb_color) == 3: + params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color] params[ATTR_XY_COLOR] = \ color_util.color_RGB_to_xy(int(rgb_color[0]), int(rgb_color[1]), diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py new file mode 100644 index 00000000000..3e8ba8b505d --- /dev/null +++ b/homeassistant/components/light/blinksticklight.py @@ -0,0 +1,76 @@ +""" +homeassistant.components.light.blinksticklight +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Blinkstick lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.blinksticklight.html +""" +import logging + +from blinkstick import blinkstick + +from homeassistant.components.light import (Light, ATTR_RGB_COLOR) + +_LOGGER = logging.getLogger(__name__) + + +REQUIREMENTS = ["blinkstick==1.1.7"] +DEPENDENCIES = [] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add device specified by serial number. """ + stick = blinkstick.find_by_serial(config['serial']) + + add_devices_callback([BlinkStickLight(stick, config['name'])]) + + +class BlinkStickLight(Light): + """ Represents a BlinkStick light. """ + + def __init__(self, stick, name): + self._stick = stick + self._name = name + self._serial = stick.get_serial() + self._rgb_color = stick.get_color() + + @property + def should_poll(self): + """ Polling needed. """ + return True + + @property + def name(self): + """ The name of the light. """ + return self._name + + @property + def rgb_color(self): + """ Read back the color of the light. """ + return self._rgb_color + + @property + def is_on(self): + """ Check whether any of the LEDs colors are non-zero. """ + return sum(self._rgb_color) > 0 + + def update(self): + """ Read back the device state """ + self._rgb_color = self._stick.get_color() + + def turn_on(self, **kwargs): + """ Turn the device on. """ + if ATTR_RGB_COLOR in kwargs: + self._rgb_color = kwargs[ATTR_RGB_COLOR] + else: + self._rgb_color = [255, 255, 255] + + self._stick.set_color(red=self._rgb_color[0], + green=self._rgb_color[1], + blue=self._rgb_color[2]) + + def turn_off(self, **kwargs): + """ Turn the device off """ + self._stick.turn_off() diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py new file mode 100644 index 00000000000..5d6f41fe509 --- /dev/null +++ b/homeassistant/components/light/rfxtrx.py @@ -0,0 +1,112 @@ +""" +homeassistant.components.light.rfxtrx +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for RFXtrx lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.rfxtrx.html +""" +import logging +import homeassistant.components.rfxtrx as rfxtrx +import RFXtrx as rfxtrxmod + +from homeassistant.components.light import Light +from homeassistant.util import slugify + +DEPENDENCIES = ['rfxtrx'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Setup the RFXtrx platform. """ + lights = [] + devices = config.get('devices', None) + if devices: + for entity_id, entity_info in devices.items(): + if entity_id not in rfxtrx.RFX_DEVICES: + _LOGGER.info("Add %s rfxtrx.light", entity_info['name']) + rfxobject = rfxtrx.get_rfx_object(entity_info['packetid']) + new_light = RfxtrxLight(entity_info['name'], rfxobject, False) + rfxtrx.RFX_DEVICES[entity_id] = new_light + lights.append(new_light) + + add_devices_callback(lights) + + def light_update(event): + """ Callback for light updates from the RFXtrx gateway. """ + if not isinstance(event.device, rfxtrxmod.LightingDevice): + return + + # Add entity if not exist and the automatic_add is True + entity_id = slugify(event.device.id_string.lower()) + if entity_id not in rfxtrx.RFX_DEVICES: + automatic_add = config.get('automatic_add', False) + if not automatic_add: + return + + _LOGGER.info( + "Automatic add %s rfxtrx.light (Class: %s Sub: %s)", + entity_id, + event.device.__class__.__name__, + event.device.subtype + ) + pkt_id = "".join("{0:02x}".format(x) for x in event.data) + entity_name = "%s : %s" % (entity_id, pkt_id) + new_light = RfxtrxLight(entity_name, event, False) + rfxtrx.RFX_DEVICES[entity_id] = new_light + add_devices_callback([new_light]) + + # Check if entity exists or previously added automatically + if entity_id in rfxtrx.RFX_DEVICES: + if event.values['Command'] == 'On'\ + or event.values['Command'] == 'Off': + if event.values['Command'] == 'On': + rfxtrx.RFX_DEVICES[entity_id].turn_on() + else: + rfxtrx.RFX_DEVICES[entity_id].turn_off() + + # Subscribe to main rfxtrx events + if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) + + +class RfxtrxLight(Light): + """ Provides a RFXtrx light. """ + def __init__(self, name, event, state): + self._name = name + self._event = event + self._state = state + + @property + def should_poll(self): + """ No polling needed for a light. """ + return False + + @property + def name(self): + """ Returns the name of the light if any. """ + return self._name + + @property + def is_on(self): + """ True if light is on. """ + return self._state + + def turn_on(self, **kwargs): + """ Turn the light on. """ + + if hasattr(self, '_event') and self._event: + self._event.device.send_on(rfxtrx.RFXOBJECT.transport) + + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the light off. """ + + if hasattr(self, '_event') and self._event: + self._event.device.send_off(rfxtrx.RFXOBJECT.transport) + + self._state = False + self.update_ha_state() diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 143473e2fde..294fccbb1f5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -31,6 +31,7 @@ DISCOVERY_PLATFORMS = { } SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' +SERVICE_PLAY_MEDIA = 'play_media' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted' @@ -46,6 +47,8 @@ ATTR_MEDIA_TRACK = 'media_track' ATTR_MEDIA_SERIES_TITLE = 'media_series_title' ATTR_MEDIA_SEASON = 'media_season' ATTR_MEDIA_EPISODE = 'media_episode' +ATTR_MEDIA_CHANNEL = 'media_channel' +ATTR_MEDIA_PLAYLIST = 'media_playlist' ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' @@ -53,6 +56,9 @@ ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' MEDIA_TYPE_MUSIC = 'music' MEDIA_TYPE_TVSHOW = 'tvshow' MEDIA_TYPE_VIDEO = 'movie' +MEDIA_TYPE_EPISODE = 'episode' +MEDIA_TYPE_CHANNEL = 'channel' +MEDIA_TYPE_PLAYLIST = 'playlist' SUPPORT_PAUSE = 1 SUPPORT_SEEK = 2 @@ -63,6 +69,7 @@ SUPPORT_NEXT_TRACK = 32 SUPPORT_YOUTUBE = 64 SUPPORT_TURN_ON = 128 SUPPORT_TURN_OFF = 256 +SUPPORT_PLAY_MEDIA = 512 YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' @@ -76,6 +83,7 @@ SERVICE_TO_METHOD = { SERVICE_MEDIA_PAUSE: 'media_pause', SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', + SERVICE_PLAY_MEDIA: 'play_media', } ATTR_TO_PROPERTY = [ @@ -92,6 +100,8 @@ ATTR_TO_PROPERTY = [ ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_SEASON, ATTR_MEDIA_EPISODE, + ATTR_MEDIA_CHANNEL, + ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_SUPPORTED_MEDIA_COMMANDS, @@ -180,6 +190,16 @@ def media_previous_track(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) +def play_media(hass, media_type, media_id, entity_id=None): + """ Send the media player the command for playing media. """ + data = {"media_type": media_type, "media_id": media_id} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) + + def setup(hass, config): """ Track states and offer events for media_players. """ component = EntityComponent( @@ -275,6 +295,23 @@ def setup(hass, config): if player.should_poll: player.update_ha_state(True) + def play_media_service(service): + """ Plays specified media_id on the media player. """ + media_type = service.data.get('media_type') + media_id = service.data.get('media_id') + + if media_type is None: + return + + if media_id is None: + return + + for player in component.extract_from_service(service): + player.play_media(media_type, media_id) + + if player.should_poll: + player.update_ha_state(True) + hass.services.register( DOMAIN, "start_fireplace", lambda service: play_youtube_video_service(service, "eyU3bRy2x44"), @@ -289,6 +326,10 @@ def setup(hass, config): DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service, descriptions.get(SERVICE_YOUTUBE_VIDEO)) + hass.services.register( + DOMAIN, SERVICE_PLAY_MEDIA, play_media_service, + descriptions.get(SERVICE_PLAY_MEDIA)) + return True @@ -373,6 +414,16 @@ class MediaPlayerDevice(Entity): """ Episode of current playing media. (TV Show only) """ return None + @property + def media_channel(self): + """ Channel currently playing. """ + return None + + @property + def media_playlist(self): + """ Title of Playlist currently playing. """ + return None + @property def app_id(self): """ ID of the current running app. """ @@ -433,6 +484,10 @@ class MediaPlayerDevice(Entity): """ Plays a YouTube media. """ raise NotImplementedError() + def play_media(self, media_type, media_id): + """ Plays a piece of media. """ + raise NotImplementedError() + # No need to overwrite these. @property def support_pause(self): @@ -469,6 +524,11 @@ class MediaPlayerDevice(Entity): """ Boolean if YouTube is supported. """ return bool(self.supported_media_commands & SUPPORT_YOUTUBE) + @property + def support_play_media(self): + """ Boolean if play media command supported. """ + return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA) + def volume_up(self): """ volume_up media player. """ if self.volume_level < 1: diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 61223446e5f..6f622c9e0cc 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -90,6 +90,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CastDevice(MediaPlayerDevice): """ Represents a Cast device on the network. """ + # pylint: disable=abstract-method # pylint: disable=too-many-public-methods def __init__(self, host): diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py new file mode 100644 index 00000000000..9db17416bda --- /dev/null +++ b/homeassistant/components/media_player/firetv.py @@ -0,0 +1,233 @@ +""" +homeassistant.components.media_player.firetv +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides control over an Amazon Fire TV (/stick) via +python-firetv, a Python 2.x module with a helper script +that exposes a HTTP server to fetch state and perform +actions. + +Steps to configure your Amazon Fire TV stick with Home Assistant: + +1. Turn on ADB Debugging on your Amazon Fire TV: + a. From the main (Launcher) screen, select Settings. + b. Select System > Developer Options. + c. Select ADB Debugging. +2. Find Amazon Fire TV device IP: + a. From the main (Launcher) screen, select Settings. + b. Select System > About > Network. +3. `pip install firetv[firetv-server]` into a Python 2.x environment +4. `firetv-server -d :5555`, background the process +5. Configure Home Assistant as follows: + +media_player: + platform: firetv + # optional: where firetv-server is running (default is 'localhost:5556') + host: localhost:5556 + # optional: device id (default is 'default') + device: livingroom-firetv + # optional: friendly name (default is 'Amazon Fire TV') + name: My Amazon Fire TV + +Note that python-firetv has support for multiple Amazon Fire TV devices. +If you have more than one configured, be sure to specify the device id used. +Run `firetv-server -h` and/or view the source for complete capabilities. + +Possible states are: + - off (TV screen is dark) + - standby (standard UI is active - not apps) + - idle (screen saver is active) + - play (video is playing) + - pause (video is paused) + - disconnected (can't communicate with device) +""" + +import logging +import requests + +from homeassistant.const import ( + STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF, + STATE_UNKNOWN, STATE_STANDBY) + +from homeassistant.components.media_player import ( + MediaPlayerDevice, + SUPPORT_PAUSE, SUPPORT_VOLUME_SET, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK) + +SUPPORT_FIRETV = SUPPORT_PAUSE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET + +DOMAIN = 'firetv' +DEVICE_LIST_URL = 'http://{0}/devices/list' +DEVICE_STATE_URL = 'http://{0}/devices/state/{1}' +DEVICE_ACTION_URL = 'http://{0}/devices/action/{1}/{2}' + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the firetv platform. """ + host = config.get('host', 'localhost:5556') + device_id = config.get('device', 'default') + try: + response = requests.get(DEVICE_LIST_URL.format(host)).json() + if device_id in response['devices'].keys(): + add_devices([ + FireTVDevice( + host, + device_id, + config.get('name', 'Amazon Fire TV') + ) + ]) + _LOGGER.info( + 'Device %s accessible and ready for control', device_id) + else: + _LOGGER.warn( + 'Device %s is not registered with firetv-server', device_id) + except requests.exceptions.RequestException: + _LOGGER.error('Could not connect to firetv-server at %s', host) + + +class FireTV(object): + """ firetv-server client. + + Should a native Python 3 ADB module become available, + python-firetv can support Python 3, it can be added + as a dependency, and this class can be dispensed of. + + For now, it acts as a client to the firetv-server + HTTP server (which must be running via Python 2). + """ + + def __init__(self, host, device_id): + self.host = host + self.device_id = device_id + + @property + def state(self): + """ Get the device state. + + An exception means UNKNOWN state. + """ + try: + response = requests.get( + DEVICE_STATE_URL.format( + self.host, + self.device_id + ) + ).json() + return response.get('state', STATE_UNKNOWN) + except requests.exceptions.RequestException: + _LOGGER.error( + 'Could not retrieve device state for %s', self.device_id) + return STATE_UNKNOWN + + def action(self, action_id): + """ Perform an action on the device. + + There is no action acknowledgment, so exceptions + result in a pass. + """ + try: + requests.get( + DEVICE_ACTION_URL.format( + self.host, + self.device_id, + action_id + ) + ) + except requests.exceptions.RequestException: + _LOGGER.error( + 'Action request for %s was not accepted for device %s', + action_id, self.device_id) + + +class FireTVDevice(MediaPlayerDevice): + """ Represents an Amazon Fire TV device on the network. """ + + def __init__(self, host, device, name): + self._firetv = FireTV(host, device) + self._name = name + self._state = STATE_UNKNOWN + + @property + def name(self): + """ Get the device name. """ + return self._name + + @property + def should_poll(self): + """ Device should be polled. """ + return True + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_FIRETV + + @property + def state(self): + """ State of the player. """ + return self._state + + def update(self): + """ Update device state. """ + self._state = { + 'idle': STATE_IDLE, + 'off': STATE_OFF, + 'play': STATE_PLAYING, + 'pause': STATE_PAUSED, + 'standby': STATE_STANDBY, + 'disconnected': STATE_UNKNOWN, + }.get(self._firetv.state, STATE_UNKNOWN) + + def turn_on(self): + """ Turns on the device. """ + self._firetv.action('turn_on') + + def turn_off(self): + """ Turns off the device. """ + self._firetv.action('turn_off') + + def media_play(self): + """ Send play commmand. """ + self._firetv.action('media_play') + + def media_pause(self): + """ Send pause command. """ + self._firetv.action('media_pause') + + def media_play_pause(self): + """ Send play/pause command. """ + self._firetv.action('media_play_pause') + + def volume_up(self): + """ Send volume up command. """ + self._firetv.action('volume_up') + + def volume_down(self): + """ Send volume down command. """ + self._firetv.action('volume_down') + + def media_previous_track(self): + """ Send previous track command (results in rewind). """ + self._firetv.action('media_previous') + + def media_next_track(self): + """ Send next track command (results in fast-forward). """ + self._firetv.action('media_next') + + def media_seek(self, position): + raise NotImplementedError() + + def mute_volume(self, mute): + raise NotImplementedError() + + def play_youtube(self, media_id): + raise NotImplementedError() + + def set_volume_level(self, volume): + raise NotImplementedError() diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index ecbb144e033..70def719146 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -35,9 +35,10 @@ URL of your running version of iTunes-API. Example: http://192.168.1.50:8181 import logging from homeassistant.components.media_player import ( - MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE, + SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, + SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, + SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS) from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON) @@ -47,7 +48,8 @@ import requests _LOGGER = logging.getLogger(__name__) SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ + SUPPORT_PLAY_MEDIA SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF @@ -118,6 +120,20 @@ class Itunes(object): """ Skips back and returns the current state. """ return self._command('previous') + def play_playlist(self, playlist_id_or_name): + """ Sets a playlist to be current and returns the current state. """ + response = self._request('GET', '/playlists') + playlists = response.get('playlists', []) + + found_playlists = \ + [playlist for playlist in playlists if + (playlist_id_or_name in [playlist["name"], playlist["id"]])] + + if len(found_playlists) > 0: + playlist = found_playlists[0] + path = '/playlists/' + playlist['id'] + '/play' + return self._request('PUT', path) + def artwork_url(self): """ Returns a URL of the current track's album art. """ return self._base_url + '/artwork' @@ -294,6 +310,11 @@ class ItunesDevice(MediaPlayerDevice): """ Album of current playing media. (Music track only) """ return self.current_album + @property + def media_playlist(self): + """ Title of the currently playing playlist. """ + return self.current_playlist + @property def supported_media_commands(self): """ Flags of media commands that are supported. """ @@ -329,6 +350,12 @@ class ItunesDevice(MediaPlayerDevice): response = self.client.previous() self.update_state(response) + def play_media(self, media_type, media_id): + """ play_media media player. """ + if media_type == MEDIA_TYPE_PLAYLIST: + response = self.client.play_playlist(media_id) + self.update_state(response) + class AirPlayDevice(MediaPlayerDevice): """ Represents an AirPlay device via an iTunes-API instance. """ diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 7bfd385f65b..2fe42e2e707 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -167,7 +167,7 @@ class KodiDevice(MediaPlayerDevice): def media_content_id(self): """ Content ID of current playing media. """ if self._item is not None: - return self._item['uniqueid'] + return self._item.get('uniqueid', None) @property def media_content_type(self): diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 46eb8947d99..5fac9ecb0f0 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -1,34 +1,11 @@ """ homeassistant.components.media_player.plex ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides an interface to the Plex API. -Provides an interface to the Plex API - -Configuration: - -To use Plex add something like this to your configuration: - -media_player: - platform: plex - name: plex_server - user: plex - password: my_secure_password - -Variables: - -name -*Required -The name of the backend device (Under Plex Media Server > settings > server). - -user -*Required -The Plex username - -password -*Required -The Plex password +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.plex.html """ - import logging from datetime import timedelta @@ -49,10 +26,8 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK -# pylint: disable=abstract-method -# pylint: disable=unused-argument - +# pylint: disable=abstract-method, unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the plex platform. """ from plexapi.myplex import MyPlexUser @@ -68,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_devices(): - """ Updates the devices objects """ + """ Updates the devices objects. """ try: devices = plexuser.devices() except BadRequest: @@ -94,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_sessions(): - """ Updates the sessions objects """ + """ Updates the sessions objects. """ try: sessions = plexserver.sessions() except BadRequest: @@ -113,7 +88,6 @@ class PlexClient(MediaPlayerDevice): """ Represents a Plex device. """ # pylint: disable=too-many-public-methods - def __init__(self, device, plex_sessions, update_devices, update_sessions): self.plex_sessions = plex_sessions self.update_devices = update_devices @@ -121,12 +95,12 @@ class PlexClient(MediaPlayerDevice): self.set_device(device) def set_device(self, device): - """ Sets the device property """ + """ Sets the device property. """ self.device = device @property def session(self): - """ Returns the session, if any """ + """ Returns the session, if any. """ if self.device.clientIdentifier not in self.plex_sessions: return None @@ -196,21 +170,21 @@ class PlexClient(MediaPlayerDevice): @property def media_season(self): - """ Season of curent playing media. (TV Show only) """ + """ Season of curent playing media (TV Show only). """ from plexapi.video import Show if isinstance(self.session, Show): return self.session.seasons()[0].index @property def media_series_title(self): - """ Series title of current playing media. (TV Show only)""" + """ Series title of current playing media (TV Show only). """ from plexapi.video import Show if isinstance(self.session, Show): return self.session.grandparentTitle @property def media_episode(self): - """ Episode of current playing media. (TV Show only) """ + """ Episode of current playing media (TV Show only). """ from plexapi.video import Show if isinstance(self.session, Show): return self.session.index diff --git a/homeassistant/components/notify/file.py b/homeassistant/components/notify/file.py index 9c0beca14ac..3fe6d555524 100644 --- a/homeassistant/components/notify/file.py +++ b/homeassistant/components/notify/file.py @@ -3,26 +3,8 @@ homeassistant.components.notify.file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ File notification service. -Configuration: - -To use the File notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: file - filename: FILENAME - timestamp: 1 or 0 - -Variables: - -filename -*Required -Name of the file to use. The file will be created if it doesn't exist and saved -in your config/ folder. - -timestamp -*Required -Add a timestamp to the entry, valid entries are 1 or 0. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.file.html """ import logging import os diff --git a/homeassistant/components/notify/instapush.py b/homeassistant/components/notify/instapush.py index 95ff0d41435..839eac24a0d 100644 --- a/homeassistant/components/notify/instapush.py +++ b/homeassistant/components/notify/instapush.py @@ -3,52 +3,8 @@ homeassistant.components.notify.instapush ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instapush notification service. -Configuration: - -To use the Instapush notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: instapush - api_key: YOUR_APP_KEY - app_secret: YOUR_APP_SECRET - event: YOUR_EVENT - tracker: YOUR_TRACKER - -Variables: - -api_key -*Required -To retrieve this value log into your account at https://instapush.im and go -to 'APPS', choose an app, and check 'Basic Info'. - -app_secret -*Required -To get this value log into your account at https://instapush.im and go to -'APPS'. The 'Application ID' can be found under 'Basic Info'. - -event -*Required -To retrieve a valid event log into your account at https://instapush.im and go -to 'APPS'. If you have no events to use with Home Assistant, create one event -for your app. - -tracker -*Required -To retrieve the tracker value log into your account at https://instapush.im and -go to 'APPS', choose the app, and check the event entries. - -Example usage of Instapush if you have an event 'notification' and a tracker -'home-assistant'. - -curl -X POST \ - -H "x-instapush-appid: YOUR_APP_KEY" \ - -H "x-instapush-appsecret: YOUR_APP_SECRET" \ - -H "Content-Type: application/json" \ - -d '{"event":"notification","trackers":{"home-assistant":"Switch 1"}}' \ - https://api.instapush.im/v1/post - -Details for the API : https://instapush.im/developer/rest +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.instapush.html """ import logging import json diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index bf8fb2162a8..a9fa6559e71 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -3,23 +3,8 @@ homeassistant.components.notify.nma ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ NMA (Notify My Android) notification service. -Configuration: - -To use the NMA notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: nma - api_key: YOUR_API_KEY - -Variables: - -api_key -*Required -Enter the API key for NMA. Go to https://www.notifymyandroid.com and create a -new API key to use with Home Assistant. - -Details for the API : https://www.notifymyandroid.com/api.jsp +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.nma.html """ import logging import xml.etree.ElementTree as ET diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 76eaf5c0c37..49aaccd3004 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -3,21 +3,8 @@ homeassistant.components.notify.pushbullet ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PushBullet platform for notify component. -Configuration: - -To use the PushBullet notifier you will need to add something like the -following to your configuration.yaml file. - -notify: - platform: pushbullet - api_key: YOUR_API_KEY - -Variables: - -api_key -*Required -Enter the API key for PushBullet. Go to https://www.pushbullet.com/ to retrieve -your API key. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.pushbullet.html """ import logging diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index c52e430ac9f..e2b4b4c4b40 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -3,35 +3,8 @@ homeassistant.components.notify.pushover ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pushover platform for notify component. -Configuration: - -To use the Pushover notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: pushover - api_key: ABCDEFGHJKLMNOPQRSTUVXYZ - user_key: ABCDEFGHJKLMNOPQRSTUVXYZ - -Variables: - -api_key -*Required -This parameter is optional but should be configured, in order to get an API -key you should go to https://pushover.net and register a new application. - -This is a quote from the pushover website regarding free/open source apps: -"If you are creating a client-side library, application, or open source project -that will be redistributed and installed by end-users, you may want to require -each of your users to register their own application rather than including your -own API token with the software." - -When setting up the application I recommend using the icon located here: -https://home-assistant.io/images/favicon-192x192.png - -user_key -*Required -To retrieve this value log into your account at https://pushover.net +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.pushover.html """ import logging diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index bd3a2b71c0c..0b168ed5075 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -3,27 +3,8 @@ homeassistant.components.notify.slack ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Slack platform for notify component. -Configuration: - -To use the Slack notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: slack - api_key: ABCDEFGHJKLMNOPQRSTUVXYZ - default_channel: '#general' - -Variables: - -api_key -*Required -The slack API token to use for sending slack messages. -You can get your slack API token here https://api.slack.com/web?sudo=1 - -default_channel -*Required -The default channel to post to if no channel is explicitly specified when -sending the notification message. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.slack.html """ import logging diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index ee2413eda30..758e7839e50 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -1,56 +1,10 @@ """ -homeassistant.components.notify.mail +homeassistant.components.notify.smtp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Mail (SMTP) notification service. -Configuration: - -To use the Mail notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: mail - server: MAIL_SERVER - port: YOUR_SMTP_PORT - sender: SENDER_EMAIL_ADDRESS - starttls: 1 or 0 - username: YOUR_SMTP_USERNAME - password: YOUR_SMTP_PASSWORD - recipient: YOUR_RECIPIENT - -Variables: - -server -*Required -SMTP server which is used to end the notifications. For Google Mail, eg. -smtp.gmail.com. Keep in mind that Google has some extra layers of protection -which need special attention (Hint: 'Less secure apps'). - -port -*Required -The port that the SMTP server is using, eg. 587 for Google Mail and STARTTLS -or 465/993 depending on your SMTP servers. - -sender -*Required -E-Mail address of the sender. - -starttls -*Optional -Enables STARTTLS, eg. 1 or 0. - -username -*Required -Username for the SMTP account. - -password -*Required -Password for the SMTP server that belongs to the given username. If the -password contains a colon it need to be wrapped in apostrophes. - -recipient -*Required -Recipient of the notification. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.smtp.html """ import logging import smtplib diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 5d246f2fd0d..7881e68476d 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -3,31 +3,8 @@ homeassistant.components.notify.syslog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Syslog notification service. -Configuration: - -To use the Syslog notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: syslog - facility: SYSLOG_FACILITY - option: SYSLOG_LOG_OPTION - priority: SYSLOG_PRIORITY - -Variables: - -facility -*Optional -Facility according to RFC 3164 (http://tools.ietf.org/html/rfc3164). Default -is 'syslog' if no value is given. - -option -*Option -Log option. Default is 'pid' if no value is given. - -priority -*Optional -Priority of the messages. Default is 'info' if no value is given. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.syslog.html """ import logging import syslog diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py new file mode 100644 index 00000000000..23b915baf1e --- /dev/null +++ b/homeassistant/components/notify/telegram.py @@ -0,0 +1,66 @@ +""" +homeassistant.components.notify.telegram +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Telegram platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.telegram.html +""" +import logging +import urllib + +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, ATTR_TITLE, BaseNotificationService) +from homeassistant.const import CONF_API_KEY + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['python-telegram-bot==2.8.7'] + + +def get_service(hass, config): + """ Get the Telegram notification service. """ + + if not validate_config(config, + {DOMAIN: [CONF_API_KEY, 'chat_id']}, + _LOGGER): + return None + + try: + import telegram + except ImportError: + _LOGGER.exception( + "Unable to import python-telegram-bot. " + "Did you maybe not install the 'python-telegram-bot' package?") + return None + + try: + bot = telegram.Bot(token=config[DOMAIN][CONF_API_KEY]) + username = bot.getMe()['username'] + _LOGGER.info("Telegram bot is' %s'", username) + except urllib.error.HTTPError: + _LOGGER.error("Please check your access token.") + return None + + return TelegramNotificationService( + config[DOMAIN][CONF_API_KEY], + config[DOMAIN]['chat_id']) + + +# pylint: disable=too-few-public-methods +class TelegramNotificationService(BaseNotificationService): + """ Implements notification service for Telegram. """ + + def __init__(self, api_key, chat_id): + import telegram + self._api_key = api_key + self._chat_id = chat_id + self.bot = telegram.Bot(token=self._api_key) + + def send_message(self, message="", **kwargs): + """ Send a message to a user. """ + + title = kwargs.get(ATTR_TITLE) + + self.bot.sendMessage(chat_id=self._chat_id, + text=title + " " + message) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 1d72f6a262b..d4f5a7336a6 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -3,31 +3,8 @@ homeassistant.components.notify.xmpp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Jabber (XMPP) notification service. -Configuration: - -To use the Jabber notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: xmpp - sender: YOUR_JID - password: YOUR_JABBER_ACCOUNT_PASSWORD - recipient: YOUR_RECIPIENT - -Variables: - -sender -*Required -The Jabber ID (JID) that will act as origin of the messages. Add your JID -including the domain, e.g. your_name@jabber.org. - -password -*Required -The password for your given Jabber account. - -recipient -*Required -The Jabber ID (JID) that will receive the messages. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.xmpp.html """ import logging diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py new file mode 100644 index 00000000000..0788986c91d --- /dev/null +++ b/homeassistant/components/rfxtrx.py @@ -0,0 +1,89 @@ +""" +homeassistant.components.rfxtrx +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides support for RFXtrx components. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rfxtrx.html +""" +import logging +from homeassistant.util import slugify + +DEPENDENCIES = [] +REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip' + + '#RFXtrx==0.2'] + +DOMAIN = "rfxtrx" +CONF_DEVICE = 'device' +CONF_DEBUG = 'debug' +RECEIVED_EVT_SUBSCRIBERS = [] +RFX_DEVICES = {} +_LOGGER = logging.getLogger(__name__) +RFXOBJECT = None + + +def setup(hass, config): + """ Setup the RFXtrx component. """ + + # Declare the Handle event + def handle_receive(event): + """ Callback all subscribers for RFXtrx gateway. """ + + # Log RFXCOM event + entity_id = slugify(event.device.id_string.lower()) + packet_id = "".join("{0:02x}".format(x) for x in event.data) + entity_name = "%s : %s" % (entity_id, packet_id) + _LOGGER.info("Receive RFXCOM event from %s => %s", + event.device, entity_name) + + # Callback to HA registered components + for subscriber in RECEIVED_EVT_SUBSCRIBERS: + subscriber(event) + + # Try to load the RFXtrx module + try: + import RFXtrx as rfxtrxmod + except ImportError: + _LOGGER.exception("Failed to import rfxtrx") + return False + + # Init the rfxtrx module + global RFXOBJECT + + if CONF_DEVICE not in config[DOMAIN]: + _LOGGER.exception( + "can found device parameter in %s YAML configuration section", + DOMAIN + ) + return False + + device = config[DOMAIN][CONF_DEVICE] + debug = config[DOMAIN].get(CONF_DEBUG, False) + + RFXOBJECT = rfxtrxmod.Core(device, handle_receive, debug=debug) + + return True + + +def get_rfx_object(packetid): + """ Return the RFXObject with the packetid. """ + try: + import RFXtrx as rfxtrxmod + except ImportError: + _LOGGER.exception("Failed to import rfxtrx") + return False + + binarypacket = bytearray.fromhex(packetid) + + pkt = rfxtrxmod.lowlevel.parse(binarypacket) + if pkt is not None: + if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket): + obj = rfxtrxmod.SensorEvent(pkt) + elif isinstance(pkt, rfxtrxmod.lowlevel.Status): + obj = rfxtrxmod.StatusEvent(pkt) + else: + obj = rfxtrxmod.ControlEvent(pkt) + + return obj + + return None diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 4a85adefd17..66c15f8272f 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -6,34 +6,34 @@ Allows users to set and activate scenes within Home Assistant. A scene is a set of states that describe how you want certain entities to be. For example, light A should be red with 100 brightness. Light B should be on. - -A scene is active if all states of the scene match the real states. - -If a scene is manually activated it will store the previous state of the -entities. These will be restored when the state is deactivated manually. - -If one of the enties that are being tracked change state on its own, the -old state will not be restored when it is being deactivated. """ import logging from collections import namedtuple from homeassistant.core import State -from homeassistant.helpers.event import track_state_change -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import reproduce_state from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON) DOMAIN = 'scene' DEPENDENCIES = ['group'] - -ATTR_ACTIVE_REQUESTED = "active_requested" +STATE = 'scening' CONF_ENTITIES = "entities" -SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match']) +SceneConfig = namedtuple('SceneConfig', ['name', 'states']) + + +def activate(hass, entity_id=None): + """ Activate a scene. """ + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def setup(hass, config): @@ -43,8 +43,9 @@ def setup(hass, config): scene_configs = config.get(DOMAIN) - if not isinstance(scene_configs, list): - logger.error('Scene config should be a list of scenes') + if not isinstance(scene_configs, list) or \ + any(not isinstance(item, dict) for item in scene_configs): + logger.error('Scene config should be a list of dictionaries') return False component = EntityComponent(logger, DOMAIN, hass) @@ -57,12 +58,8 @@ def setup(hass, config): target_scenes = component.extract_from_service(service) for scene in target_scenes: - if service.service == SERVICE_TURN_ON: - scene.turn_on() - else: - scene.turn_off() + scene.activate() - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_scene_service) hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_scene_service) return True @@ -72,14 +69,6 @@ def _process_config(scene_config): """ Process passed in config into a format to work with. """ name = scene_config.get('name') - fuzzy_match = scene_config.get('fuzzy_match') - if fuzzy_match: - # default to 1% - if isinstance(fuzzy_match, int): - fuzzy_match /= 100.0 - else: - fuzzy_match = 0.01 - states = {} c_entities = dict(scene_config.get(CONF_ENTITIES, {})) @@ -100,23 +89,16 @@ def _process_config(scene_config): states[entity_id.lower()] = State(entity_id, state, attributes) - return SceneConfig(name, states, fuzzy_match) + return SceneConfig(name, states) -class Scene(ToggleEntity): +class Scene(Entity): """ A scene is a group of entities and the states we want them to be. """ def __init__(self, hass, scene_config): self.hass = hass self.scene_config = scene_config - self.is_active = False - self.prev_states = None - self.ignore_updates = False - - track_state_change( - self.hass, self.entity_ids, self.entity_state_changed) - self.update() @property @@ -128,8 +110,8 @@ class Scene(ToggleEntity): return self.scene_config.name @property - def is_on(self): - return self.is_active + def state(self): + return STATE @property def entity_ids(self): @@ -141,82 +123,8 @@ class Scene(ToggleEntity): """ Scene state attributes. """ return { ATTR_ENTITY_ID: list(self.entity_ids), - ATTR_ACTIVE_REQUESTED: self.prev_states is not None, } - def turn_on(self): + def activate(self): """ Activates scene. Tries to get entities into requested state. """ - self.prev_states = tuple(self.hass.states.get(entity_id) - for entity_id in self.entity_ids) - - self._reproduce_state(self.scene_config.states.values()) - - def turn_off(self): - """ Deactivates scene and restores old states. """ - if self.prev_states: - self._reproduce_state(self.prev_states) - self.prev_states = None - - def entity_state_changed(self, entity_id, old_state, new_state): - """ Called when an entity part of this scene changes state. """ - if self.ignore_updates: - return - - # If new state is not what we expect, it can never be active - if self._state_as_requested(new_state): - self.update() - else: - self.is_active = False - self.prev_states = None - - self.update_ha_state() - - def update(self): - """ - Update if the scene is active. - - Will look at each requested state and see if the current entity - has the same state and has at least the same attributes with the - same values. The real state can have more attributes. - """ - self.is_active = all( - self._state_as_requested(self.hass.states.get(entity_id)) - for entity_id in self.entity_ids) - - def _state_as_requested(self, cur_state): - """ Returns if given state is as requested. """ - state = self.scene_config.states.get(cur_state and cur_state.entity_id) - - return (cur_state is not None and state.state == cur_state.state and - all(self._compare_state_attribites( - value, cur_state.attributes.get(key)) - for key, value in state.attributes.items())) - - def _fuzzy_attribute_compare(self, attr_a, attr_b): - """ - Compare the attributes passed, use fuzzy logic if they are floats. - """ - - if not (isinstance(attr_a, float) and isinstance(attr_b, float)): - return False - diff = abs(attr_a - attr_b) / (abs(attr_a) + abs(attr_b)) - return diff <= self.scene_config.fuzzy_match - - def _compare_state_attribites(self, attr1, attr2): - """ Compare the attributes passed, using fuzzy logic if specified. """ - if attr1 == attr2: - return True - if not self.scene_config.fuzzy_match: - return False - if isinstance(attr1, list): - return all(self._fuzzy_attribute_compare(a, b) - for a, b in zip(attr1, attr2)) - return self._fuzzy_attribute_compare(attr1, attr2) - - def _reproduce_state(self, states): - """ Wraps reproduce state with Scence specific logic. """ - self.ignore_updates = True - reproduce_state(self.hass, states, True) - self.ignore_updates = False - - self.update_ha_state(True) + reproduce_state(self.hass, self.scene_config.states.values(), True) diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index cfe88e0f0d6..6a11aa7189c 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -3,51 +3,11 @@ homeassistant.components.sensor.arest ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The arest sensor will consume an exposed aREST API of a device. -Configuration: - -To use the arest sensor you will need to add something like the following -to your configuration.yaml file. - -sensor: - platform: arest - resource: http://IP_ADDRESS - monitored_variables: - - name: temperature - unit: '°C' - - name: humidity - unit: '%' - -Variables: - -resource: -*Required -IP address of the device that is exposing an aREST API. - -These are the variables for the monitored_variables array: - -name -*Required -The name of the variable you wish to monitor. - -unit -*Optional -Defines the units of measurement of the sensor, if any. - -Details for the API: http://arest.io - -Format of a default JSON response by aREST: -{ - "variables":{ - "temperature":21, - "humidity":89 - }, - "id":"device008", - "name":"Bedroom", - "connected":true -} +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.arest.html """ import logging -from requests import get, exceptions +import requests from datetime import timedelta from homeassistant.util import Throttle @@ -58,36 +18,42 @@ _LOGGER = logging.getLogger(__name__) # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +CONF_RESOURCE = 'resource' +CONF_MONITORED_VARIABLES = 'monitored_variables' + def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the aREST sensor. """ - resource = config.get('resource', None) + resource = config.get(CONF_RESOURCE) + var_conf = config.get(CONF_MONITORED_VARIABLES) + + if None in (resource, var_conf): + _LOGGER.error('Not all required config keys present: %s', + ', '.join((CONF_RESOURCE, CONF_MONITORED_VARIABLES))) + return False try: - response = get(resource, timeout=10) - except exceptions.MissingSchema: + response = requests.get(resource, timeout=10).json() + except requests.exceptions.MissingSchema: _LOGGER.error("Missing resource or schema in configuration. " "Add http:// to your URL.") return False - except exceptions.ConnectionError: + except requests.exceptions.ConnectionError: _LOGGER.error("No route to device. " "Please check the IP address in the configuration file.") return False - rest = ArestData(resource) + arest = ArestData(resource) dev = [] for variable in config['monitored_variables']: - if 'unit' not in variable: - variable['unit'] = ' ' - if variable['name'] not in response.json()['variables']: + if variable['name'] not in response['variables']: _LOGGER.error('Variable: "%s" does not exist', variable['name']) - else: - dev.append(ArestSensor(rest, - response.json()['name'], - variable['name'], - variable['unit'])) + continue + + dev.append(ArestSensor(arest, response['name'], variable['name'], + variable.get('unit'))) add_devices(dev) @@ -95,8 +61,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ArestSensor(Entity): """ Implements an aREST sensor. """ - def __init__(self, rest, location, variable, unit_of_measurement): - self.rest = rest + def __init__(self, arest, location, variable, unit_of_measurement): + self.arest = arest self._name = '{} {}'.format(location.title(), variable.title()) self._variable = variable self._state = 'n/a' @@ -116,17 +82,16 @@ class ArestSensor(Entity): @property def state(self): """ Returns the state of the device. """ - return self._state - - def update(self): - """ Gets the latest data from aREST API and updates the state. """ - self.rest.update() - values = self.rest.data + values = self.arest.data if 'error' in values: - self._state = values['error'] + return values['error'] else: - self._state = values[self._variable] + return values.get(self._variable, 'n/a') + + def update(self): + """ Gets the latest data from aREST API. """ + self.arest.update() # pylint: disable=too-few-public-methods @@ -135,16 +100,14 @@ class ArestData(object): def __init__(self, resource): self.resource = resource - self.data = dict() + self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """ Gets the latest data from aREST device. """ try: - response = get(self.resource, timeout=10) - if 'error' in self.data: - del self.data['error'] + response = requests.get(self.resource, timeout=10) self.data = response.json()['variables'] - except exceptions.ConnectionError: + except requests.exceptions.ConnectionError: _LOGGER.error("No route to device. Is device offline?") - self.data['error'] = 'n/a' + self.data = {'error': 'error fetching'} diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 60a8a9172b7..803b9dbb9d4 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -3,65 +3,8 @@ homeassistant.components.sensor.bitcoin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Bitcoin information service that uses blockchain.info and its online wallet. -Configuration: - -You need to enable the API access for your online wallet to get the balance. -To do that log in and move to 'Account Setting', choose 'IP Restrictions', and -check 'Enable Api Access'. You will get an email message from blockchain.info -where you must authorize the API access. - -To use the Bitcoin sensor you will need to add something like the following -to your configuration.yaml file. - -sensor: - platform: bitcoin - wallet: 'YOUR WALLET_ID' - password: YOUR_ACCOUNT_PASSWORD - currency: YOUR CURRENCY - display_options: - - exchangerate - - trade_volume_btc - - miners_revenue_usd - - btc_mined - - trade_volume_usd - - difficulty - - minutes_between_blocks - - number_of_transactions - - hash_rate - - timestamp - - mined_blocks - - blocks_size - - total_fees_btc - - total_btc_sent - - estimated_btc_sent - - total_btc - - total_blocks - - next_retarget - - estimated_transaction_volume_usd - - miners_revenue_btc - - market_price_usd - -Variables: - -wallet -*Optional -This is your wallet identifier from https://blockchain.info to access the -online wallet. - -password -*Optional -Password your your online wallet. - -currency -*Optional -The currency to exchange to, eg. CHF, USD, EUR,etc. Default is USD. - -display_options -*Optional -An array specifying the variables to display. - -These are the variables for the display_options array. See the configuration -example above for a list of all available variables. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.bitcoin.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py new file mode 100644 index 00000000000..7f03e21c7a8 --- /dev/null +++ b/homeassistant/components/sensor/cpuspeed.py @@ -0,0 +1,76 @@ +""" +homeassistant.components.sensor.cpuspeed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shows the current CPU speed. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.cpuspeed.html +""" +import logging + +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['py-cpuinfo==0.1.6'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "CPU speed" + +ATTR_VENDOR = 'Vendor ID' +ATTR_BRAND = 'Brand' +ATTR_HZ = 'GHz Advertised' + + +# pylint: disable=unused-variable +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the CPU speed sensor. """ + + try: + import cpuinfo # noqa + except ImportError: + _LOGGER.exception( + "Unable to import cpuinfo. " + "Did you maybe not install the 'py-cpuinfo' package?") + return False + + add_devices([CpuSpeedSensor(config.get('name', DEFAULT_NAME))]) + + +class CpuSpeedSensor(Entity): + """ A CPU info sensor. """ + + def __init__(self, name): + self._name = name + self._state = None + self._unit_of_measurement = 'GHz' + self.update() + + @property + def name(self): + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + return self._unit_of_measurement + + @property + def state_attributes(self): + """ Returns the state attributes. """ + if self.info is not None: + return { + ATTR_VENDOR: self.info['vendor_id'], + ATTR_BRAND: self.info['brand'], + ATTR_HZ: round(self.info['hz_advertised_raw'][0]/10**9, 2) + } + + def update(self): + """ Gets the latest data and updates the state. """ + from cpuinfo import cpuinfo + + self.info = cpuinfo.get_cpu_info() + self._state = round(float(self.info['hz_actual_raw'][0])/10**9, 2) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 60e84059cad..84c62b26469 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -17,6 +17,26 @@ Variables: port *Required Port of your connection to your MySensors device. + +debug +*Optional +Enable or disable verbose debug logging. + +persistence +*Optional +Enable or disable local persistence of sensor information. +Note: If this is disabled, then each sensor will need to send presentation + messages after Home Assistant starts + +persistence_file +*Optional +Path to a file to save sensor information. +Note: The file extension determines the file type. Currently supported file + types are 'pickle' and 'json'. + +version +*Optional +Specifies the MySensors protocol version to use (ex. 1.4, 1.5). """ import logging @@ -30,14 +50,16 @@ from homeassistant.const import ( CONF_PORT = "port" CONF_DEBUG = "debug" CONF_PERSISTENCE = "persistence" +CONF_PERSISTENCE_FILE = "persistence_file" +CONF_VERSION = "version" ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' - '35b87d880147a34107da0d40cb815d75e6cb4af7.zip' - '#pymysensors==0.2'] + 'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip' + '#pymysensors==0.3'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -86,9 +108,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False persistence = config.get(CONF_PERSISTENCE, True) + persistence_file = config.get(CONF_PERSISTENCE_FILE, 'mysensors.pickle') + version = config.get(CONF_VERSION, '1.4') gateway = mysensors.SerialGateway(port, sensor_update, - persistence=persistence) + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) gateway.metric = is_metric gateway.debug = config.get(CONF_DEBUG, False) gateway.start() diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 4cb8a939d5e..07912b719d2 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -3,30 +3,19 @@ homeassistant.components.sensor.rfxtrx ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shows sensor values from RFXtrx sensors. -Configuration: - -To use the rfxtrx sensors you will need to add something like the following to -your configuration.yaml file. - -sensor: - platform: rfxtrx - device: PATH_TO_DEVICE - -Variables: - -device -*Required -Path to your RFXtrx device. -E.g. /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0 +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rfxtrx.html """ import logging from collections import OrderedDict from homeassistant.const import (TEMP_CELCIUS) from homeassistant.helpers.entity import Entity +import homeassistant.components.rfxtrx as rfxtrx +from RFXtrx import SensorEvent +from homeassistant.util import slugify -REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/' + - 'ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15'] +DEPENDENCIES = ['rfxtrx'] DATA_TYPES = OrderedDict([ ('Temperature', TEMP_CELCIUS), @@ -34,32 +23,30 @@ DATA_TYPES = OrderedDict([ ('Barometer', ''), ('Wind direction', ''), ('Rain rate', '')]) +_LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup the RFXtrx platform. """ - logger = logging.getLogger(__name__) - - sensors = {} # keep track of sensors added to HA def sensor_update(event): """ Callback for sensor updates from the RFXtrx gateway. """ - if event.device.id_string in sensors: - sensors[event.device.id_string].event = event - else: - logger.info("adding new sensor: %s", event.device.type_string) - new_sensor = RfxtrxSensor(event) - sensors[event.device.id_string] = new_sensor - add_devices([new_sensor]) - try: - import RFXtrx as rfxtrx - except ImportError: - logger.exception( - "Failed to import rfxtrx") - return False + if isinstance(event.device, SensorEvent): + entity_id = slugify(event.device.id_string.lower()) - device = config.get("device", "") - rfxtrx.Core(device, sensor_update) + # Add entity if not exist and the automatic_add is True + if entity_id not in rfxtrx.RFX_DEVICES: + automatic_add = config.get('automatic_add', True) + if automatic_add: + _LOGGER.info("Automatic add %s rfxtrx.sensor", entity_id) + new_sensor = RfxtrxSensor(event) + rfxtrx.RFX_DEVICES[entity_id] = new_sensor + add_devices_callback([new_sensor]) + else: + rfxtrx.RFX_DEVICES[entity_id].event = event + + if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) class RfxtrxSensor(Entity): @@ -67,7 +54,6 @@ class RfxtrxSensor(Entity): def __init__(self, event): self.event = event - self._unit_of_measurement = None self._data_type = None for data_type in DATA_TYPES: @@ -86,13 +72,14 @@ class RfxtrxSensor(Entity): @property def state(self): + """ Returns the state of the device. """ if self._data_type: return self.event.values[self._data_type] return None @property def name(self): - """ Get the mame of the sensor. """ + """ Get the name of the sensor. """ return self._name @property diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index b372e777478..7aeb14b27f5 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -1,47 +1,10 @@ """ homeassistant.components.sensor.sabnzbd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Monitors SABnzbd NZB client API +Monitors SABnzbd NZB client API. -Configuration: - -To use the SABnzbd sensor you will need to add something like the following to -your configuration.yaml file. - -sensor: - platform: sabnzbd - name: SAB - api_key: YOUR_API_KEY - base_url: YOUR_SABNZBD_BASE_URL - monitored_variables: - - type: 'current_status' - - type: 'speed' - - type: 'queue_size' - - type: 'queue_remaining' - - type: 'disk_size' - - type: 'disk_free' - -Variables: - -base_url -*Required -This is the base URL of your SABnzbd instance including the port number if not -running on 80, e.g. http://192.168.1.32:8124/ - -name -*Optional -The name to use when displaying this SABnzbd instance. - -monitored_variables -*Required -An array specifying the variables to monitor. - -These are the variables for the monitored_variables array: - -type -*Required -The variable you wish to monitor, see the configuration example above for a -list of all available variables. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.sabnzbd.html """ from homeassistant.util import Throttle from datetime import timedelta diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 82f62da10e0..1fee6a6328c 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -3,60 +3,8 @@ homeassistant.components.sensor.systemmonitor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shows system monitor values such as: disk, memory, and processor use. -Configuration: - -To use the System monitor sensor you will need to add something like the -following to your configuration.yaml file. - -sensor: - platform: systemmonitor - resources: - - type: 'disk_use_percent' - arg: '/' - - type: 'disk_use' - arg: '/home' - - type: 'disk_free' - arg: '/' - - type: 'memory_use_percent' - - type: 'memory_use' - - type: 'memory_free' - - type: 'swap_use_percent' - - type: 'swap_use' - - type: 'swap_free' - - type: 'network_in' - arg: 'eth0' - - type: 'network_out' - arg: 'eth0' - - type: 'packets_in' - arg: 'eth0' - - type: 'packets_out' - arg: 'eth0' - - type: 'ipv4_address' - arg: 'eth0' - - type: 'ipv6_address' - arg: 'eth0' - - type: 'processor_use' - - type: 'process' - arg: 'octave-cli' - - type: 'last_boot' - - type: 'since_last_boot' - -Variables: - -resources -*Required -An array specifying the variables to monitor. - -These are the variables for the resources array: - -type -*Required -The variable you wish to monitor, see the configuration example above for a -sample list of variables. - -arg -*Optional -Additional details for the type, eg. path, binary name, etc. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.systemmonitor.html """ import logging @@ -64,7 +12,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_ON, STATE_OFF -REQUIREMENTS = ['psutil==3.0.0'] +REQUIREMENTS = ['psutil==3.2.2'] SENSOR_TYPES = { 'disk_use_percent': ['Disk Use', '%'], 'disk_use': ['Disk Use', 'GiB'], diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py new file mode 100644 index 00000000000..c5c60e98e7d --- /dev/null +++ b/homeassistant/components/shell_command.py @@ -0,0 +1,46 @@ +""" +homeassistant.components.shell_command +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Exposes regular shell commands as services. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/shell_command.html +""" +import logging +import subprocess + +from homeassistant.util import slugify + +DOMAIN = 'shell_command' +DEPENDENCIES = [] + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """ Sets up the shell_command component. """ + conf = config.get(DOMAIN) + + if not isinstance(conf, dict): + _LOGGER.error('Expected configuration to be a dictionary') + return False + + for name in conf.keys(): + if name != slugify(name): + _LOGGER.error('Invalid service name: %s. Try %s', + name, slugify(name)) + return False + + def service_handler(call): + """ Execute a shell command service. """ + try: + subprocess.call(conf[call.service].split(' '), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + except subprocess.SubprocessError: + _LOGGER.exception('Error running command') + + for name in conf.keys(): + hass.services.register(DOMAIN, name, service_handler) + + return True diff --git a/homeassistant/components/simple_alarm.py b/homeassistant/components/simple_alarm.py index 46cb52fa950..9eb9e542cf4 100644 --- a/homeassistant/components/simple_alarm.py +++ b/homeassistant/components/simple_alarm.py @@ -1,10 +1,10 @@ """ homeassistant.components.simple_alarm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Intruder alerts component. -Provides a simple alarm feature: - - flash a light when a known device comes home - - flash a light red if a light turns on while there is no one home. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/simple_alarm.html """ import logging @@ -17,7 +17,7 @@ DOMAIN = "simple_alarm" DEPENDENCIES = ['group', 'device_tracker', 'light'] # Attribute to tell which light has to flash whem a known person comes home -# If ommitted will flash all. +# If omitted will flash all. CONF_KNOWN_LIGHT = "known_light" # Attribute to tell which light has to flash whem an unknown person comes home diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index ce4dbd1e937..7765f1b7276 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -1,23 +1,10 @@ """ homeassistant.components.sun ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to keep track of the sun. -Event listener --------------- -The suns event listener will call the service when the sun rises or sets with -an offset. - -The sun event need to have the type 'sun', which service to call, which event -(sunset or sunrise) and the offset. - -{ - "type": "sun", - "service": "switch.turn_on", - "event": "sunset", - "offset": "-01:00:00" -} +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sun.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 239e24a4925..ec04e3c210f 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -3,38 +3,9 @@ homeassistant.components.switch.arest ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The arest switch can control the digital pins of a device running with the aREST RESTful framework for Arduino, the ESP8266, and the Raspberry Pi. -Only tested with Arduino boards so far. -Configuration: - -To use the arest switch you will need to add something like the following -to your configuration.yaml file. - -sensor: - platform: arest - resource: http://IP_ADDRESS - pins: - 11: - name: Fan Office - 12: - name: Light Desk - -Variables: - -resource: -*Required -IP address of the device that is exposing an aREST API. - -pins: -The number of the digital pin to switch. - -These are the variables for the pins array: - -name -*Required -The name for the pin that will be used in the frontend. - -Details for the API: http://arest.io +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.arest.html """ import logging from requests import get, exceptions diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py new file mode 100644 index 00000000000..49a788d0d04 --- /dev/null +++ b/homeassistant/components/switch/rfxtrx.py @@ -0,0 +1,112 @@ +""" +homeassistant.components.switch.rfxtrx +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for RFXtrx switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.rfxtrx.html +""" +import logging +import homeassistant.components.rfxtrx as rfxtrx +from RFXtrx import LightingDevice + +from homeassistant.components.switch import SwitchDevice +from homeassistant.util import slugify + +DEPENDENCIES = ['rfxtrx'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Setup the RFXtrx platform. """ + + # Add switch from config file + switchs = [] + devices = config.get('devices') + if devices: + for entity_id, entity_info in devices.items(): + if entity_id not in rfxtrx.RFX_DEVICES: + _LOGGER.info("Add %s rfxtrx.switch", entity_info['name']) + rfxobject = rfxtrx.get_rfx_object(entity_info['packetid']) + newswitch = RfxtrxSwitch(entity_info['name'], rfxobject, False) + rfxtrx.RFX_DEVICES[entity_id] = newswitch + switchs.append(newswitch) + + add_devices_callback(switchs) + + def switch_update(event): + """ Callback for sensor updates from the RFXtrx gateway. """ + if isinstance(event.device, LightingDevice): + return + + # Add entity if not exist and the automatic_add is True + entity_id = slugify(event.device.id_string.lower()) + if entity_id not in rfxtrx.RFX_DEVICES: + automatic_add = config.get('automatic_add', False) + if not automatic_add: + return + + _LOGGER.info( + "Automatic add %s rfxtrx.switch (Class: %s Sub: %s)", + entity_id, + event.device.__class__.__name__, + event.device.subtype + ) + pkt_id = "".join("{0:02x}".format(x) for x in event.data) + entity_name = "%s : %s" % (entity_id, pkt_id) + new_switch = RfxtrxSwitch(entity_name, event, False) + rfxtrx.RFX_DEVICES[entity_id] = new_switch + add_devices_callback([new_switch]) + + # Check if entity exists or previously added automatically + if entity_id in rfxtrx.RFX_DEVICES: + if event.values['Command'] == 'On'\ + or event.values['Command'] == 'Off': + if event.values['Command'] == 'On': + rfxtrx.RFX_DEVICES[entity_id].turn_on() + else: + rfxtrx.RFX_DEVICES[entity_id].turn_off() + + # Subscribe to main rfxtrx events + if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: + rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update) + + +class RfxtrxSwitch(SwitchDevice): + """ Provides a RFXtrx switch. """ + def __init__(self, name, event, state): + self._name = name + self._event = event + self._state = state + + @property + def should_poll(self): + """ No polling needed for a RFXtrx switch. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def is_on(self): + """ True if device is on. """ + return self._state + + def turn_on(self, **kwargs): + """ Turn the device on. """ + if self._event: + self._event.device.send_on(rfxtrx.RFXOBJECT.transport) + + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + if self._event: + self._event.device.send_off(rfxtrx.RFXOBJECT.transport) + + self._state = False + self.update_ha_state() diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 7d26c78f0f8..b021ec86c35 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -136,22 +136,16 @@ class ThermostatDevice(Entity): def state_attributes(self): """ Returns optional state attributes. """ - thermostat_unit = self.unit_of_measurement - user_unit = self.hass.config.temperature_unit - data = { - ATTR_CURRENT_TEMPERATURE: round(convert( - self.current_temperature, thermostat_unit, user_unit), 1), - ATTR_MIN_TEMP: round(convert( - self.min_temp, thermostat_unit, user_unit), 0), - ATTR_MAX_TEMP: round(convert( - self.max_temp, thermostat_unit, user_unit), 0), - ATTR_TEMPERATURE: round(convert( - self.target_temperature, thermostat_unit, user_unit), 0), - ATTR_TEMPERATURE_LOW: round(convert( - self.target_temperature_low, thermostat_unit, user_unit), 0), - ATTR_TEMPERATURE_HIGH: round(convert( - self.target_temperature_high, thermostat_unit, user_unit), 0), + ATTR_CURRENT_TEMPERATURE: + self._convert(self.current_temperature, 1), + ATTR_MIN_TEMP: self._convert(self.min_temp, 0), + ATTR_MAX_TEMP: self._convert(self.max_temp, 0), + ATTR_TEMPERATURE: self._convert(self.target_temperature, 0), + ATTR_TEMPERATURE_LOW: + self._convert(self.target_temperature_low, 0), + ATTR_TEMPERATURE_HIGH: + self._convert(self.target_temperature_high, 0), } operation = self.operation @@ -228,3 +222,14 @@ class ThermostatDevice(Entity): def max_temp(self): """ Return maxmum temperature. """ return convert(35, TEMP_CELCIUS, self.unit_of_measurement) + + def _convert(self, temp, round_dec=None): + """ Convert temperature from this thermost into user preferred + temperature. """ + if temp is None: + return None + + value = convert(temp, self.unit_of_measurement, + self.hass.config.temperature_unit) + + return value if round_dec is None else round(value, round_dec) diff --git a/homeassistant/const.py b/homeassistant/const.py index c256aa921d1..4d5f68e24c5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.7.4dev0" +__version__ = "0.7.6.dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' @@ -51,6 +51,8 @@ STATE_STANDBY = 'standby' STATE_ALARM_DISARMED = 'disarmed' STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' +STATE_ALARM_PENDING = 'pending' +STATE_ALARM_TRIGGERED = 'triggered' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -128,6 +130,7 @@ SERVICE_MEDIA_SEEK = "media_seek" SERVICE_ALARM_DISARM = "alarm_disarm" SERVICE_ALARM_ARM_HOME = "alarm_arm_home" SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" +SERVICE_ALARM_TRIGGER = "alarm_trigger" # #### API / REMOTE #### SERVER_PORT = 8123 @@ -154,6 +157,7 @@ HTTP_UNAUTHORIZED = 401 HTTP_NOT_FOUND = 404 HTTP_METHOD_NOT_ALLOWED = 405 HTTP_UNPROCESSABLE_ENTITY = 422 +HTTP_INTERNAL_SERVER_ERROR = 500 HTTP_HEADER_HA_AUTH = "X-HA-access" HTTP_HEADER_ACCEPT_ENCODING = "Accept-Encoding" diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 909b86a67ed..24a37c5b5ea 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -13,6 +13,8 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, STATE_PLAYING, STATE_PAUSED, ATTR_ENTITY_ID) +from homeassistant.components.media_player import (SERVICE_PLAY_MEDIA) + _LOGGER = logging.getLogger(__name__) @@ -57,7 +59,11 @@ def reproduce_state(hass, states, blocking=False): state.entity_id) continue - if state.domain == 'media_player' and state.state == STATE_PAUSED: + if state.domain == 'media_player' and state.attributes and \ + 'media_type' in state.attributes and \ + 'media_id' in state.attributes: + service = SERVICE_PLAY_MEDIA + elif state.domain == 'media_player' and state.state == STATE_PAUSED: service = SERVICE_MEDIA_PAUSE elif state.domain == 'media_player' and state.state == STATE_PLAYING: service = SERVICE_MEDIA_PLAY diff --git a/homeassistant/startup/__init__.py b/homeassistant/startup/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 805937376a0..ada6d150188 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -233,35 +233,55 @@ class Throttle(object): self.limit_no_throttle = limit_no_throttle def __call__(self, method): - lock = threading.Lock() - if self.limit_no_throttle is not None: method = Throttle(self.limit_no_throttle)(method) + # Different methods that can be passed in: + # - a function + # - an unbound function on a class + # - a method (bound function on a class) + + # We want to be able to differentiate between function and unbound + # methods (which are considered functions). + # All methods have the classname in their qualname seperated by a '.' + # Functions have a '.' in their qualname if defined inline, but will + # be prefixed by '..' so we strip that out. + is_func = (not hasattr(method, '__self__') and + '.' not in method.__qualname__.split('..')[-1]) + @wraps(method) def wrapper(*args, **kwargs): """ Wrapper that allows wrapped to be called only once per min_time. If we cannot acquire the lock, it is running so return None. """ - if not lock.acquire(False): + # pylint: disable=protected-access + if hasattr(method, '__self__'): + host = method.__self__ + elif is_func: + host = wrapper + else: + host = args[0] if args else wrapper + + if not hasattr(host, '_throttle_lock'): + host._throttle_lock = threading.Lock() + + if not host._throttle_lock.acquire(False): return None + + last_call = getattr(host, '_throttle_last_call', None) + # Check if method is never called or no_throttle is given + force = not last_call or kwargs.pop('no_throttle', False) + try: - last_call = wrapper.last_call - - # Check if method is never called or no_throttle is given - force = not last_call or kwargs.pop('no_throttle', False) - if force or utcnow() - last_call > self.min_time: result = method(*args, **kwargs) - wrapper.last_call = utcnow() + host._throttle_last_call = utcnow() return result else: return None finally: - lock.release() - - wrapper.last_call = None + host._throttle_lock.release() return wrapper diff --git a/requirements_all.txt b/requirements_all.txt index 668cedf65b2..dae1f319a23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,78 +10,78 @@ vincenty==0.1.2 # Sun (sun) astral==0.8.1 -# Philips Hue library (lights.hue) +# Philips Hue (lights.hue) phue==0.8 -# Limitlessled/Easybulb/Milight library (lights.limitlessled) +# Limitlessled/Easybulb/Milight (lights.limitlessled) ledcontroller==1.1.0 -# Chromecast bindings (media_player.cast) +# Chromecast (media_player.cast) pychromecast==0.6.12 # Keyboard (keyboard) pyuserinput==0.1.9 -# Tellstick bindings (*.tellstick) +# Tellstick (*.tellstick) tellcore-py==1.1.2 -# Nmap bindings (device_tracker.nmap) +# Nmap (device_tracker.nmap) python-nmap==0.4.3 -# PushBullet bindings (notify.pushbullet) +# PushBullet (notify.pushbullet) pushbullet.py==0.7.1 -# Nest Thermostat bindings (thermostat.nest) +# Nest Thermostat (thermostat.nest) python-nest==2.6.0 # Z-Wave (*.zwave) pydispatcher==2.0.5 -# ISY994 bindings (*.isy994) +# ISY994 (isy994) PyISY==1.0.5 # PSutil (sensor.systemmonitor) -psutil==3.0.0 +psutil==3.2.2 -# Pushover bindings (notify.pushover) +# Pushover (notify.pushover) python-pushover==0.2 # Transmission Torrent Client (*.transmission) transmissionrpc==0.11 -# OpenWeatherMap Web API (sensor.openweathermap) +# OpenWeatherMap (sensor.openweathermap) pyowm==2.2.1 -# XMPP Bindings (notify.xmpp) +# XMPP (notify.xmpp) sleekxmpp==1.3.1 dnspython3==1.12.0 # Blockchain (sensor.bitcoin) blockchain==1.1.2 -# MPD Bindings (media_player.mpd) +# Music Player Daemon (media_player.mpd) python-mpd2==0.5.4 # Hikvision (switch.hikvisioncam) hikvision==0.4 -# console log coloring +# Console log coloring colorlog==2.6.0 # JSON-RPC interface (media_player.kodi) jsonrpc-requests==0.1 -# Forecast.io Bindings (sensor.forecast) +# Forecast.io (sensor.forecast) python-forecastio==1.3.3 -# Firmata Bindings (*.arduino) +# Firmata (*.arduino) PyMata==2.07a -# Rfxtrx sensor (sensor.rfxtrx) +# Rfxtrx (rfxtrx) https://github.com/Danielhiversen/pyRFXtrx/archive/ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15 -# Mysensors -https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75e6cb4af7.zip#pymysensors==0.2 +# Mysensors (sensor.mysensors) +https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 # Netgear (device_tracker.netgear) pynetgear==0.3 @@ -101,18 +101,18 @@ slacker==0.6.8 # Temper sensors (sensor.temper) https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3 -# PyEdimax +# PyEdimax (switch.edimax) https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 # RPI-GPIO platform (*.rpi_gpio) # Uncomment for Raspberry Pi # RPi.GPIO==0.5.11 -# Adafruit temperature/humidity sensor -# uncomment on a Raspberry Pi / Beaglebone +# Adafruit temperature/humidity sensor (sensor.dht) +# Uncomment on a Raspberry Pi / Beaglebone # http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0 -# PAHO MQTT Binding (mqtt) +# PAHO MQTT (mqtt) paho-mqtt==1.1 # PyModbus (modbus) @@ -121,22 +121,32 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # Verisure (verisure) https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 -# Python tools for interacting with IFTTT Maker Channel (ifttt) +# IFTTT Maker Channel (ifttt) pyfttt==0.3 -# sensor.sabnzbd +# SABnzbd (sensor.sabnzbd) https://github.com/balloob/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 -# switch.vera -# sensor.vera -# light.vera +# Vera (*.vera) https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip#python-vera==0.1 -# Sonos bindings (media_player.sonos) +# Sonos (media_player.sonos) SoCo==0.11.1 # PlexAPI (media_player.plex) https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2 +# SNMP (device_tracker.snmp) +pysnmp==4.2.5 + +# Blinkstick (light.blinksticklight) +blinkstick==1.1.7 + +# Telegram (notify.telegram) +python-telegram-bot==2.8.7 + +# CPUinfo (sensor.cpuinfo) +py-cpuinfo==0.1.6 + # Radio Thermostat (thermostat.radiotherm) radiotherm=1.2 diff --git a/setup.py b/setup.py index 044d5428809..b9b5cdd0d5d 100755 --- a/setup.py +++ b/setup.py @@ -9,11 +9,12 @@ DOWNLOAD_URL = ('https://github.com/balloob/home-assistant/archive/' PACKAGES = find_packages(exclude=['tests', 'tests.*']) -PACKAGE_DATA = \ - {'homeassistant.components.frontend': ['index.html.template'], - 'homeassistant.components.frontend.www_static': ['*.*'], - 'homeassistant.components.frontend.www_static.images': ['*.*'], - 'homeassistant.startup': ['*.*']} +# PACKAGE_DATA = \ +# {'homeassistant.components.frontend': ['index.html.template'], +# 'homeassistant.components.frontend.www_static': ['*.*'], +# 'homeassistant.components.frontend.www_static.images': ['*.*'], +# 'homeassistant.components.mqtt': ['*.crt'], +# 'homeassistant.startup': ['*.*']} REQUIRES = [ 'requests>=2,<3', @@ -23,6 +24,7 @@ REQUIRES = [ 'vincenty==0.1.2' ] + # package_data=PACKAGE_DATA, setup( name=PACKAGE_NAME, version=__version__, @@ -34,7 +36,6 @@ setup( description='Open-source home automation platform running on Python 3.', packages=PACKAGES, include_package_data=True, - package_data=PACKAGE_DATA, zip_safe=False, platforms='any', install_requires=REQUIRES, diff --git a/tests/__init__.py b/tests/__init__.py index c39a22e0b57..e69de29bb2d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +0,0 @@ -import logging -logging.disable(logging.CRITICAL) diff --git a/tests/common.py b/tests/common.py index 9263cae04e3..b8108c673fd 100644 --- a/tests/common.py +++ b/tests/common.py @@ -125,16 +125,14 @@ def mock_http_component(hass): @mock.patch('homeassistant.components.mqtt.MQTT') -@mock.patch('homeassistant.components.mqtt.MQTT.publish') -def mock_mqtt_component(hass, mock_mqtt, mock_mqtt_publish): +def mock_mqtt_component(hass, mock_mqtt): mqtt.setup(hass, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'mock-broker', } }) hass.config.components.append(mqtt.DOMAIN) - - return mock_mqtt_publish + return mock_mqtt class MockHTTP(object): diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py new file mode 100644 index 00000000000..7608600f1b5 --- /dev/null +++ b/tests/components/alarm_control_panel/test_manual.py @@ -0,0 +1,302 @@ +""" +tests.components.alarm_control_panel.test_manual +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests manual alarm control panel component. +""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) +from homeassistant.components import alarm_control_panel +import homeassistant.util.dt as dt_util + +from tests.common import fire_time_changed + +CODE = 'HELLO_CODE' + + +class TestAlarmControlPanelManual(unittest.TestCase): + """ Test the manual alarm module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_arm_home_no_pending(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 0 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, CODE) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_HOME, + self.hass.states.get(entity_id).state) + + def test_arm_home_with_pending(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 1 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_HOME, + self.hass.states.get(entity_id).state) + + def test_arm_home_with_invalid_code(self): + """ Attempt to arm home without a valid code. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 1 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, CODE + '2') + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_arm_away_no_pending(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 0 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + def test_arm_away_with_pending(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 1 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + def test_arm_away_with_invalid_code(self): + """ Attempt to arm away without a valid code. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 1 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE + '2') + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_no_pending(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 0 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_pending(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 1 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=2) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_disarm_while_pending_trigger(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 5 + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_disarm_during_trigger_with_invalid_code(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 5, + 'code': CODE + '2' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py new file mode 100644 index 00000000000..6cba26c15a6 --- /dev/null +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -0,0 +1,178 @@ +""" +tests.components.alarm_control_panel.test_manual +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests manual alarm control panel component. +""" +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) +from homeassistant.components import alarm_control_panel + +from tests.common import mock_mqtt_component, fire_mqtt_message + +CODE = 'HELLO_CODE' + + +class TestAlarmControlPanelMQTT(unittest.TestCase): + """ Test the manual alarm module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + @patch('homeassistant.components.alarm_control_panel.mqtt._LOGGER.error') + def test_fail_setup_without_state_topic(self, mock_error): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'command_topic': 'alarm/command' + }})) + + self.assertEqual(1, mock_error.call_count) + + @patch('homeassistant.components.alarm_control_panel.mqtt._LOGGER.error') + def test_fail_setup_without_command_topic(self, mock_error): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'state_topic': 'alarm/state' + }})) + + self.assertEqual(1, mock_error.call_count) + + def test_update_state_via_state_topic(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_UNKNOWN, + self.hass.states.get(entity_id).state) + + for state in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED): + fire_mqtt_message(self.hass, 'alarm/state', state) + self.hass.pool.block_till_done() + self.assertEqual(state, self.hass.states.get(entity_id).state) + + def test_ignore_update_state_if_unknown_via_state_topic(self): + """ Test arm home method. """ + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_UNKNOWN, + self.hass.states.get(entity_id).state) + + fire_mqtt_message(self.hass, 'alarm/state', 'unsupported state') + self.hass.pool.block_till_done() + self.assertEqual(STATE_UNKNOWN, self.hass.states.get(entity_id).state) + + def test_arm_home_publishes_mqtt(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + }})) + + alarm_control_panel.alarm_arm_home(self.hass) + self.hass.pool.block_till_done() + self.assertEqual(('alarm/command', 'ARM_HOME', 0), + self.mock_publish.mock_calls[-1][1]) + + def test_arm_home_not_publishes_mqtt_with_invalid_code(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234' + }})) + + call_count = self.mock_publish.call_count + alarm_control_panel.alarm_arm_home(self.hass, 'abcd') + self.hass.pool.block_till_done() + self.assertEqual(call_count, self.mock_publish.call_count) + + def test_arm_away_publishes_mqtt(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + }})) + + alarm_control_panel.alarm_arm_away(self.hass) + self.hass.pool.block_till_done() + self.assertEqual(('alarm/command', 'ARM_AWAY', 0), + self.mock_publish.mock_calls[-1][1]) + + def test_arm_away_not_publishes_mqtt_with_invalid_code(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234' + }})) + + call_count = self.mock_publish.call_count + alarm_control_panel.alarm_arm_away(self.hass, 'abcd') + self.hass.pool.block_till_done() + self.assertEqual(call_count, self.mock_publish.call_count) + + def test_disarm_publishes_mqtt(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + }})) + + alarm_control_panel.alarm_disarm(self.hass) + self.hass.pool.block_till_done() + self.assertEqual(('alarm/command', 'DISARM', 0), + self.mock_publish.mock_calls[-1][1]) + + def test_disarm_not_publishes_mqtt_with_invalid_code(self): + self.assertTrue(alarm_control_panel.setup(self.hass, { + 'alarm_control_panel': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234' + }})) + + call_count = self.mock_publish.call_count + alarm_control_panel.alarm_disarm(self.hass, 'abcd') + self.hass.pool.block_till_done() + self.assertEqual(call_count, self.mock_publish.call_count) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index a7c13e866c6..a31f694f8c0 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -8,6 +8,7 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation +import homeassistant.components.automation.state as state class TestAutomationState(unittest.TestCase): @@ -334,3 +335,19 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_if_fails_setup_if_to_boolean_value(self): + self.assertFalse(state.trigger( + self.hass, { + 'platform': 'state', + 'entity_id': 'test.entity', + 'to': True, + }, lambda x: x)) + + def test_if_fails_setup_if_from_boolean_value(self): + self.assertFalse(state.trigger( + self.hass, { + 'platform': 'state', + 'entity_id': 'test.entity', + 'from': True, + }, lambda x: x)) diff --git a/tests/components/test_light.py b/tests/components/test_light.py index 515b79b6fc0..156ba51e59a 100644 --- a/tests/components/test_light.py +++ b/tests/components/test_light.py @@ -152,9 +152,13 @@ class TestLight(unittest.TestCase): data) method, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_XY_COLOR: color_util.color_RGB_to_xy(255, 255, 255)}, - data) + self.assertEquals( + data[light.ATTR_XY_COLOR], + color_util.color_RGB_to_xy(255, 255, 255)) + + self.assertEquals( + data[light.ATTR_RGB_COLOR], + [255, 255, 255]) method, data = dev3.last_call('turn_on') self.assertEqual({light.ATTR_XY_COLOR: [.4, .6]}, data) diff --git a/tests/components/test_media_player.py b/tests/components/test_media_player.py index 28d39206c47..211626ea3fb 100644 --- a/tests/components/test_media_player.py +++ b/tests/components/test_media_player.py @@ -40,7 +40,7 @@ class TestMediaPlayer(unittest.TestCase): def test_services(self): """ - Test if the call service methods conver to correct service calls. + Test if the call service methods convert to correct service calls. """ services = { SERVICE_TURN_ON: media_player.turn_on, diff --git a/tests/components/test_scene.py b/tests/components/test_scene.py new file mode 100644 index 00000000000..2fc8fe085c2 --- /dev/null +++ b/tests/components/test_scene.py @@ -0,0 +1,68 @@ +""" +tests.components.test_scene +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests scene component. +""" +import unittest + +from homeassistant import loader +from homeassistant.components import light, scene + +from tests.common import get_test_home_assistant + + +class TestScene(unittest.TestCase): + """ Test scene component. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_config_not_list(self): + self.assertFalse(scene.setup(self.hass, { + 'scene': {'some': 'dict'} + })) + + def test_config_no_dict_in_list(self): + self.assertFalse(scene.setup(self.hass, { + 'scene': [[]] + })) + + def test_activate_scene(self): + test_light = loader.get_component('light.test') + test_light.init() + + self.assertTrue(light.setup(self.hass, { + light.DOMAIN: {'platform': 'test'} + })) + + light_1, light_2 = test_light.DEVICES[0:2] + + light.turn_off(self.hass, [light_1.entity_id, light_2.entity_id]) + + self.hass.pool.block_till_done() + + self.assertTrue(scene.setup(self.hass, { + 'scene': [{ + 'name': 'test', + 'entities': { + light_1.entity_id: 'on', + light_2.entity_id: { + 'state': 'on', + 'brightness': 100, + } + } + }] + })) + + scene.activate(self.hass, 'scene.test') + self.hass.pool.block_till_done() + + self.assertTrue(light_1.is_on) + self.assertTrue(light_2.is_on) + self.assertEqual(100, + light_2.last_call('turn_on')[1].get('brightness')) diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py new file mode 100644 index 00000000000..d9248d8f861 --- /dev/null +++ b/tests/components/test_shell_command.py @@ -0,0 +1,71 @@ +""" +tests.test_shell_command +~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import os +import tempfile +import unittest +from unittest.mock import patch +from subprocess import SubprocessError + +from homeassistant import core +from homeassistant.components import shell_command + + +class TestShellCommand(unittest.TestCase): + """ Test the demo module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = core.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_executing_service(self): + """ Test if able to call a configured service. """ + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'called.txt') + self.assertTrue(shell_command.setup(self.hass, { + 'shell_command': { + 'test_service': "touch {}".format(path) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.assertTrue(os.path.isfile(path)) + + def test_config_not_dict(self): + """ Test if config is not a dict. """ + self.assertFalse(shell_command.setup(self.hass, { + 'shell_command': ['some', 'weird', 'list'] + })) + + def test_config_not_valid_service_names(self): + """ Test if config contains invalid service names. """ + self.assertFalse(shell_command.setup(self.hass, { + 'shell_command': { + 'this is invalid because space': 'touch bla.txt' + }})) + + @patch('homeassistant.components.shell_command.subprocess.call', + side_effect=SubprocessError) + @patch('homeassistant.components.shell_command._LOGGER.error') + def test_subprocess_raising_error(self, mock_call, mock_error): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'called.txt') + self.assertTrue(shell_command.setup(self.hass, { + 'shell_command': { + 'test_service': "touch {}".format(path) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.assertFalse(os.path.isfile(path)) + self.assertEqual(1, mock_error.call_count) diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 94358f5eb51..2e520ac4980 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -218,3 +218,27 @@ class TestUtil(unittest.TestCase): self.assertEqual(3, len(calls1)) self.assertEqual(2, len(calls2)) + + def test_throttle_per_instance(self): + """ Test that the throttle method is done per instance of a class. """ + + class Tester(object): + @util.Throttle(timedelta(seconds=1)) + def hello(self): + return True + + self.assertTrue(Tester().hello()) + self.assertTrue(Tester().hello()) + + def test_throttle_on_method(self): + """ Test that throttle works when wrapping a method. """ + + class Tester(object): + def hello(self): + return True + + tester = Tester() + throttled = util.Throttle(timedelta(seconds=1))(tester.hello) + + self.assertTrue(throttled()) + self.assertIsNone(throttled())