diff --git a/.coveragerc b/.coveragerc index d0967918a60..1a62d7e60f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -122,6 +122,8 @@ omit = homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/concord232.py + homeassistant/components/binary_sensor/flic.py + homeassistant/components/binary_sensor/hikvision.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/amcrest.py @@ -165,6 +167,7 @@ omit = homeassistant/components/discovery.py homeassistant/components/downloader.py homeassistant/components/emoncms_history.py + homeassistant/components/emulated_hue/upnp.py homeassistant/components/fan/mqtt.py homeassistant/components/feedreader.py homeassistant/components/foursquare.py @@ -183,6 +186,7 @@ omit = homeassistant/components/light/x10.py homeassistant/components/light/yeelight.py homeassistant/components/lirc.py + homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/cmus.py @@ -210,6 +214,7 @@ omit = homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/vlc.py homeassistant/components/media_player/yamaha.py homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py @@ -248,6 +253,7 @@ omit = homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/bom.py + homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cups.py @@ -280,6 +286,7 @@ omit = homeassistant/components/sensor/mhz19.py homeassistant/components/sensor/miflora.py homeassistant/components/sensor/mqtt_room.py + homeassistant/components/sensor/netdata.py homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/nut.py homeassistant/components/sensor/nzbget.py @@ -292,6 +299,7 @@ omit = homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py + homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/snmp.py homeassistant/components/sensor/sonarr.py @@ -313,10 +321,12 @@ omit = homeassistant/components/sensor/waqi.py homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py - homeassistant/components/sensor/waqi.py + homeassistant/components/sensor/zamg.py homeassistant/components/switch/acer_projector.py homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/arest.py + homeassistant/components/switch/broadlink.py + homeassistant/components/switch/digitalloggers.py homeassistant/components/switch/dlink.py homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8621851ffb6..a63c1400723 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,14 @@ # Contributing to Home Assistant -Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them? +Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them? The process is straight-forward. + - Read [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md) by Kubernetes (but skip step 0) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. - Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant. -Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details. +Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details. diff --git a/Dockerfile b/Dockerfile index b42d7edcc89..634edb8af59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,12 @@ WORKDIR /usr/src/app RUN pip3 install --no-cache-dir colorlog cython -# For the nmap tracker, bluetooth tracker, Z-Wave -RUN apt-get update && \ - apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \ +# For the nmap tracker, bluetooth tracker, Z-Wave, tellstick +RUN echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list && \ + wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - && \ + apt-get update && \ + apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev \ + libtelldus-core2 && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* COPY script/build_python_openzwave script/build_python_openzwave diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 95bb7cee24a..3d03336c0fe 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,6 +20,7 @@ import homeassistant.loader as loader import homeassistant.util.package as pkg_util from homeassistant.util.async import ( run_coroutine_threadsafe, run_callback_threadsafe) +from homeassistant.util.logging import AsyncHandler from homeassistant.util.yaml import clear_secret_cache from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError @@ -528,6 +529,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, except ImportError: pass + # AsyncHandler allready exists? + if hass.data.get(core.DATA_ASYNCHANDLER): + return + # Log errors to a file if we have write access to file or config dir err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_path_exists = os.path.isfile(err_log_path) @@ -548,8 +553,12 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, err_handler.setFormatter( logging.Formatter('%(asctime)s %(name)s: %(message)s', datefmt='%y-%m-%d %H:%M:%S')) + + async_handler = AsyncHandler(hass.loop, err_handler) + hass.data[core.DATA_ASYNCHANDLER] = async_handler + logger = logging.getLogger('') - logger.addHandler(err_handler) + logger.addHandler(async_handler) logger.setLevel(logging.INFO) else: diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 49decfc62fe..1b64431c7a1 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -4,6 +4,7 @@ Component to interface with an alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel/ """ +import asyncio import logging import os @@ -42,40 +43,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({ }) -def setup(hass, config): - """Track states and offer events for sensors.""" - component = EntityComponent( - logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - - component.setup(config) - - def alarm_service_handler(service): - """Map services to methods on Alarm.""" - target_alarms = component.extract_from_service(service) - - code = service.data.get(ATTR_CODE) - - method = SERVICE_TO_METHOD[service.service] - - for alarm in target_alarms: - getattr(alarm, method)(code) - - for alarm in target_alarms: - if not alarm.should_poll: - continue - - alarm.update_ha_state(True) - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - for service in SERVICE_TO_METHOD: - hass.services.register(DOMAIN, service, alarm_service_handler, - descriptions.get(service), - schema=ALARM_SERVICE_SCHEMA) - return True - - def alarm_disarm(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} @@ -120,6 +87,53 @@ def alarm_trigger(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) +@asyncio.coroutine +def async_setup(hass, config): + """Track states and offer events for sensors.""" + component = EntityComponent( + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + + yield from component.async_setup(config) + + @asyncio.coroutine + def async_alarm_service_handler(service): + """Map services to methods on Alarm.""" + target_alarms = component.async_extract_from_service(service) + + code = service.data.get(ATTR_CODE) + + method = "async_{}".format(SERVICE_TO_METHOD[service.service]) + + for alarm in target_alarms: + yield from getattr(alarm, method)(code) + + update_tasks = [] + for alarm in target_alarms: + if not alarm.should_poll: + continue + + update_coro = hass.loop.create_task( + alarm.async_update_ha_state(True)) + if hasattr(alarm, 'async_update'): + update_tasks.append(hass.loop.create_task(update_coro)) + else: + yield from update_coro + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + for service in SERVICE_TO_METHOD: + hass.services.async_register( + DOMAIN, service, async_alarm_service_handler, + descriptions.get(service), schema=ALARM_SERVICE_SCHEMA) + + return True + + # pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" @@ -138,18 +152,42 @@ class AlarmControlPanel(Entity): """Send disarm command.""" raise NotImplementedError() + @asyncio.coroutine + def async_alarm_disarm(self, code=None): + """Send disarm command.""" + yield from self.hass.loop.run_in_executor( + None, self.alarm_disarm, code) + def alarm_arm_home(self, code=None): """Send arm home command.""" raise NotImplementedError() + @asyncio.coroutine + def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + yield from self.hass.loop.run_in_executor( + None, self.alarm_arm_home, code) + def alarm_arm_away(self, code=None): """Send arm away command.""" raise NotImplementedError() + @asyncio.coroutine + def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + yield from self.hass.loop.run_in_executor( + None, self.alarm_arm_away, code) + def alarm_trigger(self, code=None): """Send alarm trigger command.""" raise NotImplementedError() + @asyncio.coroutine + def async_alarm_trigger(self, code=None): + """Send alarm trigger command.""" + yield from self.hass.loop.run_in_executor( + None, self.alarm_trigger, code) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index cd37fc6a828..07f90cf4476 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -56,11 +56,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._password = password self._state = STATE_UNKNOWN - @property - def should_poll(self): - """No polling needed.""" - return True - def update(self): """Fetch the latest state.""" self._state = self._alarm.state diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 0bdcf274c08..de153a9e0a5 100755 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -71,11 +71,6 @@ class Concord232Alarm(alarm.AlarmControlPanel): self._alarm.last_partition_update = datetime.datetime.now() self.update() - @property - def should_poll(self): - """Polling needed.""" - return True - @property def name(self): """Return the name of the device.""" @@ -126,7 +121,3 @@ class Concord232Alarm(alarm.AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" self._alarm.arm('auto') - - def alarm_trigger(self, code=None): - """Alarm trigger command.""" - raise NotImplementedError() diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index e84320738a2..96b0fc83ea7 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -97,7 +97,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): def _update_callback(self, partition): """Update HA state, if needed.""" if partition is None or int(partition) == self._partition_number: - self.hass.async_add_job(self.update_ha_state) + self.hass.async_add_job(self.async_update_ha_state()) @property def code_format(self): diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 073d55508ed..cc67795d713 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -116,7 +116,7 @@ class ManualAlarm(alarm.AlarmControlPanel): self._state = STATE_ALARM_DISARMED self._state_ts = dt_util.utcnow() - self.update_ha_state() + self.schedule_update_ha_state() def alarm_arm_home(self, code=None): """Send arm home command.""" @@ -125,7 +125,7 @@ class ManualAlarm(alarm.AlarmControlPanel): self._state = STATE_ALARM_ARMED_HOME self._state_ts = dt_util.utcnow() - self.update_ha_state() + self.schedule_update_ha_state() if self._pending_time: track_point_in_time( @@ -139,7 +139,7 @@ class ManualAlarm(alarm.AlarmControlPanel): self._state = STATE_ALARM_ARMED_AWAY self._state_ts = dt_util.utcnow() - self.update_ha_state() + self.schedule_update_ha_state() if self._pending_time: track_point_in_time( @@ -151,7 +151,7 @@ class ManualAlarm(alarm.AlarmControlPanel): self._pre_trigger_state = self._state self._state = STATE_ALARM_TRIGGERED self._state_ts = dt_util.utcnow() - self.update_ha_state() + self.schedule_update_ha_state() if self._trigger_time: track_point_in_time( diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index cb32fc924e6..58ec8d915ab 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -62,11 +62,6 @@ class NX584Alarm(alarm.AlarmControlPanel): self._alarm.list_zones() self._state = STATE_UNKNOWN - @property - def should_poll(self): - """Polling needed.""" - return True - @property def name(self): """Return the name of the device.""" @@ -122,7 +117,3 @@ class NX584Alarm(alarm.AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" self._alarm.arm('exit') - - def alarm_trigger(self, code=None): - """Alarm trigger command.""" - raise NotImplementedError() diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 40ebfb2f39f..7a8f8409c59 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -61,11 +61,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): else: self._state = STATE_UNKNOWN - @property - def should_poll(self): - """Poll the SimpliSafe API.""" - return True - @property def name(self): """Return the name of the device.""" @@ -104,7 +99,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): return self.simplisafe.set_state('off') _LOGGER.info('SimpliSafe alarm disarming') - self.update() def alarm_arm_home(self, code=None): """Send arm home command.""" @@ -112,7 +106,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): return self.simplisafe.set_state('home') _LOGGER.info('SimpliSafe alarm arming home') - self.update() def alarm_arm_away(self, code=None): """Send arm away command.""" @@ -120,7 +113,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): return self.simplisafe.set_state('away') _LOGGER.info('SimpliSafe alarm arming away') - self.update() def _validate_code(self, code, state): """Validate given code.""" diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 4ef07c68f59..c1a394fe462 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -84,18 +84,15 @@ class VerisureAlarm(alarm.AlarmControlPanel): hub.my_pages.alarm.set(code, 'DISARMED') _LOGGER.info('verisure alarm disarming') hub.my_pages.alarm.wait_while_pending() - self.update() def alarm_arm_home(self, code=None): """Send arm home command.""" hub.my_pages.alarm.set(code, 'ARMED_HOME') _LOGGER.info('verisure alarm arming home') hub.my_pages.alarm.wait_while_pending() - self.update() def alarm_arm_away(self, code=None): """Send arm away command.""" hub.my_pages.alarm.set(code, 'ARMED_AWAY') _LOGGER.info('verisure alarm arming away') hub.my_pages.alarm.wait_while_pending() - self.update() diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 39deae3d66e..4818c02d9ff 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -4,6 +4,8 @@ Offer MQTT listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#mqtt-trigger """ +import json + import voluptuous as vol from homeassistant.core import callback @@ -31,13 +33,20 @@ def async_trigger(hass, config, action): def mqtt_automation_listener(msg_topic, msg_payload, qos): """Listen for MQTT messages.""" if payload is None or payload == msg_payload: + data = { + 'platform': 'mqtt', + 'topic': msg_topic, + 'payload': msg_payload, + 'qos': qos, + } + + try: + data['payload_json'] = json.loads(msg_payload) + except ValueError: + pass + hass.async_run_job(action, { - 'trigger': { - 'platform': 'mqtt', - 'topic': msg_topic, - 'payload': msg_payload, - 'qos': qos, - } + 'trigger': data }) return mqtt.async_subscribe(hass, topic, mqtt_automation_listener) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index fb146991602..65cca462ed9 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -64,10 +64,19 @@ def async_trigger(hass, config, action): call_action() return + @callback + def clear_listener(): + """Clear all unsub listener.""" + nonlocal async_remove_state_for_cancel + nonlocal async_remove_state_for_listener + async_remove_state_for_listener = None + async_remove_state_for_cancel = None + @callback def state_for_listener(now): """Fire on state changes after a delay and calls action.""" async_remove_state_for_cancel() + clear_listener() call_action() @callback @@ -77,6 +86,7 @@ def async_trigger(hass, config, action): return async_remove_state_for_listener() async_remove_state_for_cancel() + clear_listener() async_remove_state_for_listener = async_track_point_in_utc_time( hass, state_for_listener, dt_util.utcnow() + time_delta) diff --git a/homeassistant/components/binary_sensor/flic.py b/homeassistant/components/binary_sensor/flic.py new file mode 100644 index 00000000000..980af069f38 --- /dev/null +++ b/homeassistant/components/binary_sensor/flic.py @@ -0,0 +1,257 @@ +"""Contains functionality to use flic buttons as a binary sensor.""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.util.async import run_callback_threadsafe + + +REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_TIMEOUT = 3 + +CLICK_TYPE_SINGLE = "single" +CLICK_TYPE_DOUBLE = "double" +CLICK_TYPE_HOLD = "hold" +CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD] + +CONF_IGNORED_CLICK_TYPES = "ignored_click_types" + +EVENT_NAME = "flic_click" +EVENT_DATA_NAME = "button_name" +EVENT_DATA_ADDRESS = "button_address" +EVENT_DATA_TYPE = "click_type" +EVENT_DATA_QUEUED_TIME = "queued_time" + +# Validation of the user's configuration +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default='localhost'): cv.string, + vol.Optional(CONF_PORT, default=5551): cv.port, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list, + [vol.In(CLICK_TYPES)]) +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Setup the flic platform.""" + import pyflic + + # Initialize flic client responsible for + # connecting to buttons and retrieving events + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + discovery = config.get(CONF_DISCOVERY) + + try: + client = pyflic.FlicClient(host, port) + except ConnectionRefusedError: + _LOGGER.error("Failed to connect to flic server.") + return + + def new_button_callback(address): + """Setup newly verified button as device in home assistant.""" + hass.add_job(async_setup_button(hass, config, async_add_entities, + client, address)) + + client.on_new_verified_button = new_button_callback + if discovery: + start_scanning(hass, config, async_add_entities, client) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: client.close()) + hass.loop.run_in_executor(None, client.handle_events) + + # Get addresses of already verified buttons + addresses = yield from async_get_verified_addresses(client) + if addresses: + for address in addresses: + yield from async_setup_button(hass, config, async_add_entities, + client, address) + + +def start_scanning(hass, config, async_add_entities, client): + """Start a new flic client for scanning & connceting to new buttons.""" + import pyflic + + scan_wizard = pyflic.ScanWizard() + + def scan_completed_callback(scan_wizard, result, address, name): + """Restart scan wizard to constantly check for new buttons.""" + if result == pyflic.ScanWizardResult.WizardSuccess: + _LOGGER.info("Found new button (%s)", address) + elif result != pyflic.ScanWizardResult.WizardFailedTimeout: + _LOGGER.warning("Failed to connect to button (%s). Reason: %s", + address, result) + + # Restart scan wizard + start_scanning(hass, config, async_add_entities, client) + + scan_wizard.on_completed = scan_completed_callback + client.add_scan_wizard(scan_wizard) + + +@asyncio.coroutine +def async_setup_button(hass, config, async_add_entities, client, address): + """Setup single button device.""" + timeout = config.get(CONF_TIMEOUT) + ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES) + button = FlicButton(hass, client, address, timeout, ignored_click_types) + _LOGGER.info("Connected to button (%s)", address) + + yield from async_add_entities([button]) + + +@asyncio.coroutine +def async_get_verified_addresses(client): + """Retrieve addresses of verified buttons.""" + future = asyncio.Future() + loop = asyncio.get_event_loop() + + def get_info_callback(items): + """Set the addressed of connected buttons as result of the future.""" + addresses = items["bd_addr_of_verified_buttons"] + run_callback_threadsafe(loop, future.set_result, addresses) + client.get_info(get_info_callback) + + return future + + +class FlicButton(BinarySensorDevice): + """Representation of a flic button.""" + + def __init__(self, hass, client, address, timeout, ignored_click_types): + """Initialize the flic button.""" + import pyflic + + self._hass = hass + self._address = address + self._timeout = timeout + self._is_down = False + self._ignored_click_types = ignored_click_types or [] + self._hass_click_types = { + pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE, + pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, + pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, + pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD, + } + + self._channel = self._create_channel() + client.add_connection_channel(self._channel) + + def _create_channel(self): + """Create a new connection channel to the button.""" + import pyflic + + channel = pyflic.ButtonConnectionChannel(self._address) + channel.on_button_up_or_down = self._on_up_down + + # If all types of clicks should be ignored, skip registering callbacks + if set(self._ignored_click_types) == set(CLICK_TYPES): + return channel + + if CLICK_TYPE_DOUBLE in self._ignored_click_types: + # Listen to all but double click type events + channel.on_button_click_or_hold = self._on_click + elif CLICK_TYPE_HOLD in self._ignored_click_types: + # Listen to all but hold click type events + channel.on_button_single_or_double_click = self._on_click + else: + # Listen to all click type events + channel.on_button_single_or_double_click_or_hold = self._on_click + + return channel + + @property + def name(self): + """Return the name of the device.""" + return "flic_%s" % self.address.replace(":", "") + + @property + def address(self): + """Return the bluetooth address of the device.""" + return self._address + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._is_down + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state_attributes(self): + """Return device specific state attributes.""" + attr = super(FlicButton, self).state_attributes + attr["address"] = self.address + + return attr + + def _queued_event_check(self, click_type, time_diff): + """Generate a log message and returns true if timeout exceeded.""" + time_string = "{:d} {}".format( + time_diff, "second" if time_diff == 1 else "seconds") + + if time_diff > self._timeout: + _LOGGER.warning( + "Queued %s dropped for %s. Time in queue was %s.", + click_type, self.address, time_string) + return True + else: + _LOGGER.info( + "Queued %s allowed for %s. Time in queue was %s.", + click_type, self.address, time_string) + return False + + def _on_up_down(self, channel, click_type, was_queued, time_diff): + """Update device state, if event was not queued.""" + import pyflic + + if was_queued and self._queued_event_check(click_type, time_diff): + return + + self._is_down = click_type == pyflic.ClickType.ButtonDown + self.schedule_update_ha_state() + + def _on_click(self, channel, click_type, was_queued, time_diff): + """Fire click event, if event was not queued.""" + # Return if click event was queued beyond allowed timeout + if was_queued and self._queued_event_check(click_type, time_diff): + return + + # Return if click event is in ignored click types + hass_click_type = self._hass_click_types[click_type] + if hass_click_type in self._ignored_click_types: + return + + self._hass.bus.fire(EVENT_NAME, { + EVENT_DATA_NAME: self.name, + EVENT_DATA_ADDRESS: self.address, + EVENT_DATA_QUEUED_TIME: time_diff, + EVENT_DATA_TYPE: hass_click_type + }) + + def _connection_status_changed(self, channel, + connection_status, disconnect_reason): + """Remove device, if button disconnects.""" + import pyflic + + if connection_status == pyflic.ConnectionStatus.Disconnected: + _LOGGER.info("Button (%s) disconnected. Reason: %s", + self.address, disconnect_reason) + self.remove() diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py new file mode 100644 index 00000000000..90d61cbf3b7 --- /dev/null +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -0,0 +1,262 @@ +""" +Support for Hikvision event stream events represented as binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.hikvision/ +""" +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, + CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) + +REQUIREMENTS = ['pyhik==0.0.6', 'pydispatcher==2.0.5'] +_LOGGER = logging.getLogger(__name__) + +CONF_IGNORED = 'ignored' +CONF_DELAY = 'delay' + +DEFAULT_PORT = 80 +DEFAULT_IGNORED = False +DEFAULT_DELAY = 0 + +ATTR_DELAY = 'delay' + +SENSOR_CLASS_MAP = { + 'Motion': 'motion', + 'Line Crossing': 'motion', + 'IO Trigger': None, + 'Field Detection': 'motion', + 'Video Loss': None, + 'Tamper Detection': 'motion', + 'Shelter Alarm': None, + 'Disk Full': None, + 'Disk Error': None, + 'Net Interface Broken': 'connectivity', + 'IP Conflict': 'connectivity', + 'Illegal Access': None, + 'Video Mismatch': None, + 'Bad Video': None, + 'PIR Alarm': 'motion', + 'Face Detection': 'motion', +} + +CUSTOMIZE_SCHEMA = vol.Schema({ + vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int + }) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=None): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CUSTOMIZE, default={}): + vol.Schema({cv.string: CUSTOMIZE_SCHEMA}), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Setup Hikvision binary sensor devices.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + customize = config.get(CONF_CUSTOMIZE) + + if config.get(CONF_SSL): + protocol = "https" + else: + protocol = "http" + + url = '{}://{}'.format(protocol, host) + + data = HikvisionData(hass, url, port, name, username, password) + + if data.sensors is None: + _LOGGER.error('Hikvision event stream has no data, unable to setup.') + return False + + entities = [] + + for sensor in data.sensors: + # Build sensor name, then parse customize config. + sensor_name = sensor.replace(' ', '_') + + custom = customize.get(sensor_name.lower(), {}) + ignore = custom.get(CONF_IGNORED) + delay = custom.get(CONF_DELAY) + + _LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s', + data.name, sensor_name, ignore, delay) + if not ignore: + entities.append(HikvisionBinarySensor(hass, sensor, data, delay)) + + add_entities(entities) + + +class HikvisionData(object): + """Hikvision camera event stream object.""" + + def __init__(self, hass, url, port, name, username, password): + """Initialize the data oject.""" + from pyhik.hikvision import HikCamera + self._url = url + self._port = port + self._name = name + self._username = username + self._password = password + + # Establish camera + self._cam = HikCamera(self._url, self._port, + self._username, self._password) + + if self._name is None: + self._name = self._cam.get_name + + # Start event stream + self._cam.start_stream() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik) + + def stop_hik(self, event): + """Shutdown Hikvision subscriptions and subscription thread on exit.""" + self._cam.disconnect() + + @property + def sensors(self): + """Return list of available sensors and their states.""" + return self._cam.current_event_states + + @property + def cam_id(self): + """Return camera id.""" + return self._cam.get_id + + @property + def name(self): + """Return camera name.""" + return self._name + + +class HikvisionBinarySensor(BinarySensorDevice): + """Representation of a Hikvision binary sensor.""" + + def __init__(self, hass, sensor, cam, delay): + """Initialize the binary_sensor.""" + from pydispatch import dispatcher + + self._hass = hass + self._cam = cam + self._name = self._cam.name + ' ' + sensor + self._id = self._cam.cam_id + '.' + sensor + self._sensor = sensor + + if delay is None: + self._delay = 0 + else: + self._delay = delay + + self._timer = None + + # Form signal for dispatcher + signal = 'ValueChanged.{}'.format(self._cam.cam_id) + + dispatcher.connect(self._update_callback, + signal=signal, + sender=self._sensor) + + def _sensor_state(self): + """Extract sensor state.""" + return self._cam.sensors[self._sensor][0] + + def _sensor_last_update(self): + """Extract sensor last update time.""" + return self._cam.sensors[self._sensor][3] + + @property + def name(self): + """Return the name of the Hikvision sensor.""" + return self._name + + @property + def unique_id(self): + """Return an unique ID.""" + return '{}.{}'.format(self.__class__, self._id) + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._sensor_state() + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + try: + return SENSOR_CLASS_MAP[self._sensor] + except KeyError: + # Sensor must be unknown to us, add as generic + return None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update() + + if self._delay != 0: + attr[ATTR_DELAY] = self._delay + + return attr + + def _update_callback(self, signal, sender): + """Update the sensor's state, if needed.""" + _LOGGER.debug('Dispatcher callback, signal: %s, sender: %s', + signal, sender) + + if sender is not self._sensor: + return + + if self._delay > 0 and not self.is_on: + # Set timer to wait until updating the state + def _delay_update(now): + """Timer callback for sensor update.""" + _LOGGER.debug('%s Called delayed (%ssec) update.', + self._name, self._delay) + self.schedule_update_ha_state() + self._timer = None + + if self._timer is not None: + self._timer() + self._timer = None + + self._timer = track_point_in_utc_time( + self._hass, _delay_update, + utcnow() + timedelta(seconds=self._delay)) + + elif self._delay > 0 and self.is_on: + # For delayed sensors kill any callbacks on true events and update + if self._timer is not None: + self._timer() + self._timer = None + + self.schedule_update_ha_state() + + else: + self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 93b3bb5817c..94ef0faaad0 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -23,9 +23,11 @@ _LOGGER = logging.getLogger(__name__) # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { - "Someone known": "motion", - "Someone unknown": "motion", - "Motion": "motion", + "Someone known": 'occupancy', + "Someone unknown": 'motion', + "Motion": 'motion', + "Tag Vibration": 'vibration', + "Tag Open": 'opening', } CONF_HOME = 'home' @@ -48,6 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): home = config.get(CONF_HOME, None) timeout = config.get(CONF_TIMEOUT, 15) + module_name = None + import lnetatmo try: data = WelcomeData(netatmo.NETATMO_AUTH, home) @@ -64,23 +68,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): camera_name not in config[CONF_CAMERAS]: continue for variable in sensors: - add_devices([WelcomeBinarySensor(data, camera_name, home, timeout, - variable)]) + if variable in ('Tag Vibration', 'Tag Open'): + continue + add_devices([WelcomeBinarySensor(data, camera_name, module_name, + home, timeout, variable)]) + + for module_name in data.get_module_names(camera_name): + for variable in sensors: + if variable in ('Tag Vibration', 'Tag Open'): + add_devices([WelcomeBinarySensor(data, camera_name, + module_name, home, + timeout, variable)]) class WelcomeBinarySensor(BinarySensorDevice): """Represent a single binary sensor in a Netatmo Welcome device.""" - def __init__(self, data, camera_name, home, timeout, sensor): + def __init__(self, data, camera_name, module_name, home, timeout, sensor): """Setup for access to the Netatmo camera events.""" self._data = data self._camera_name = camera_name + self._module_name = module_name self._home = home self._timeout = timeout if home: self._name = home + ' / ' + camera_name else: self._name = camera_name + if module_name: + self._name += ' / ' + module_name self._sensor_name = sensor self._name += ' ' + sensor camera_id = data.welcomedata.cameraByName(camera=camera_name, @@ -112,7 +128,7 @@ class WelcomeBinarySensor(BinarySensorDevice): def update(self): """Request an update from the Netatmo API.""" self._data.update() - self._data.welcomedata.updateEvent(home=self._data.home) + self._data.update_event() if self._sensor_name == "Someone known": self._state =\ @@ -129,5 +145,16 @@ class WelcomeBinarySensor(BinarySensorDevice): self._data.welcomedata.motionDetected(self._home, self._camera_name, self._timeout*60) + elif self._sensor_name == "Tag Vibration": + self._state =\ + self._data.welcomedata.moduleMotionDetected(self._home, + self._module_name, + self._camera_name, + self._timeout*60) + elif self._sensor_name == "Tag Open": + self._state =\ + self._data.welcomedata.moduleOpened(self._home, + self._module_name, + self._camera_name) else: return None diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 2d0e3f7226f..b129b5f24d4 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -40,6 +40,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for sensor in pywink.get_smoke_and_co_detectors(): add_devices([WinkBinarySensorDevice(sensor, hass)]) + for hub in pywink.get_hubs(): + add_devices([WinkHub(hub, hass)]) + class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): """Representation of a Wink binary sensor.""" @@ -79,3 +82,24 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): def sensor_class(self): """Return the class of this sensor, from SENSOR_CLASSES.""" return SENSOR_TYPES.get(self.capability) + + +class WinkHub(WinkDevice, BinarySensorDevice, Entity): + """Representation of a Wink Hub.""" + + def __init(self, wink, hass): + """Initialize the hub sensor.""" + WinkDevice.__init__(self, wink, hass) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'update needed': self.wink.update_needed(), + 'firmware version': self.wink.firmware_version() + } + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.wink.state() diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 97ed45f21e4..c6568677583 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -18,16 +18,26 @@ REQUIREMENTS = ['amcrest==1.0.0'] _LOGGER = logging.getLogger(__name__) -DEFAULT_PORT = 80 +CONF_RESOLUTION = 'resolution' + DEFAULT_NAME = 'Amcrest Camera' +DEFAULT_PORT = 80 +DEFAULT_RESOLUTION = 'high' NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_TITLE = 'Amcrest Camera Setup' +RESOLUTION_LIST = { + 'high': 0, + 'low': 1, +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): + vol.All(vol.In(RESOLUTION_LIST)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) @@ -64,13 +74,14 @@ class AmcrestCam(Camera): def __init__(self, device_info, data): """Initialize an Amcrest camera.""" super(AmcrestCam, self).__init__() - self._name = device_info.get(CONF_NAME) self._data = data + self._name = device_info.get(CONF_NAME) + self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] def camera_image(self): """Return a still image reponse from the camera.""" # Send the request to snap a picture and return raw jpg data - response = self._data.camera.snapshot() + response = self._data.camera.snapshot(channel=self._resolution) return response.data @property diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index c5603dac142..b42c62e56d0 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FILE_PATH): cv.string, vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), - vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP): + vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT): vol.Coerce(int), vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index c98ac6d0106..84d8c32f9ff 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -195,8 +195,9 @@ class Thermostat(ClimateDevice): mode = self.mode events = self.thermostat['events'] for event in events: - if event['running']: - mode = event['holdClimateRef'] + if event['holdClimateRef'] == 'away' or \ + event['type'] == 'autoAway': + mode = "away" break return 'away' in mode diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 1b3d20d8b59..a40795c37c5 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -198,24 +198,30 @@ class GenericThermostat(ClimateDevice): return if self.ac_mode: - too_hot = self._cur_temp - self._target_temp > self._tolerance is_cooling = self._is_device_active - if too_hot and not is_cooling: - _LOGGER.info('Turning on AC %s', self.heater_entity_id) - switch.turn_on(self.hass, self.heater_entity_id) - elif not too_hot and is_cooling: - _LOGGER.info('Turning off AC %s', self.heater_entity_id) - switch.turn_off(self.hass, self.heater_entity_id) + if is_cooling: + too_cold = self._target_temp - self._cur_temp > self._tolerance + if too_cold: + _LOGGER.info('Turning off AC %s', self.heater_entity_id) + switch.turn_off(self.hass, self.heater_entity_id) + else: + too_hot = self._cur_temp - self._target_temp > self._tolerance + if too_hot: + _LOGGER.info('Turning on AC %s', self.heater_entity_id) + switch.turn_on(self.hass, self.heater_entity_id) else: - too_cold = self._target_temp - self._cur_temp > self._tolerance is_heating = self._is_device_active - - if too_cold and not is_heating: - _LOGGER.info('Turning on heater %s', self.heater_entity_id) - switch.turn_on(self.hass, self.heater_entity_id) - elif not too_cold and is_heating: - _LOGGER.info('Turning off heater %s', self.heater_entity_id) - switch.turn_off(self.hass, self.heater_entity_id) + if is_heating: + too_hot = self._cur_temp - self._target_temp > self._tolerance + if too_hot: + _LOGGER.info('Turning off heater %s', + self.heater_entity_id) + switch.turn_off(self.hass, self.heater_entity_id) + else: + too_cold = self._target_temp - self._cur_temp > self._tolerance + if too_cold: + _LOGGER.info('Turning on heater %s', self.heater_entity_id) + switch.turn_on(self.hass, self.heater_entity_id) @property def _is_device_active(self): diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index ab41c10e6d1..e098c3c3709 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -155,8 +155,8 @@ class NestThermostat(ClimateDevice): """Set new target temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if target_temp_low is not None and target_temp_high is not None: - if self._mode == STATE_HEAT_COOL: + if self._mode == STATE_HEAT_COOL: + if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) else: temp = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index d06e148cfdd..9a0e5666036 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -23,10 +23,19 @@ ATTR_FAN = 'fan' ATTR_MODE = 'mode' CONF_HOLD_TEMP = 'hold_temp' +CONF_AWAY_TEMPERATURE_HEAT = 'away_temperature_heat' +CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool' + +DEFAULT_AWAY_TEMPERATURE_HEAT = 60 +DEFAULT_AWAY_TEMPERATURE_COOL = 85 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, + vol.Optional(CONF_AWAY_TEMPERATURE_HEAT, + default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float), + vol.Optional(CONF_AWAY_TEMPERATURE_COOL, + default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float), }) @@ -45,12 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False hold_temp = config.get(CONF_HOLD_TEMP) + away_temps = [ + config.get(CONF_AWAY_TEMPERATURE_HEAT), + config.get(CONF_AWAY_TEMPERATURE_COOL) + ] tstats = [] for host in hosts: try: tstat = radiotherm.get_thermostat(host) - tstats.append(RadioThermostat(tstat, hold_temp)) + tstats.append(RadioThermostat(tstat, hold_temp, away_temps)) except OSError: _LOGGER.exception("Unable to connect to Radio Thermostat: %s", host) @@ -61,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RadioThermostat(ClimateDevice): """Representation of a Radio Thermostat.""" - def __init__(self, device, hold_temp): + def __init__(self, device, hold_temp, away_temps): """Initialize the thermostat.""" self.device = device self.set_time() @@ -71,7 +84,10 @@ class RadioThermostat(ClimateDevice): self._name = None self._fmode = None self._tmode = None - self.hold_temp = hold_temp + self._hold_temp = hold_temp + self._away = False + self._away_temps = away_temps + self._prev_temp = None self.update() self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF] @@ -113,6 +129,11 @@ class RadioThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._away + def update(self): """Update the data from the thermostat.""" self._current_temperature = self.device.temp['raw'] @@ -138,7 +159,7 @@ class RadioThermostat(ClimateDevice): self.device.t_cool = round(temperature * 2.0) / 2.0 elif self._current_operation == STATE_HEAT: self.device.t_heat = round(temperature * 2.0) / 2.0 - if self.hold_temp: + if self._hold_temp or self._away: self.device.hold = 1 else: self.device.hold = 0 @@ -162,3 +183,23 @@ class RadioThermostat(ClimateDevice): self.device.t_cool = round(self._target_temperature * 2.0) / 2.0 elif operation_mode == STATE_HEAT: self.device.t_heat = round(self._target_temperature * 2.0) / 2.0 + + def turn_away_mode_on(self): + """Turn away on. + + The RTCOA app simulates away mode by using a hold. + """ + away_temp = None + if not self._away: + self._prev_temp = self._target_temperature + if self._current_operation == STATE_HEAT: + away_temp = self._away_temps[0] + elif self._current_operation == STATE_COOL: + away_temp = self._away_temps[1] + self._away = True + self.set_temperature(temperature=away_temp) + + def turn_away_mode_off(self): + """Turn away off.""" + self._away = False + self.set_temperature(temperature=self._prev_temp) diff --git a/homeassistant/components/cover/tellduslive.py b/homeassistant/components/cover/tellduslive.py new file mode 100644 index 00000000000..c48a14e9133 --- /dev/null +++ b/homeassistant/components/cover/tellduslive.py @@ -0,0 +1,46 @@ +""" +Support for Tellstick covers using Tellstick Net. + +This platform uses the Telldus Live online service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.tellduslive/ + +""" +import logging + +from homeassistant.components.cover import CoverDevice +from homeassistant.components.tellduslive import TelldusLiveEntity + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup covers.""" + if discovery_info is None: + return + add_devices(TelldusLiveCover(hass, cover) for cover in discovery_info) + + +class TelldusLiveCover(TelldusLiveEntity, CoverDevice): + """Representation of a cover.""" + + @property + def is_closed(self): + """Return the current position of the cover.""" + return self.device.is_down + + def close_cover(self, **kwargs): + """Close the cover.""" + self.device.down() + self.changed() + + def open_cover(self, **kwargs): + """Open the cover.""" + self.device.up() + self.changed() + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self.device.stop() + self.changed() diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 91f0720e927..d497ea4c314 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -282,7 +282,7 @@ class DeviceTracker(object): list(self.group.tracking) + [device.entity_id]) # lookup mac vendor string to be stored in config - device.set_vendor_for_mac() + yield from device.set_vendor_for_mac() # update known_devices.yaml self.hass.async_add_job( @@ -370,6 +370,7 @@ class Device(Entity): self.away_hide = hide_if_away self.vendor = vendor + self._attributes = {} @property def name(self): @@ -399,12 +400,13 @@ class Device(Entity): if self.battery: attr[ATTR_BATTERY] = self.battery - if self.attributes: - for key, value in self.attributes.items(): - attr[key] = value - return attr + @property + def device_state_attributes(self): + """Return device state attributes.""" + return self._attributes + @property def hidden(self): """If device should be hidden.""" @@ -419,8 +421,11 @@ class Device(Entity): self.host_name = host_name self.location_name = location_name self.gps_accuracy = gps_accuracy or 0 - self.battery = battery - self.attributes = attributes + if battery: + self.battery = battery + if attributes: + self._attributes.update(attributes) + self.gps = None if gps is not None: diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 2eced1b4dd4..4e860846f8e 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -286,8 +286,10 @@ class AsusWrtDeviceScanner(object): # match mac addresses to IP addresses in ARP table for arp in result.arp: - if match.group('mac').lower() in arp.decode('utf-8'): - arp_match = _ARP_REGEX.search(arp.decode('utf-8')) + if match.group('mac').lower() in \ + arp.decode('utf-8').lower(): + arp_match = _ARP_REGEX.search( + arp.decode('utf-8').lower()) if not arp_match: _LOGGER.warning('Could not parse arp row: %s', arp) continue diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 2e897ccb10c..22099630bd1 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -64,9 +64,22 @@ class GPSLoggerView(HomeAssistantView): if 'battery' in data: battery = float(data['battery']) + attrs = {} + if 'speed' in data: + attrs['speed'] = float(data['speed']) + if 'direction' in data: + attrs['direction'] = float(data['direction']) + if 'altitude' in data: + attrs['altitude'] = float(data['altitude']) + if 'provider' in data: + attrs['provider'] = data['provider'] + if 'activity' in data: + attrs['activity'] = data['activity'] + yield from hass.loop.run_in_executor( None, partial(self.see, dev_id=device, gps=gps_location, battery=battery, - gps_accuracy=accuracy)) + gps_accuracy=accuracy, + attributes=attrs)) return 'Setting location for {}'.format(device) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 2e8bbc5d2a1..404492e35cf 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -25,6 +25,8 @@ _LOGGER = logging.getLogger(__name__) CONF_EXCLUDE = 'exclude' # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = 'home_interval' +CONF_OPTIONS = 'scan_options' +DEFAULT_OPTIONS = '-F --host-timeout 5s' MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) @@ -33,7 +35,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOSTS): cv.ensure_list, vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int, vol.Optional(CONF_EXCLUDE, default=[]): - vol.All(cv.ensure_list, vol.Length(min=1)) + vol.All(cv.ensure_list, vol.Length(min=1)), + vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS): + cv.string }) @@ -69,8 +73,9 @@ class NmapDeviceScanner(object): self.last_results = [] self.hosts = config[CONF_HOSTS] - self.exclude = config.get(CONF_EXCLUDE, []) + self.exclude = config[CONF_EXCLUDE] minutes = config[CONF_HOME_INTERVAL] + self._options = config[CONF_OPTIONS] self.home_interval = timedelta(minutes=minutes) self.success_init = self._update_info() @@ -103,7 +108,7 @@ class NmapDeviceScanner(object): from nmap import PortScanner, PortScannerError scanner = PortScanner() - options = '-F --host-timeout 5s ' + options = self._options if self.home_interval: boundary = dt_util.now() - self.home_interval diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index d654c3e3eef..ab84eb22e04 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -9,16 +9,20 @@ import urllib import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.loader as loader from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD # Unifi package doesn't list urllib3 as a requirement -REQUIREMENTS = ['urllib3', 'unifi==1.2.5'] +REQUIREMENTS = ['urllib3', 'pyunifi==1.3'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' CONF_SITE_ID = 'site_id' +NOTIFICATION_ID = 'unifi_notification' +NOTIFICATION_TITLE = 'Unifi Device Tracker Setup' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default='localhost'): cv.string, vol.Optional(CONF_SITE_ID, default='default'): cv.string, @@ -30,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_scanner(hass, config): """Setup Unifi device_tracker.""" - from unifi.controller import Controller + from pyunifi.controller import Controller host = config[DOMAIN].get(CONF_HOST) username = config[DOMAIN].get(CONF_USERNAME) @@ -38,10 +42,18 @@ def get_scanner(hass, config): site_id = config[DOMAIN].get(CONF_SITE_ID) port = config[DOMAIN].get(CONF_PORT) + persistent_notification = loader.get_component('persistent_notification') try: ctrl = Controller(host, username, password, port, 'v4', site_id) except urllib.error.HTTPError as ex: - _LOGGER.error('Failed to connect to unifi: %s', ex) + _LOGGER.error('Failed to connect to Unifi: %s', ex) + persistent_notification.create( + hass, 'Failed to connect to Unifi. ' + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) return False return UnifiScanner(ctrl) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 142764ea522..32cb205fa0f 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.7.7'] +REQUIREMENTS = ['netdisco==0.8.1'] DOMAIN = 'discovery' @@ -36,6 +36,8 @@ SERVICE_HANDLERS = { 'yamaha': ('media_player', 'yamaha'), 'logitech_mediaserver': ('media_player', 'squeezebox'), 'directv': ('media_player', 'directv'), + 'denonavr': ('media_player', 'denonavr'), + 'samsung_tv': ('media_player', 'samsungtv'), } CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index ed06da9495b..57a4f18825a 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -6,12 +6,16 @@ from aiohttp import web from homeassistant import core from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS ) +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS, + SUPPORT_VOLUME_SET, +) from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -65,8 +69,11 @@ class HueAllLightsStateView(HomeAssistantView): for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): + state, brightness = get_entity_state(self.config, entity) + number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(entity) + json_response[number] = entity_to_json( + entity, state, brightness) return self.json(json_response) @@ -97,16 +104,9 @@ class HueOneLightStateView(HomeAssistantView): _LOGGER.error('Entity not exposed: %s', entity_id) return web.Response(text="Entity not exposed", status=404) - cached_state = self.config.cached_states.get(entity_id, None) + state, brightness = get_entity_state(self.config, entity) - if cached_state is None: - final_state = entity.state == STATE_ON - final_brightness = entity.attributes.get( - ATTR_BRIGHTNESS, 255 if final_state else 0) - else: - final_state, final_brightness = cached_state - - json_response = entity_to_json(entity, final_state, final_brightness) + json_response = entity_to_json(entity, state, brightness) return self.json(json_response) @@ -158,14 +158,24 @@ class HueOneLightChangeView(HomeAssistantView): result, brightness = parsed + # Choose general HA domain + domain = core.DOMAIN + # Convert the resulting "on" status into the service we need to call service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} + # Make sure the entity actually supports brightness + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: + if brightness is not None: + data[ATTR_BRIGHTNESS] = brightness + # If the requested entity is a script add some variables - if entity.domain == "script": + elif entity.domain == "script": data['variables'] = { 'requested_state': STATE_ON if result else STATE_OFF } @@ -173,8 +183,16 @@ class HueOneLightChangeView(HomeAssistantView): if brightness is not None: data['variables']['requested_level'] = brightness - elif brightness is not None: - data[ATTR_BRIGHTNESS] = brightness + # If the requested entity is a media player, convert to volume + elif entity.domain == "media_player": + media_commands = entity.attributes.get( + ATTR_SUPPORTED_MEDIA_COMMANDS, 0) + if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET: + if brightness is not None: + domain = entity.domain + service = SERVICE_VOLUME_SET + # Convert 0-100 to 0.0-1.0 + data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0 if entity.domain in config.off_maps_to_on_domains: # Map the off command to on @@ -187,9 +205,14 @@ class HueOneLightChangeView(HomeAssistantView): # as the actual requested command. config.cached_states[entity_id] = (result, brightness) - # Perform the requested action - yield from hass.services.async_call(core.DOMAIN, service, data, - blocking=True) + # Separate call to turn on needed + if domain != core.DOMAIN: + hass.async_add_job(hass.services.async_call( + core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, + blocking=True)) + + hass.async_add_job(hass.services.async_call( + domain, service, data, blocking=True)) json_response = \ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] @@ -219,23 +242,23 @@ def parse_hue_api_put_light_body(request_json, entity): result = False if HUE_API_STATE_BRI in request_json: + try: + # Clamp brightness from 0 to 255 + brightness = \ + max(0, min(int(request_json[HUE_API_STATE_BRI]), 255)) + except ValueError: + return None + # Make sure the entity actually supports brightness entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: - try: - # Clamp brightness from 0 to 255 - brightness = \ - max(0, min(int(request_json[HUE_API_STATE_BRI]), 255)) - except ValueError: - return None - report_brightness = True result = (brightness > 0) - elif entity.domain.lower() == "script": - # Convert 0-255 to 0-100 - level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100 + elif entity.domain == "script" or entity.domain == "media_player": + # Convert 0-255 to 0-100 + level = brightness / 255 * 100 brightness = round(level) report_brightness = True result = True @@ -243,16 +266,35 @@ def parse_hue_api_put_light_body(request_json, entity): return (result, brightness) if report_brightness else (result, None) +def get_entity_state(config, entity): + """Retrieve and convert state and brightness values for an entity.""" + cached_state = config.cached_states.get(entity.entity_id, None) + + if cached_state is None: + final_state = entity.state != STATE_OFF + final_brightness = entity.attributes.get( + ATTR_BRIGHTNESS, 255 if final_state else 0) + + # Make sure the entity actually supports brightness + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: + pass + + elif entity.domain == "media_player": + level = entity.attributes.get( + ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0) + # Convert 0.0-1.0 to 0-255 + final_brightness = round(min(1.0, level) * 255) + else: + final_state, final_brightness = cached_state + + return (final_state, final_brightness) + + def entity_to_json(entity, is_on=None, brightness=None): """Convert an entity to its Hue bridge JSON representation.""" - if is_on is None: - is_on = entity.state == STATE_ON - - if brightness is None: - brightness = 255 if is_on else 0 - - name = entity.attributes.get( - ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME]) + name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) return { 'state': diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 837fdd0e1fe..6c480254cb8 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,18 +1,18 @@ """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" FINGERPRINTS = { - "core.js": "5dfb2d3e567fad37af0321d4b29265ed", - "frontend.html": "6a89b74ab2b76c7d28fad2aea9444ec2", + "core.js": "ad1ebcd0614c98a390d982087a7ca75c", + "frontend.html": "826ee6a4b39c939e31aa468b1ef618f9", "mdi.html": "46a76f877ac9848899b8ed382427c16f", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e", "panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825", - "panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a", + "panels/ha-panel-dev-service.html": "ac74f7ce66fd7136d25c914ea12f4351", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb", - "panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2", + "panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89", "websocket_test.html": "575de64b431fe11c3785bf96d7813450" } diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index a70b70d2fd8..6cab0b713f3 100644 --- a/homeassistant/components/frontend/www_static/core.js +++ b/homeassistant/components/frontend/www_static/core.js @@ -1,4 +1,4 @@ -!(function(){"use strict";function t(t){return t&&t.__esModule?t.default:t}function e(t,e){return e={exports:{}},t(e,e.exports),e.exports}function n(t,e){var n=e.authToken,r=e.host;return xe({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function r(){return Ve.getInitialState()}function i(t,e){var n=e.errorMessage;return t.withMutations((function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)}))}function o(t,e){var n=e.authToken,r=e.host;return Fe({authToken:n,host:r})}function u(){return Ge.getInitialState()}function a(t,e){var n=e.rememberAuth;return n}function s(t){return t.withMutations((function(t){t.set("isStreaming",!0).set("hasError",!1)}))}function c(t){return t.withMutations((function(t){t.set("isStreaming",!1).set("hasError",!0)}))}function f(){return Xe.getInitialState()}function h(t){return{type:"auth",api_password:t}}function l(){return{type:"get_states"}}function p(){return{type:"get_config"}}function _(){return{type:"get_services"}}function d(){return{type:"get_panels"}}function v(t,e,n){var r={type:"call_service",domain:t,service:e};return n&&(r.service_data=n),r}function y(t){var e={type:"subscribe_events"};return t&&(e.event_type=t),e}function g(t){return{type:"unsubscribe_events",subscription:t}}function m(){return{type:"ping"}}function S(t,e){return{type:"result",success:!1,error:{code:t,message:e}}}function E(t){return t.result}function b(t,e){var n=new tn(t,e);return n.connect()}function I(t,e,n,r){void 0===r&&(r=null);var i=t.evaluate(Mo.authInfo),o=i.host+"/api/"+n;return new Promise(function(t,n){var u=new XMLHttpRequest;u.open(e,o,!0),u.setRequestHeader("X-HA-access",i.authToken),u.onload=function(){var e;try{e="application/json"===u.getResponseHeader("content-type")?JSON.parse(u.responseText):u.responseText}catch(t){e=u.responseText}u.status>199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function O(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?sn({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||sn;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function gt(t,e){var n=e.date;return n.toISOString()}function mt(){return Qr.getInitialState()}function St(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,$r({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],$r(e.map(In.fromJSON)))}))}))}function Et(){return ti.getInitialState()}function bt(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,ii(e.map(In.fromJSON)))}))}))}function It(){return oi.getInitialState()}function Ot(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(si,r)}))}function wt(){return ci.getInitialState()}function Tt(t,e){t.dispatch(Wr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function At(t,e){void 0===e&&(e=null),t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),on(t,"GET",n).then((function(e){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function Ct(t,e){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_START,{date:e}),on(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function Dt(t){var e=t.evaluate(li);return Ct(t,e)}function zt(t){t.registerStores({currentEntityHistoryDate:Qr,entityHistory:ti,isLoadingEntityHistory:ni,recentEntityHistory:oi,recentEntityHistoryUpdated:ci})}function Rt(t){t.registerStores({moreInfoEntityId:Yr})}function Mt(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;o0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function Yt(t){var e=fo[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),fo[t.hassId]=!1)}function Jt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),Yt(t);var r=t.evaluate(Mo.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",b(i,{authToken:r}).then((function(e){var r=Bt((function(){return e.ping()}),so);r(),e.socket.addEventListener("message",r),fo[t.hassId]={conn:e,scheduleHealthCheck:r},co.forEach((function(n){return e.subscribeEvents(ao.bind(null,t),n)})),t.batch((function(){t.dispatch(Ye.STREAM_START),n&&io.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(Ye.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(Ye.STREAM_START),io.fetchAll(t)}))}))}))}function Wt(t){t.registerStores({streamStatus:Xe})}function Xt(t,e,n){void 0===n&&(n={});var r=n.rememberAuth;void 0===r&&(r=!1);var i=n.host;void 0===i&&(i=""),t.dispatch(Ue.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),io.fetchAll(t).then((function(){t.dispatch(Ue.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),vo.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=mo),t.dispatch(Ue.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Qt(t){t.dispatch(Ue.LOG_OUT,{})}function Zt(t){t.registerStores({authAttempt:Ve,authCurrent:Ge,rememberAuth:Be})}function $t(){if(!("localStorage"in window))return{};var t=window.localStorage,e="___test";try{return t.setItem(e,e),t.removeItem(e),t}catch(t){return{}}}function te(){var t=new Uo({debug:!1});return t.hassId=Ho++,t}function ee(t,e,n){Object.keys(n).forEach((function(r){var i=n[r];if("register"in i&&i.register(e),"getters"in i&&Object.defineProperty(t,r+"Getters",{value:i.getters,enumerable:!0}),"actions"in i){var o={};Object.getOwnPropertyNames(i.actions).forEach((function(t){"function"==typeof i.actions[t]&&Object.defineProperty(o,t,{value:i.actions[t].bind(null,e),enumerable:!0})})),Object.defineProperty(t,r+"Actions",{value:o,enumerable:!0})}}))}function ne(t,e){return xo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function re(t){return on(t,"GET","error_log")}function ie(t,e){var n=e.date;return n.toISOString()}function oe(){return Jo.getInitialState()}function ue(t,e){var n=e.date,r=e.entries;return t.set(n,eu(r.map($o.fromJSON)))}function ae(){return nu.getInitialState()}function se(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function ce(){return ou.getInitialState()}function fe(t,e){t.dispatch(Bo.LOGBOOK_DATE_SELECTED,{date:e})}function he(t,e){t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),on(t,"GET","logbook/"+e).then((function(n){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function le(t){return!t||(new Date).getTime()-t>su}function pe(t){t.registerStores({currentLogbookDate:Jo,isLoadingLogbookEntries:Xo,logbookEntries:nu,logbookEntriesUpdated:ou})}function _e(t){return t.set("active",!0)}function de(t){return t.set("active",!1)}function ve(){return Su.getInitialState()}function ye(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",on(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(yu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),Vn.createNotification(t,n),!1}))}function ge(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return on(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(yu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Vn.createNotification(t,n),!1}))}function me(t){t.registerStores({pushNotifications:Su})}function Se(t,e){return on(t,"POST","template",{template:e})}function Ee(t){return t.set("isListening",!0)}function be(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function Ie(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function Oe(){return Nu.getInitialState()}function we(){return Nu.getInitialState()}function Te(){return Nu.getInitialState()}function Ae(t){return Pu[t.hassId]}function Ce(t){var e=Ae(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(Lu.VOICE_TRANSMITTING,{finalTranscript:n}),tr.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(Lu.VOICE_DONE)}),(function(){t.dispatch(Lu.VOICE_ERROR)}))}}function De(t){var e=Ae(t);e&&(e.recognition.stop(),Pu[t.hassId]=!1)}function ze(t){Ce(t),De(t)}function Re(t){var e=ze.bind(null,t);e();var n=new webkitSpeechRecognition;Pu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(Lu.VOICE_START)},n.onerror=function(){return t.dispatch(Lu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ae(t);if(n){for(var r="",i="",o=e.resultIndex;o>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?_(t)+e:e}function v(){return!0}function y(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function g(t,e){return S(t,e,0)}function m(t,e){return S(t,e,e)}function S(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[On]);if("function"==typeof e)return e}function C(t){return t&&"number"==typeof t.length}function D(t){return null===t||void 0===t?U():o(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?U().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function k(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function P(t){return!(!t||!t[Tn])}function U(){return An||(An=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new N(t).fromEntrySeq():O(t)?new k(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return C(t)?new L(t):w(t)?new N(t):O(t)?new k(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?B(e,t,"",{"":t}):Y(t)}function B(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map((function(n,r){return B(t,n,r,e)}))):J(e)?t.call(r,n,z(e).map((function(n,r){return B(t,n,r,e)}))):e}function Y(t){return Array.isArray(t)?R(t).map(Y).toList():J(t)?z(t).map(Y).toMap():t}function J(t){return t&&(t.constructor===Object||void 0===t.constructor)}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!s(t);if(c(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var h=!0,l=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,yn)):!W(t.get(r,yn),e))return h=!1,!1}));return h&&t.size===l}function Q(t,e){if(!(this instanceof Q))return new Q(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Cn)return Cn;Cn=this}}function Z(t,e){if(!t)throw new Error(e)}function $(t,e,n){if(!(this instanceof $))return new $(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>Pn?ut(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return st(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=xn[t];return void 0===e&&(e=at(t),Hn===Un&&(Hn=0,xn={}),Hn++,xn[t]=e),e}function at(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ht(t){return null===t||void 0===t?bt():lt(t)&&!c(t)?t:bt().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function lt(t){return!(!t||!t[Vn])}function pt(t,e){this.ownerID=t,this.entries=e}function _t(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function dt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&St(t._root)}function mt(t,e){return b(t,e[0],e[1])}function St(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(qn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function bt(){return Fn||(Fn=Et(0))}function It(t,e,n){var r,i;if(t._root){var o=f(gn),u=f(mn);if(r=Ot(t._root,t.__ownerID,0,void 0,e,n,o,u),!u.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new pt(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):bt()}function Ot(t,e,n,r,i,o,u,a){return t?t.update(e,n,r,i,o,u,a):o===yn?t:(h(a),h(u),new yt(e,r,[i,o]))}function wt(t){return t.constructor===yt||t.constructor===vt}function Tt(t,e,n,r,i){if(t.keyHash===r)return new vt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&vn,a=(0===n?r:r>>>n)&vn,s=u===a?[Tt(t,e,n+_n,r,i)]:(o=new yt(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new dt(t,o+1,u)}function zt(t,e,r){for(var i=[],u=0;u>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function Nt(t,e,n,r){var i=r?t:p(t);return i[e]=n,i}function Pt(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&io?0:o-n,c=u-n;return c>dn&&(c=dn),function(){if(i===c)return Xn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>dn&&(f=dn),function(){for(;;){if(a){var t=a();if(t!==Xn)return t;a=null}if(c===f)return Xn;var o=e?--f:c++;a=n(s&&s[o],r-_n,i+(o<=t.size||e<0)return t.withMutations((function(t){e<0?Wt(t,e).set(0,n):Wt(t,0,e+1).set(e,n)}));e+=t._origin;var r=t._tail,i=t._root,o=f(mn);return e>=Qt(t._capacity)?r=Bt(r,t.__ownerID,0,e,n,o):i=Bt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):Ft(t._origin,t._capacity,t._level,i,r):t}function Bt(t,e,n,r,i,o){var u=r>>>n&vn,a=t&&u0){var c=t&&t.array[u],f=Bt(c,e,n-_n,r,i,o);return f===c?t:(s=Yt(t,e),s.array[u]=f,s)}return a&&t.array[u]===i?t:(h(o),s=Yt(t,e),void 0===i&&u===s.array.length-1?s.array.pop():s.array[u]=i,s)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Vt(t?t.array.slice():[],e)}function Jt(t,e){if(e>=Qt(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&vn],r-=_n;return n}}function Wt(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var r=t.__ownerID||new l,i=t._origin,o=t._capacity,u=i+e,a=void 0===n?o:n<0?o+n:i+n;if(u===i&&a===o)return t;if(u>=a)return t.clear();for(var s=t._level,c=t._root,f=0;u+f<0;)c=new Vt(c&&c.array.length?[void 0,c]:[],r),s+=_n,f+=1<=1<h?new Vt([],r):_;if(_&&p>h&&u_n;y-=_n){var g=h>>>y&vn;v=v.array[g]=Yt(v.array[g],r)}v.array[h>>>_n&vn]=_}if(a=p)u-=p,a-=p,s=_n,c=null,d=d&&d.removeBefore(r,0,u);else if(u>i||p>>s&vn;if(m!==p>>>s&vn)break;m&&(f+=(1<i&&(c=c.removeBefore(r,s,u-f)),c&&pu&&(u=c.size),o(s)||(c=c.map((function(t){return K(t)}))),i.push(c)}return u>t.size&&(t=t.setSize(u)),Lt(t,e,i)}function Qt(t){return t>>_n<<_n}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function $t(t){return lt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Qn||(Qn=te(bt(),Gt()))}function ne(t,e,n){var r,i,o=t._map,u=t._list,a=o.get(e),s=void 0!==a;if(n===yn){if(!s)return t;u.size>=dn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function ae(t){var e=Ce(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=De,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?Sn:En,n)},e}function se(t,e,n){var r=Ce(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function ce(t,e){var n=Ce(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=De,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function fe(t,e,n,r){var i=Ce(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function he(t,e,n){var r=ht().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function le(t,e,n){var r=u(t),i=(c(t)?Zt():ht()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Ae(t);return i.map((function(e){return Oe(t,o(e))}))}function pe(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n===1/0?n=i:n|=0),y(e,n,i))return t;var o=g(e,i),u=m(n,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,n,r);var a,s=u-o;s===s&&(a=s<0?0:s);var c=Ce(t);return c.size=0===a?a:t.size&&a||void 0,!r&&P(t)&&a>=0&&(c.get=function(e,n){return e=d(this,e),e>=0&&ea)return I();var t=i.next();return r||e===En?t:e===Sn?b(e,s-1,void 0,t):b(e,s-1,t.value[1],t)})},c}function _e(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function de(t,e,n,r){var i=Ce(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===Sn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function ve(t,e){var r=u(t),i=[t].concat(e).map((function(t){return o(t)?r&&(t=n(t)):t=r?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===i.length)return t;if(1===i.length){var s=i[0];if(s===t||r&&u(s)||a(t)&&a(s))return s}var c=new L(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),c}function ye(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){function u(t,c){var f=this;t.__iterate((function(t,i){return(!e||c0}function Ie(t,n,r){var i=Ce(t);return i.size=new L(r).map((function(t){return t.size})).min(),i.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(En,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},i.__iteratorUncached=function(t,i){var o=r.map((function(t){return t=e(t),T(i?t.reverse():t)})),u=0,a=!1;return new E(function(){var e;return a||(e=o.map((function(t){return t.next()})),a=e.some((function(t){return t.done}))),a?I():b(t,u++,n.apply(null,e.map((function(t){return t.value}))))})},i}function Oe(t,e){return P(t)?e:t.constructor(e)}function we(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Te(t){return ft(t.size),_(t)}function Ae(t){return u(t)?n:a(t)?r:i}function Ce(t){return Object.create((u(t)?z:a(t)?R:M).prototype)}function De(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):D.prototype.cacheResult.call(this)}function ze(t,e){return t>e?1:te?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=u(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0});return un(i,r)}function un(t,e){return e=Rn(e,3432918353),e=Rn(e<<15|e>>>-15,461845907),e=Rn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Rn(e^e>>>16,2246822507),e=Rn(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=a,e.isAssociative=s,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",fn="@@__IMMUTABLE_KEYED__@@",hn="@@__IMMUTABLE_INDEXED__@@",ln="@@__IMMUTABLE_ORDERED__@@",pn="delete",_n=5,dn=1<<_n,vn=dn-1,yn={},gn={value:!1},mn={value:!1},Sn=0,En=1,bn=2,In="function"==typeof Symbol&&Symbol.iterator,On="@@iterator",wn=In||On;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=Sn,E.VALUES=En,E.ENTRIES=bn,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(D,e),D.of=function(){return D(arguments)},D.prototype.toSeq=function(){return this},D.prototype.toString=function(){return this.__toString("Seq {","}")},D.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},D.prototype.__iterate=function(t,e){return F(this,t,e,!0)},D.prototype.__iterator=function(t,e){return G(this,t,e,!0)},t(z,D),z.prototype.toKeyedSeq=function(){return this},t(R,D),R.of=function(){return R(arguments)},R.prototype.toIndexedSeq=function(){return this},R.prototype.toString=function(){return this.__toString("Seq [","]")},R.prototype.__iterate=function(t,e){return F(this,t,e,!1)},R.prototype.__iterator=function(t,e){return G(this,t,e,!1)},t(M,D),M.of=function(){return M(arguments)},M.prototype.toSetSeq=function(){return this},D.isSeq=P,D.Keyed=z,D.Set=M,D.Indexed=R;var Tn="@@__IMMUTABLE_SEQ__@@";D.prototype[Tn]=!0,t(L,R),L.prototype.get=function(t,e){return this.has(t)?this._array[d(this,t)]:e},L.prototype.__iterate=function(t,e){for(var n=this,r=this._array,i=r.length-1,o=0;o<=i;o++)if(t(r[e?i-o:o],o,n)===!1)return o+1;return o},L.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},j.prototype[ln]=!0,t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},k.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(Q,R),Q.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Q.prototype.get=function(t,e){return this.has(t)?this._value:e},Q.prototype.includes=function(t){return W(this._value,t)},Q.prototype.slice=function(t,e){var n=this.size;return y(t,e,n)?this:new Q(this._value,m(e,n)-g(t,n))},Q.prototype.reverse=function(){return this},Q.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Q.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Q.prototype.__iterate=function(t,e){for(var n=this,r=0;r=0&&e=0&&nn?I():b(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:X(this,t)};var Dn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var zn,Rn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Mn=Object.isExtensible,Ln=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),jn="function"==typeof WeakMap;jn&&(zn=new WeakMap);var kn=0,Nn="__immutablehash__";"function"==typeof Symbol&&(Nn=Symbol(Nn));var Pn=16,Un=255,Hn=0,xn={};t(ht,et),ht.of=function(){var t=sn.call(arguments,0);return bt().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},ht.prototype.toString=function(){return this.__toString("Map {","}")},ht.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ht.prototype.set=function(t,e){return It(this,t,e)},ht.prototype.setIn=function(t,e){return this.updateIn(t,yn,(function(){return e}))},ht.prototype.remove=function(t){return It(this,t,yn)},ht.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return yn}))},ht.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},ht.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=jt(this,Re(t),e,n);return r===yn?void 0:r},ht.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):bt()},ht.prototype.merge=function(){return zt(this,void 0,arguments)},ht.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return zt(this,t,e)},ht.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},ht.prototype.mergeDeep=function(){return zt(this,Rt,arguments)},ht.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return zt(this,Mt(t),e)},ht.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},ht.prototype.sort=function(t){return Zt(Se(this,t))},ht.prototype.sortBy=function(t,e){return Zt(Se(this,e,t))},ht.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ht.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new l)},ht.prototype.asImmutable=function(){return this.__ensureOwner()},ht.prototype.wasAltered=function(){return this.__altered},ht.prototype.__iterator=function(t,e){return new gt(this,t,e)},ht.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},ht.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ht.isMap=lt;var Vn="@@__IMMUTABLE_MAP__@@",qn=ht.prototype;qn[Vn]=!0,qn[pn]=qn.remove,qn.removeIn=qn.deleteIn,pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Gn)return At(t,s,r,i);var _=t&&t===this.ownerID,d=_?s:p(s);return l?a?c===f-1?d.pop():d[c]=d.pop():d[c]=[r,i]:d.push([r,i]),_?(this.entries=d,this):new pt(t,d)}},_t.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&vn),o=this.bitmap;return 0===(o&i)?r:this.nodes[kt(o&i-1)].get(t+_n,e,n,r)},_t.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=1<=Kn)return Dt(t,l,c,a,_);if(f&&!_&&2===l.length&&wt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&wt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?Nt(l,h,_,d):Ut(l,h,d):Pt(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new _t(t,v,y)},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&vn,o=this.nodes[i];return o?o.get(t+_n,e,n,r):r},dt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=i===yn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Ot(f,t,e+_n,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&vn;if(r>=this.array.length)return new Vt([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-_n,n),i===u&&o)return this}if(o&&!i)return this;var a=Yt(this,t);if(!o)for(var s=0;s>>e&vn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-_n,n),i===o&&r===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Wn,Xn={};t(Zt,ht),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=$t,Zt.prototype[ln]=!0,Zt.prototype[pn]=Zt.prototype.remove;var Qn;t(re,z),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=se(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Te(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Te(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},re.prototype[ln]=!0,t(ie,R),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(oe,M),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ue,z),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){we(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ue.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){we(r);var i=o(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=De,t(Me,et),Me.prototype.toString=function(){return this.__toString(je(this)+" {","}")},Me.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Me.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Me.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Le(this,bt()))},Me.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+je(this));if(this._map&&!this._map.has(t)){var n=this._defaultValues[t];if(e===n)return this}var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:Le(this,r)},Me.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Le(this,e)},Me.prototype.wasAltered=function(){return this._map.wasAltered()},Me.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Me.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Me.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Le(this,e,t):(this.__ownerID=t,this._map=e,this)};var Zn=Me.prototype;Zn[pn]=Zn.remove,Zn.deleteIn=Zn.removeIn=qn.removeIn,Zn.merge=qn.merge,Zn.mergeWith=qn.mergeWith,Zn.mergeIn=qn.mergeIn,Zn.mergeDeep=qn.mergeDeep,Zn.mergeDeepWith=qn.mergeDeepWith,Zn.mergeDeepIn=qn.mergeDeepIn,Zn.setIn=qn.setIn,Zn.update=qn.update,Zn.updateIn=qn.updateIn,Zn.withMutations=qn.withMutations,Zn.asMutable=qn.asMutable,Zn.asImmutable=qn.asImmutable,t(Pe,rt),Pe.of=function(){return this(arguments)},Pe.fromKeys=function(t){return this(n(t).keySeq())},Pe.prototype.toString=function(){return this.__toString("Set {","}")},Pe.prototype.has=function(t){return this._map.has(t)},Pe.prototype.add=function(t){ -return He(this,this._map.set(t,!0))},Pe.prototype.remove=function(t){return He(this,this._map.remove(t))},Pe.prototype.clear=function(){return He(this,this._map.clear())},Pe.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ft(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pop=function(){return this.slice(1)},Be.prototype.unshift=function(){return this.push.apply(this,arguments)},Be.prototype.unshiftAll=function(t){return this.pushAll(t)},Be.prototype.shift=function(){return this.pop.apply(this,arguments)},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):We()},Be.prototype.slice=function(t,e){if(y(t,e,this.size))return this;var n=g(t,this.size),r=m(e,this.size);if(r!==this.size)return nt.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Je(i,o)},Be.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Je(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Be.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},Be.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Be.isStack=Ye;var ir="@@__IMMUTABLE_STACK__@@",or=Be.prototype;or[ir]=!0,or.withMutations=qn.withMutations,or.asMutable=qn.asMutable,or.asImmutable=qn.asImmutable,or.wasAltered=qn.wasAltered;var ur;e.Iterator=E,Xe(e,{toArray:function(){ft(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,n){t[n]=e})),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return ht(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate((function(e,n){t[n]=e})),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return qe(u(this)?this.valueSeq():this)},toSet:function(){return Pe(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Be(u(this)?this.valueSeq():this)},toList:function(){return Ht(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sn.call(arguments,0);return Oe(this,ve(this,t))},includes:function(t){return this.some((function(e){return W(e,t)}))},entries:function(){return this.__iterator(bn)},every:function(t,e){ft(this.size);var n=!0;return this.__iterate((function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1})),n},filter:function(t,e){return Oe(this,fe(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate((function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""})),e},keys:function(){return this.__iterator(Sn)},map:function(t,e){return Oe(this,se(this,t,e))},reduce:function(t,e,n){ft(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate((function(e,o,u){i?(i=!1,r=e):r=t.call(n,r,e,o,u)})),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Oe(this,ce(this,!0))},slice:function(t,e){return Oe(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return Oe(this,Se(this,t))},values:function(){return this.__iterator(En)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return _(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return he(this,t,e)},equals:function(t){return X(this,t)},entrySeq:function(){var t=this;if(t._cache)return new L(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter($e(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate((function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1})),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(v)},flatMap:function(t,e){return Oe(this,ge(this,t,e))},flatten:function(t){return Oe(this,ye(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find((function(e,n){return W(n,t)}),void 0,e)},getIn:function(t,e){for(var n,r=this,i=Re(t);!(n=i.next()).done;){var o=n.value;if(r=r&&r.get?r.get(o,yn):yn,r===yn)return e}return r},groupBy:function(t,e){return le(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey((function(e){return W(e,t)}))},keySeq:function(){return this.toSeq().map(Qe).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return Oe(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return Oe(this,de(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return Oe(this,Se(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return Oe(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return Oe(this,_e(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var ar=e.prototype;ar[cn]=!0,ar[wn]=ar.values,ar.__toJS=ar.toArray,ar.__toStringMapper=en,ar.inspect=ar.toSource=function(){return this.toString()},ar.chain=ar.flatMap,ar.contains=ar.includes,Xe(n,{flip:function(){return Oe(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return Oe(this,this.toSeq().map((function(i,o){return t.call(e,[o,i],r++,n)})).fromEntrySeq())},mapKeys:function(t,e){var n=this;return Oe(this,this.toSeq().flip().map((function(r,i){return t.call(e,r,i,n)})).flip())}});var sr=n.prototype;sr[fn]=!0,sr[wn]=ar.entries,sr.__toJS=ar.toObject,sr.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Xe(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return Oe(this,fe(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return Oe(this,ce(this,!1))},slice:function(t,e){return Oe(this,pe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=g(t,t<0?this.count():this.size);var r=this.slice(0,t);return Oe(this,1===n?r:r.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return Oe(this,ye(this,t,!1))},get:function(t,e){return t=d(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=d(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,m.toFactory)(E),t.exports=e.default},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new C({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return S(t,[n])}))})),m(t)}))}function u(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){var r=t.get("logger");if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var i=t.get("state"),o=t.get("dirtyStores"),u=i.withMutations((function(u){r.dispatchStart(t,e,n),t.get("stores").forEach((function(i,a){var s=u.get(a),c=void 0;try{c=i.handle(s,e,n)}catch(e){throw r.dispatchError(t,e.message),e}if(void 0===c&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw r.dispatchError(t,h),new Error(h)}u.set(a,c),s!==c&&(o=o.add(a))})),r.dispatchEnd(t,u,o,i)})),a=t.set("state",u).set("dirtyStores",o).update("storeStates",(function(t){return S(t,o)}));return m(a)}function s(t,e){var n=[],r=(0,O.toImmutable)({}).withMutations((function(r){(0,A.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=b.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return S(t,n)}))}function c(t,e,n){var r=e;(0,T.isKeyPath)(e)&&(e=(0,w.fromKeyPath)(e));var i=t.get("nextId"),o=(0,w.getStoreDeps)(e),u=b.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,b.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,T.isKeyPath)(e)&&(0,T.isKeyPath)(r)?(0,T.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return S(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,T.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,w.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");var r=t.get("cache"),o=r.lookup(e),u=!o||y(t,o);return u&&(o=g(t,e)),i(o.get("value"),t.update("cache",(function(t){return u?t.miss(e,o):t.hit(e)})))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",b.default.Set())}function y(t,e){var n=e.get("storeStates");return!n.size||n.some((function(e,n){return t.getIn(["storeStates",n])!==e}))}function g(t,e){var n=(0,w.getDeps)(e).map((function(e){return _(t,e).result})),r=(0,w.getComputeFn)(e).apply(null,n),i=(0,w.getStoreDeps)(e),o=(0,O.toImmutable)({}).withMutations((function(e){i.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return(0,I.CacheEntry)({value:r,storeStates:o,dispatchId:t.get("dispatchId")})}function m(t){return t.update("dispatchId",(function(t){return t+1}))}function S(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var E=n(3),b=r(E),I=n(9),O=n(5),w=n(10),T=n(11),A=n(4),C=b.default.Record({result:null,reactorState:null})},function(t,e,n){function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){return new s}Object.defineProperty(e,"__esModule",{value:!0});var o=(function(){function t(t,e){for(var n=0;nn.dispatchId)throw new Error("Refusing to cache older value");return n})))}},{key:"evict",value:function(e){return new t(this.cache.remove(e))}}]),t})();e.BasicCache=s;var c=1e3,f=1,h=(function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1],i=arguments.length<=2||void 0===arguments[2]?new s:arguments[2],o=arguments.length<=3||void 0===arguments[3]?(0,u.OrderedSet)():arguments[3];r(this,t),console.log("using LRU"),this.limit=e,this.evictCount=n,this.cache=i,this.lru=o}return o(t,[{key:"lookup",value:function(t,e){return this.cache.lookup(t,e)}},{key:"has",value:function(t){return this.cache.has(t)}},{key:"asMap",value:function(){return this.cache.asMap()}},{key:"hit",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache,this.lru.remove(e).add(e)):this}},{key:"miss",value:function(e,n){var r;if(this.lru.size>=this.limit){if(this.has(e))return new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.remove(e).add(e));var i=this.lru.take(this.evictCount).reduce((function(t,e){return t.evict(e)}),this.cache).miss(e,n);r=new t(this.limit,this.evictCount,i,this.lru.skip(this.evictCount).add(e))}else r=new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.add(e));return r}},{key:"evict",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache.evict(e),this.lru.remove(e)):this}}]),t})();e.LRUCache=h},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(8),i={dispatchStart:function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},dispatchError:function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},dispatchEnd:function(t,e,n,i){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}};e.ConsoleGroupLogger=i;var o={dispatchStart:function(t,e,n){},dispatchError:function(t,e){},dispatchEnd:function(t,e,n){}};e.NoopLogger=o},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n(9),o=n(12),u=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=u;var a=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=a;var s=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,i.DefaultCache)(),logger:o.NoopLogger,storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:u});e.ReactorState=s;var c=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=c}])}))})),ke=t(je),Ne=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},Pe=Ne,Ue=Pe({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),He=ke.Store,xe=ke.toImmutable,Ve=new He({getInitialState:function(){return xe({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Ue.VALIDATING_AUTH_TOKEN,n),this.on(Ue.VALID_AUTH_TOKEN,r),this.on(Ue.INVALID_AUTH_TOKEN,i)}}),qe=ke.Store,Fe=ke.toImmutable,Ge=new qe({getInitialState:function(){return Fe({authToken:null,host:""})},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,o),this.on(Ue.LOG_OUT,u)}}),Ke=ke.Store,Be=new Ke({getInitialState:function(){return!0},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,a)}}),Ye=Pe({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Je=ke.Store,We=ke.toImmutable,Xe=new Je({getInitialState:function(){return We({isStreaming:!1,hasError:!1})},initialize:function(){this.on(Ye.STREAM_START,s),this.on(Ye.STREAM_ERROR,c),this.on(Ye.LOG_OUT,f)}}),Qe=1,Ze=2,$e=3,tn=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};tn.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},tn.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},tn.prototype.connect=function(){var t=this;return new Promise(function(e,n){var r=t.commands;Object.keys(r).forEach((function(t){var e=r[t];e.reject&&e.reject(S($e,"Connection lost"))}));var i=!1;t.connectionTries+=1,t.socket=new WebSocket(t.url),t.socket.addEventListener("open",(function(){t.connectionTries=0})),t.socket.addEventListener("message",(function(o){var u=JSON.parse(o.data);switch(u.type){case"event":t.commands[u.id].eventCallback(u.event);break;case"result":u.success?t.commands[u.id].resolve(u):t.commands[u.id].reject(u.error), -delete t.commands[u.id];break;case"pong":break;case"auth_required":t.sendMessage(h(t.options.authToken));break;case"auth_invalid":n(Ze),i=!0;break;case"auth_ok":e(t),t.fireEvent("ready"),t.commandId=1,t.commands={},Object.keys(r).forEach((function(e){var n=r[e];n.eventType&&t.subscribeEvents(n.eventCallback,n.eventType).then((function(t){n.unsubscribe=t}))}))}})),t.socket.addEventListener("close",(function(){if(!i&&!t.closeRequested){0===t.connectionTries?t.fireEvent("disconnected"):n(Qe);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},tn.prototype.close=function(){this.closeRequested=!0,this.socket.close()},tn.prototype.getStates=function(){return this.sendMessagePromise(l()).then(E)},tn.prototype.getServices=function(){return this.sendMessagePromise(_()).then(E)},tn.prototype.getPanels=function(){return this.sendMessagePromise(d()).then(E)},tn.prototype.getConfig=function(){return this.sendMessagePromise(p()).then(E)},tn.prototype.callService=function(t,e,n){return this.sendMessagePromise(v(t,e,n))},tn.prototype.subscribeEvents=function(t,e){var n=this;return this.sendMessagePromise(y(e)).then((function(r){var i={eventCallback:t,eventType:e,unsubscribe:function(){return n.sendMessagePromise(g(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},tn.prototype.ping=function(){return this.sendMessagePromise(m())},tn.prototype.sendMessage=function(t){this.socket.send(JSON.stringify(t))},tn.prototype.sendMessagePromise=function(t){var e=this;return new Promise(function(n,r){e.commandId+=1;var i=e.commandId;t.id=i,e.commands[i]={resolve:n,reject:r},e.sendMessage(t)})};var en=Pe({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),nn=ke.Store,rn=new nn({getInitialState:function(){return!0},initialize:function(){this.on(en.API_FETCH_ALL_START,(function(){return!0})),this.on(en.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(en.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(en.LOG_OUT,(function(){return!1}))}}),on=I,un=Pe({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),an=ke.Store,sn=ke.toImmutable,cn=new an({getInitialState:function(){return sn({})},initialize:function(){var t=this;this.on(un.API_FETCH_SUCCESS,O),this.on(un.API_SAVE_SUCCESS,O),this.on(un.API_DELETE_SUCCESS,w),this.on(un.LOG_OUT,(function(){return t.getInitialState()}))}}),fn=Object.prototype.hasOwnProperty,hn=Object.prototype.propertyIsEnumerable,ln=A()?Object.assign:function(t,e){for(var n,r,i=arguments,o=T(t),u=1;u199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function O(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?sn({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||sn;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function mt(t,e){var n=e.date;return n.toISOString()}function gt(){return Qr.getInitialState()}function St(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,$r({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],$r(e.map(In.fromJSON)))}))}))}function bt(){return ti.getInitialState()}function Et(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,ii(e.map(In.fromJSON)))}))}))}function It(){return oi.getInitialState()}function Ot(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(si,r)}))}function wt(){return ci.getInitialState()}function Tt(t,e){t.dispatch(Wr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function At(t,e){void 0===e&&(e=null),t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),on(t,"GET",n).then((function(e){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function Dt(t,e){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_START,{date:e}),on(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function Ct(t){var e=t.evaluate(li);return Dt(t,e)}function zt(t){t.registerStores({currentEntityHistoryDate:Qr,entityHistory:ti,isLoadingEntityHistory:ni,recentEntityHistory:oi,recentEntityHistoryUpdated:ci})}function Rt(t){t.registerStores({moreInfoEntityId:Yr})}function Mt(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;o0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function Yt(t){var e=fo[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),fo[t.hassId]=!1)}function Jt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),Yt(t);var r=t.evaluate(Mo.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",E(i,{authToken:r}).then((function(e){var r=Bt((function(){return e.ping()}),so);r(),e.socket.addEventListener("message",r),fo[t.hassId]={conn:e,scheduleHealthCheck:r},co.forEach((function(n){return e.subscribeEvents(ao.bind(null,t),n)})),t.batch((function(){t.dispatch(Ye.STREAM_START),n&&io.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(Ye.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(Ye.STREAM_START),io.fetchAll(t)}))}))}))}function Wt(t){t.registerStores({streamStatus:Xe})}function Xt(t,e,n){void 0===n&&(n={});var r=n.rememberAuth;void 0===r&&(r=!1);var i=n.host;void 0===i&&(i=""),t.dispatch(Ue.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),io.fetchAll(t).then((function(){t.dispatch(Ue.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),vo.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=go),t.dispatch(Ue.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Qt(t){t.dispatch(Ue.LOG_OUT,{})}function Zt(t){t.registerStores({authAttempt:Ve,authCurrent:Ge,rememberAuth:Be})}function $t(){if(!("localStorage"in window))return{};var t=window.localStorage,e="___test";try{return t.setItem(e,e),t.removeItem(e),t}catch(t){return{}}}function te(){var t=new Uo({debug:!1});return t.hassId=Ho++,t}function ee(t,e,n){Object.keys(n).forEach((function(r){var i=n[r];if("register"in i&&i.register(e),"getters"in i&&Object.defineProperty(t,r+"Getters",{value:i.getters,enumerable:!0}),"actions"in i){var o={};Object.getOwnPropertyNames(i.actions).forEach((function(t){"function"==typeof i.actions[t]&&Object.defineProperty(o,t,{value:i.actions[t].bind(null,e),enumerable:!0})})),Object.defineProperty(t,r+"Actions",{value:o,enumerable:!0})}}))}function ne(t,e){return xo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function re(t){return on(t,"GET","error_log")}function ie(t,e){var n=e.date;return n.toISOString()}function oe(){return Jo.getInitialState()}function ue(t,e){var n=e.date,r=e.entries;return t.set(n,eu(r.map($o.fromJSON)))}function ae(){return nu.getInitialState()}function se(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function ce(){return ou.getInitialState()}function fe(t,e){t.dispatch(Bo.LOGBOOK_DATE_SELECTED,{date:e})}function he(t,e){t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),on(t,"GET","logbook/"+e).then((function(n){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function le(t){return!t||(new Date).getTime()-t>su}function pe(t){t.registerStores({currentLogbookDate:Jo,isLoadingLogbookEntries:Xo,logbookEntries:nu,logbookEntriesUpdated:ou})}function _e(t){return t.set("active",!0)}function de(t){return t.set("active",!1)}function ve(){return Su.getInitialState()}function ye(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",on(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(yu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),Vn.createNotification(t,n),!1}))}function me(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return on(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(yu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Vn.createNotification(t,n),!1}))}function ge(t){t.registerStores({pushNotifications:Su})}function Se(t,e){return on(t,"POST","template",{template:e})}function be(t){return t.set("isListening",!0)}function Ee(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function Ie(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function Oe(){return ku.getInitialState()}function we(){return ku.getInitialState()}function Te(){return ku.getInitialState()}function Ae(t){return Pu[t.hassId]}function De(t){var e=Ae(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(Lu.VOICE_TRANSMITTING,{finalTranscript:n}),tr.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(Lu.VOICE_DONE)}),(function(){t.dispatch(Lu.VOICE_ERROR)}))}}function Ce(t){var e=Ae(t);e&&(e.recognition.stop(),Pu[t.hassId]=!1)}function ze(t){De(t),Ce(t)}function Re(t){var e=ze.bind(null,t);e();var n=new webkitSpeechRecognition;Pu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(Lu.VOICE_START)},n.onerror=function(){return t.dispatch(Lu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ae(t);if(n){for(var r="",i="",o=e.resultIndex;o=n)}function c(t,e){return h(t,e,0)}function f(t,e){return h(t,e,e)}function h(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function l(t){return v(t)?t:C(t)}function p(t){return y(t)?t:z(t)}function _(t){return m(t)?t:R(t)}function d(t){return v(t)&&!g(t)?t:M(t)}function v(t){return!(!t||!t[dn])}function y(t){return!(!t||!t[vn])}function m(t){return!(!t||!t[yn])}function g(t){return y(t)||m(t)}function S(t){return!(!t||!t[mn])}function b(t){this.next=t}function E(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(En&&t[En]||t[In]);if("function"==typeof e)return e}function D(t){return t&&"number"==typeof t.length}function C(t){return null===t||void 0===t?U():v(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?U().toKeyedSeq():v(t)?y(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?U():v(t)?y(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():v(t)?y(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function N(t){this._iterable=t,this.size=t.length||t.size}function k(t){this._iterator=t,this._iteratorCache=[]}function P(t){return!(!t||!t[wn])}function U(){return Tn||(Tn=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new k(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return D(t)?new L(t):w(t)?new k(t):O(t)?new N(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new b(function(){var t=i[n?o-u:u];return u++>o?I():E(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(){throw TypeError("Abstract")}function B(){}function Y(){}function J(){}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){return e?Q(e,t,"",{"":t}):Z(t)}function Q(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map((function(n,r){return Q(t,n,r,e)}))):$(e)?t.call(r,n,z(e).map((function(n,r){return Q(t,n,r,e)}))):e}function Z(t){return Array.isArray(t)?R(t).map(Z).toList():$(t)?z(t).map(Z).toMap():t}function $(t){return t&&(t.constructor===Object||void 0===t.constructor)}function tt(t){return t>>>1&1073741824|3221225471&t}function et(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return tt(n)}return"string"===e?t.length>jn?nt(t):rt(t):"function"==typeof t.hashCode?t.hashCode():it(t)}function nt(t){var e=Pn[t];return void 0===e&&(e=rt(t),kn===Nn&&(kn=0,Pn={}),kn++,Pn[t]=e),e}function rt(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ut(t,e){if(!t)throw new Error(e)}function at(t){ut(t!==1/0,"Cannot perform this action with an infinite size.")}function st(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ct(t){this._iter=t,this.size=t.size}function ft(t){this._iter=t,this.size=t.size}function ht(t){this._iter=t,this.size=t.size}function lt(t){var e=Lt(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=jt,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new b(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===Sn?gn:Sn,n)},e}function pt(t,e,n){var r=Lt(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,ln);return o===ln?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new b(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return E(r,a,e.call(n,u[1],a,t),i)})},r}function _t(t,e){var n=Lt(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=lt(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=jt,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function dt(t,e,n,r){var i=Lt(t);return r&&(i.has=function(r){var i=t.get(r,ln);return i!==ln&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,ln);return o!==ln&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new b(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return E(i,r?c:a++,f,o)}})},i}function vt(t,e,n){var r=Pt().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function yt(t,e,n){var r=y(t),i=(S(t)?Ie():Pt()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Mt(t);return i.map((function(e){return Ct(t,o(e))}))}function mt(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n|=0),s(e,n,i))return t;var o=c(e,i),a=f(n,i);if(o!==o||a!==a)return mt(t.toSeq().cacheResult(),e,n,r);var h,l=a-o;l===l&&(h=l<0?0:l);var p=Lt(t);return p.size=0===h?h:t.size&&h||void 0,!r&&P(t)&&h>=0&&(p.get=function(e,n){return e=u(this,e),e>=0&&eh)return I();var t=i.next();return r||e===Sn?t:e===gn?E(e,a-1,void 0,t):E(e,a-1,t.value[1],t)})},p}function gt(t,e,n){var r=Lt(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new b(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:E(r,s,c,t):(a=!1,I())})},r}function St(t,e,n,r){var i=Lt(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new b(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===Sn?t:i===gn?E(i,c++,void 0,t):E(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:E(i,o,f,t)})},i}function bt(t,e){var n=y(t),r=[t].concat(e).map((function(t){return v(t)?n&&(t=p(t)):t=n?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===r.length)return t;if(1===r.length){var i=r[0];if(i===t||n&&y(i)||m(t)&&m(i))return i}var o=new L(r);return n?o=o.toKeyedSeq():m(t)||(o=o.toSetSeq()),o=o.flatten(!0),o.size=r.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),o}function Et(t,e,n){var r=Lt(t);return r.__iterateUncached=function(r,i){function o(t,s){var c=this;t.__iterate((function(t,i){return(!e||s0}function Dt(t,e,n){var r=Lt(t);return r.size=new L(n).map((function(t){return t.size})).min(),r.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(Sn,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},r.__iteratorUncached=function(t,r){var i=n.map((function(t){return t=l(t),T(r?t.reverse():t)})),o=0,u=!1; +return new b(function(){var n;return u||(n=i.map((function(t){return t.next()})),u=n.some((function(t){return t.done}))),u?I():E(t,o++,e.apply(null,n.map((function(t){return t.value}))))})},r}function Ct(t,e){return P(t)?e:t.constructor(e)}function zt(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Rt(t){return at(t.size),o(t)}function Mt(t){return y(t)?p:m(t)?_:d}function Lt(t){return Object.create((y(t)?z:m(t)?R:M).prototype)}function jt(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):C.prototype.cacheResult.call(this)}function Nt(t,e){return t>e?1:t>>n)&hn,a=(0===n?r:r>>>n)&hn,s=u===a?[Zt(t,e,n+cn,r,i)]:(o=new Ft(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new Vt(t,o+1,u)}function ne(t,e,n){for(var r=[],i=0;i>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function ae(t,e,n,r){var o=r?t:i(t);return o[e]=n,o}function se(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&ro?0:o-n,c=u-n;return c>fn&&(c=fn),function(){if(i===c)return Yn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>fn&&(f=fn),function(){for(;;){if(a){var t=a();if(t!==Yn)return t;a=null}if(c===f)return Yn;var o=e?--f:c++;a=n(s&&s[o],r-cn,i+(o<=t.size||n<0)return t.withMutations((function(t){n<0?Se(t,n).set(0,r):Se(t,0,n+1).set(n,r)}));n+=t._origin;var i=t._tail,o=t._root,a=e(_n);return n>=Ee(t._capacity)?i=ye(i,t.__ownerID,0,n,r,a):o=ye(o,t.__ownerID,t._level,n,r,a),a.value?t.__ownerID?(t._root=o,t._tail=i,t.__hash=void 0,t.__altered=!0,t):_e(t._origin,t._capacity,t._level,o,i):t}function ye(t,e,r,i,o,u){var a=i>>>r&hn,s=t&&a0){var f=t&&t.array[a],h=ye(f,e,r-cn,i,o,u);return h===f?t:(c=me(t,e),c.array[a]=h,c)}return s&&t.array[a]===o?t:(n(u),c=me(t,e),void 0===o&&a===c.array.length-1?c.array.pop():c.array[a]=o,c)}function me(t,e){return e&&t&&e===t.ownerID?t:new le(t?t.array.slice():[],e)}function ge(t,e){if(e>=Ee(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&hn],r-=cn;return n}}function Se(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var i=t.__ownerID||new r,o=t._origin,u=t._capacity,a=o+e,s=void 0===n?u:n<0?u+n:o+n;if(a===o&&s===u)return t;if(a>=s)return t.clear();for(var c=t._level,f=t._root,h=0;a+h<0;)f=new le(f&&f.array.length?[void 0,f]:[],i),c+=cn,h+=1<=1<l?new le([],i):_;if(_&&p>l&&acn;y-=cn){var m=l>>>y&hn;v=v.array[m]=me(v.array[m],i)}v.array[l>>>cn&hn]=_}if(s=p)a-=p,s-=p,c=cn,f=null,d=d&&d.removeBefore(i,0,a);else if(a>o||p>>c&hn;if(g!==p>>>c&hn)break;g&&(h+=(1<o&&(f=f.removeBefore(i,c,a-h)),f&&pi&&(i=a.size),v(u)||(a=a.map((function(t){return X(t)}))),r.push(a)}return i>t.size&&(t=t.setSize(i)),ie(t,e,r)}function Ee(t){return t>>cn<=fn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):we(r,i)}function De(t){return null===t||void 0===t?Re():Ce(t)?t:Re().unshiftAll(t)}function Ce(t){return!(!t||!t[Wn])}function ze(t,e,n,r){var i=Object.create(Xn);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Re(){return Qn||(Qn=ze(0))}function Me(t){return null===t||void 0===t?ke():Le(t)&&!S(t)?t:ke().withMutations((function(e){var n=d(t);at(n.size),n.forEach((function(t){return e.add(t)}))}))}function Le(t){return!(!t||!t[Zn])}function je(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function Ne(t,e){var n=Object.create($n);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function ke(){return tr||(tr=Ne(Jt()))}function Pe(t){return null===t||void 0===t?xe():Ue(t)?t:xe().withMutations((function(e){var n=d(t);at(n.size),n.forEach((function(t){return e.add(t)}))}))}function Ue(t){return Le(t)&&S(t)}function He(t,e){var n=Object.create(er);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function xe(){return nr||(nr=He(Te()))}function Ve(t,e){var n,r=function(o){if(o instanceof r)return o;if(!(this instanceof r))return new r(o);if(!n){n=!0;var u=Object.keys(t);Ge(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=Pt(o)},i=r.prototype=Object.create(rr);return i.constructor=r,r}function qe(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Fe(t){return t._name||t.constructor.name||"Record"}function Ge(t,e){try{e.forEach(Ke.bind(void 0,t))}catch(t){}}function Ke(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){ut(this.__ownerID,"Cannot set on an immutable record."),this.set(e,t)}})}function Be(t,e){if(t===e)return!0;if(!v(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||y(t)!==y(e)||m(t)!==m(e)||S(t)!==S(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!g(t);if(S(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var o=t;t=e,e=o}var u=!0,a=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,ln)):!W(t.get(r,ln),e))return u=!1,!1}));return u&&t.size===a}function Ye(t,e,n){if(!(this instanceof Ye))return new Ye(t,e,n);if(ut(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),ee?-1:0}function rn(t){if(t.size===1/0)return 0;var e=S(t),n=y(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+un(et(t),et(e))|0}:function(t,e){r=r+un(et(t),et(e))|0}:e?function(t){r=31*r+et(t)|0}:function(t){r=r+et(t)|0});return on(i,r)}function on(t,e){return e=Dn(e,3432918353),e=Dn(e<<15|e>>>-15,461845907),e=Dn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Dn(e^e>>>16,2246822507),e=Dn(e^e>>>13,3266489909),e=tt(e^e>>>16)}function un(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var an=Array.prototype.slice,sn="delete",cn=5,fn=1<r?I():E(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new b(function(){var u=r[e?i-o:o];return o++>i?I():E(t,u,n[u])})},j.prototype[mn]=!0,t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},N.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new b(I);var i=0;return new b(function(){var e=r.next();return e.done?e:E(t,i++,e.value)})},t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return E(t,i,r[i++])})};var Tn;t(K,l),t(B,K),t(Y,K),t(J,K),K.Keyed=B,K.Indexed=Y,K.Set=J;var An,Dn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Cn=Object.isExtensible,zn=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),Rn="function"==typeof WeakMap;Rn&&(An=new WeakMap);var Mn=0,Ln="__immutablehash__";"function"==typeof Symbol&&(Ln=Symbol(Ln));var jn=16,Nn=255,kn=0,Pn={};t(st,z),st.prototype.get=function(t,e){return this._iter.get(t,e)},st.prototype.has=function(t){return this._iter.has(t)},st.prototype.valueSeq=function(){return this._iter.valueSeq()},st.prototype.reverse=function(){var t=this,e=_t(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},st.prototype.map=function(t,e){var n=this,r=pt(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},st.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Rt(this):0,function(i){return t(i,e?--n:n++,r)}),e)},st.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(Sn,e),r=e?Rt(this):0;return new b(function(){var i=n.next();return i.done?i:E(t,e?--r:r++,i.value,i)})},st.prototype[mn]=!0,t(ct,R),ct.prototype.includes=function(t){return this._iter.includes(t)},ct.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ct.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Sn,e),r=0;return new b(function(){var e=n.next();return e.done?e:E(t,r++,e.value,e)})},t(ft,M),ft.prototype.has=function(t){return this._iter.includes(t)},ft.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},ft.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Sn,e);return new b(function(){var e=n.next();return e.done?e:E(t,e.value,e.value,e)})},t(ht,z),ht.prototype.entrySeq=function(){return this._iter.toSeq()},ht.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){zt(e);var r=v(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ht.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Sn,e);return new b(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){zt(r);var i=v(r);return E(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ct.prototype.cacheResult=st.prototype.cacheResult=ft.prototype.cacheResult=ht.prototype.cacheResult=jt,t(Pt,B),Pt.prototype.toString=function(){return this.__toString("Map {","}")},Pt.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},Pt.prototype.set=function(t,e){return Wt(this,t,e)},Pt.prototype.setIn=function(t,e){return this.updateIn(t,ln,(function(){return e}))},Pt.prototype.remove=function(t){return Wt(this,t,ln)},Pt.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return ln}))},Pt.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},Pt.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=oe(this,kt(t),e,n);return r===ln?void 0:r},Pt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Jt()},Pt.prototype.merge=function(){return ne(this,void 0,arguments)},Pt.prototype.mergeWith=function(t){var e=an.call(arguments,1);return ne(this,t,e)},Pt.prototype.mergeIn=function(t){var e=an.call(arguments,1);return this.updateIn(t,Jt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},Pt.prototype.mergeDeep=function(){return ne(this,re(void 0),arguments)},Pt.prototype.mergeDeepWith=function(t){var e=an.call(arguments,1);return ne(this,re(t),e)},Pt.prototype.mergeDeepIn=function(t){var e=an.call(arguments,1);return this.updateIn(t,Jt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},Pt.prototype.sort=function(t){return Ie(wt(this,t))},Pt.prototype.sortBy=function(t,e){return Ie(wt(this,e,t))},Pt.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},Pt.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new r)},Pt.prototype.asImmutable=function(){return this.__ensureOwner()},Pt.prototype.wasAltered=function(){return this.__altered},Pt.prototype.__iterator=function(t,e){return new Gt(this,t,e)},Pt.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},Pt.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Yt(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Pt.isMap=Ut;var Un="@@__IMMUTABLE_MAP__@@",Hn=Pt.prototype;Hn[Un]=!0,Hn[sn]=Hn.remove,Hn.removeIn=Hn.deleteIn,Ht.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Vn)return $t(t,f,o,u);var _=t&&t===this.ownerID,d=_?f:i(f);return p?c?h===l-1?d.pop():d[h]=d.pop():d[h]=[o,u]:d.push([o,u]),_?(this.entries=d,this):new Ht(t,d)}},xt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=1<<((0===t?e:e>>>t)&hn),o=this.bitmap;return 0===(o&i)?r:this.nodes[ue(o&i-1)].get(t+cn,e,n,r)},xt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&hn,s=1<=qn)return ee(t,l,c,a,_);if(f&&!_&&2===l.length&&Qt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&Qt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?ae(l,h,_,d):ce(l,h,d):se(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new xt(t,v,y)},Vt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=(0===t?e:e>>>t)&hn,o=this.nodes[i];return o?o.get(t+cn,e,n,r):r},Vt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&hn,s=i===ln,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Xt(f,t,e+cn,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&hn;if(r>=this.array.length)return new le([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-cn,n),i===u&&o)return this}if(o&&!i)return this;var a=me(this,t);if(!o)for(var s=0;s>>e&hn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-cn,n),i===o&&r===this.array.length-1)return this}var u=me(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Bn,Yn={};t(Ie,Pt),Ie.of=function(){return this(arguments)},Ie.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Ie.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Ie.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):Te()},Ie.prototype.set=function(t,e){return Ae(this,t,e)},Ie.prototype.remove=function(t){return Ae(this,t,ln)},Ie.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Ie.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Ie.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Ie.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?we(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Ie.isOrderedMap=Oe,Ie.prototype[mn]=!0,Ie.prototype[sn]=Ie.prototype.remove;var Jn;t(De,Y),De.of=function(){return this(arguments)},De.prototype.toString=function(){return this.__toString("Stack [","]")},De.prototype.get=function(t,e){var n=this._head;for(t=u(this,t);n&&t--;)n=n.next;return n?n.value:e},De.prototype.peek=function(){return this._head&&this._head.value},De.prototype.push=function(){var t=arguments;if(0===arguments.length)return this;for(var e=this.size+arguments.length,n=this._head,r=arguments.length-1;r>=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):ze(e,n)},De.prototype.pushAll=function(t){if(t=_(t),0===t.size)return this;at(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):ze(e,n)},De.prototype.pop=function(){return this.slice(1)},De.prototype.unshift=function(){return this.push.apply(this,arguments)},De.prototype.unshiftAll=function(t){return this.pushAll(t)},De.prototype.shift=function(){return this.pop.apply(this,arguments)},De.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Re()},De.prototype.slice=function(t,e){if(s(t,e,this.size))return this;var n=c(t,this.size),r=f(e,this.size);if(r!==this.size)return Y.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):ze(i,o)},De.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?ze(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},De.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},De.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new b(function(){if(r){var e=r.value;return r=r.next,E(t,n++,e)}return I()})},De.isStack=Ce;var Wn="@@__IMMUTABLE_STACK__@@",Xn=De.prototype;Xn[Wn]=!0,Xn.withMutations=Hn.withMutations,Xn.asMutable=Hn.asMutable,Xn.asImmutable=Hn.asImmutable,Xn.wasAltered=Hn.wasAltered;var Qn;t(Me,J),Me.of=function(){return this(arguments)},Me.fromKeys=function(t){return this(p(t).keySeq())},Me.prototype.toString=function(){return this.__toString("Set {","}")},Me.prototype.has=function(t){return this._map.has(t)},Me.prototype.add=function(t){return je(this,this._map.set(t,!0))},Me.prototype.remove=function(t){return je(this,this._map.remove(t))},Me.prototype.clear=function(){return je(this,this._map.clear())},Me.prototype.union=function(){var t=an.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n1?" by "+this._step:"")+" ]"},Ye.prototype.get=function(t,e){return this.has(t)?this._start+u(this,t)*this._step:e},Ye.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e=0&&nn?I():E(t,o++,u)})},Ye.prototype.equals=function(t){return t instanceof Ye?this._start===t._start&&this._end===t._end&&this._step===t._step:Be(this,t)};var ir;t(Je,R),Je.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Je.prototype.get=function(t,e){return this.has(t)?this._value:e},Je.prototype.includes=function(t){return W(this._value,t)},Je.prototype.slice=function(t,e){var n=this.size;return s(t,e,n)?this:new Je(this._value,f(e,n)-c(t,n))},Je.prototype.reverse=function(){return this},Je.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Je.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Je.prototype.__iterate=function(t,e){for(var n=this,r=0;rthis.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=u(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,y.toFactory)(g),t.exports=e.default},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new M({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,R.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,D.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return I(t,[n])}))})),E(t)}))}function u(t,e){return t.withMutations((function(t){(0,R.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var r=t.get("state"),i=t.get("dirtyStores"),o=r.withMutations((function(r){A.default.dispatchStart(t,e,n),t.get("stores").forEach((function(o,u){var a=r.get(u),s=void 0;try{s=o.handle(a,e,n)}catch(e){throw A.default.dispatchError(t,e.message),e}if(void 0===s&&f(t,"throwOnUndefinedStoreReturnValue")){var c="Store handler must return a value, did you forget a return statement";throw A.default.dispatchError(t,c),new Error(c)}r.set(u,s),a!==s&&(i=i.add(u))})),A.default.dispatchEnd(t,r,i)})),u=t.set("state",o).set("dirtyStores",i).update("storeStates",(function(t){return I(t,i)}));return E(u)}function s(t,e){var n=[],r=(0,D.toImmutable)({}).withMutations((function(r){(0,R.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=w.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return I(t,n)}))}function c(t,e,n){var r=e;(0,z.isKeyPath)(e)&&(e=(0,C.fromKeyPath)(e));var i=t.get("nextId"),o=(0,C.getStoreDeps)(e),u=w.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,w.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,z.isKeyPath)(e)&&(0,z.isKeyPath)(r)?(0,z.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,D.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return I(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,z.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,C.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");if(g(t,e))return i(b(t,e),t);var r=(0,C.getDeps)(e).map((function(e){return _(t,e).result})),o=(0,C.getComputeFn)(e).apply(null,r);return i(o,S(t,e,o))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",w.default.Set())}function y(t){return t}function m(t,e){var n=y(e);return t.getIn(["cache",n])}function g(t,e){var n=m(t,e);if(!n)return!1;var r=n.get("storeStates");return 0!==r.size&&r.every((function(e,n){return t.getIn(["storeStates",n])===e}))}function S(t,e,n){var r=y(e),i=t.get("dispatchId"),o=(0,C.getStoreDeps)(e),u=(0,D.toImmutable)({}).withMutations((function(e){o.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return t.setIn(["cache",r],w.default.Map({value:n,storeStates:u,dispatchId:i}))}function b(t,e){var n=y(e);return t.getIn(["cache",n,"value"])}function E(t){return t.update("dispatchId",(function(t){return t+1}))}function I(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var O=n(3),w=r(O),T=n(9),A=r(T),D=n(5),C=n(10),z=n(11),R=n(4),M=w.default.Record({result:null,reactorState:null})},function(t,e,n){var r=n(8);e.dispatchStart=function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},e.dispatchError=function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},e.dispatchEnd=function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=i;var o=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=o;var u=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,r.Map)(),storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:i});e.ReactorState=u;var a=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=a}])}))})),Ne=t(je),ke=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},Pe=ke,Ue=Pe({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),He=Ne.Store,xe=Ne.toImmutable,Ve=new He({getInitialState:function(){return xe({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Ue.VALIDATING_AUTH_TOKEN,n),this.on(Ue.VALID_AUTH_TOKEN,r),this.on(Ue.INVALID_AUTH_TOKEN,i)}}),qe=Ne.Store,Fe=Ne.toImmutable,Ge=new qe({getInitialState:function(){return Fe({authToken:null,host:""})},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,o),this.on(Ue.LOG_OUT,u)}}),Ke=Ne.Store,Be=new Ke({getInitialState:function(){return!0},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,a)}}),Ye=Pe({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Je=Ne.Store,We=Ne.toImmutable,Xe=new Je({getInitialState:function(){return We({isStreaming:!1,hasError:!1})},initialize:function(){this.on(Ye.STREAM_START,s),this.on(Ye.STREAM_ERROR,c),this.on(Ye.LOG_OUT,f)}}),Qe=1,Ze=2,$e=3,tn=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};tn.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},tn.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},tn.prototype.connect=function(){var t=this;return new Promise(function(e,n){var r=t.commands;Object.keys(r).forEach((function(t){var e=r[t];e.reject&&e.reject(S($e,"Connection lost"))}));var i=!1;t.connectionTries+=1,t.socket=new WebSocket(t.url),t.socket.addEventListener("open",(function(){t.connectionTries=0})),t.socket.addEventListener("message",(function(o){var u=JSON.parse(o.data);switch(u.type){case"event":t.commands[u.id].eventCallback(u.event);break;case"result":u.success?t.commands[u.id].resolve(u):t.commands[u.id].reject(u.error),delete t.commands[u.id];break;case"pong":break;case"auth_required":t.sendMessage(h(t.options.authToken));break;case"auth_invalid":n(Ze),i=!0;break;case"auth_ok":e(t),t.fireEvent("ready"),t.commandId=1,t.commands={},Object.keys(r).forEach((function(e){var n=r[e];n.eventType&&t.subscribeEvents(n.eventCallback,n.eventType).then((function(t){n.unsubscribe=t}))}))}})),t.socket.addEventListener("close",(function(){if(!i&&!t.closeRequested){0===t.connectionTries?t.fireEvent("disconnected"):n(Qe);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},tn.prototype.close=function(){this.closeRequested=!0,this.socket.close()},tn.prototype.getStates=function(){return this.sendMessagePromise(l()).then(b)},tn.prototype.getServices=function(){return this.sendMessagePromise(_()).then(b)},tn.prototype.getPanels=function(){return this.sendMessagePromise(d()).then(b)},tn.prototype.getConfig=function(){return this.sendMessagePromise(p()).then(b)},tn.prototype.callService=function(t,e,n){return this.sendMessagePromise(v(t,e,n))},tn.prototype.subscribeEvents=function(t,e){var n=this;return this.sendMessagePromise(y(e)).then((function(r){var i={eventCallback:t,eventType:e,unsubscribe:function(){return n.sendMessagePromise(m(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},tn.prototype.ping=function(){return this.sendMessagePromise(g())},tn.prototype.sendMessage=function(t){this.socket.send(JSON.stringify(t))},tn.prototype.sendMessagePromise=function(t){var e=this;return new Promise(function(n,r){e.commandId+=1;var i=e.commandId;t.id=i,e.commands[i]={resolve:n,reject:r},e.sendMessage(t)})};var en=Pe({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),nn=Ne.Store,rn=new nn({getInitialState:function(){return!0},initialize:function(){this.on(en.API_FETCH_ALL_START,(function(){return!0})),this.on(en.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(en.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(en.LOG_OUT,(function(){return!1}))}}),on=I,un=Pe({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),an=Ne.Store,sn=Ne.toImmutable,cn=new an({getInitialState:function(){return sn({})},initialize:function(){var t=this;this.on(un.API_FETCH_SUCCESS,O),this.on(un.API_SAVE_SUCCESS,O),this.on(un.API_DELETE_SUCCESS,w),this.on(un.LOG_OUT,(function(){return t.getInitialState()}))}}),fn=Object.prototype.hasOwnProperty,hn=Object.prototype.propertyIsEnumerable,ln=A()?Object.assign:function(t,e){for(var n,r,i=arguments,o=T(t),u=1;u \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 2e509569ab2..3708f52a280 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 2652823d35b..b5c3575cb5f 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 2652823d35b77411988751cc74820dcfc3a0e2ac +Subproject commit b5c3575cb5f284178e52d75db24c46131afb4cfa diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index 51805ebd91d..9ce5304213e 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index f6c74234e4d..e9007a00c93 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html index d63acba7549..38dd2b6e961 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html @@ -1,4 +1,180 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz index e153a5290fb..56a98ed3431 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 25535a72081..02760e32c34 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","71255a0807fe2c461c870c5f3aaedc56"],["/frontend/panels/dev-event-c2d5ec676be98d4474d19f94d0262c1e.html","6c55fc819751923ab00c62ae3fbb7222"],["/frontend/panels/dev-info-a9c07bf281fe9791fb15827ec1286825.html","931f9327e368db710fcdf5f7202f2588"],["/frontend/panels/dev-service-b3fe49532c5c03198fafb0c6ed58b76a.html","4194cb43b74108dc6d10354da2fd81fd"],["/frontend/panels/dev-state-65e5f791cc467561719bf591f1386054.html","78158786a6597ef86c3fd6f4985cde92"],["/frontend/panels/dev-template-7d744ab7f7c08b6d6ad42069989de400.html","8a6ee994b1cdb45b081299b8609915ed"],["/frontend/panels/map-1bf6965b24d76db71a1871865cd4a3a2.html","a74c01c2ee68c83c9938af067ec33b81"],["/static/core-5dfb2d3e567fad37af0321d4b29265ed.js","9a50270db7613e3af449f6c773366f4d"],["/static/frontend-6a89b74ab2b76c7d28fad2aea9444ec2.html","db91a94d9dcb88f12188d3d88c1200a2"],["/static/mdi-46a76f877ac9848899b8ed382427c16f.html","a846c4082dd5cffd88ac72cbe943e691"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.toString().match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a))return e.add(new Request(a,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e.delete(a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);var n="index.html";!t&&n&&(a=addDirectoryIndex(a,n),t=urlsToCacheKeys.has(a));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(a=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var a,n;for(a=0;a32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",l=0,u="",w=!1,_=!1,g=[];e:for(;(e[l-1]!=p||0==l)&&!this._isInvalid;){var b=e[l];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}u="",d="no scheme";continue}u+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))u+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}u="",l=0,d="no scheme";continue}if(this._scheme=u,u="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=r(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[l+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[l+1],E=e[l+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),u+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var o=t[this.name];return o&&o[0]===t?o[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(o))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function o(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();r(e),n.length&&(e.callback_(n,e),t=!0)}),t&&o()}function r(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var o=v.get(n);if(o)for(var r=0;r0){var r=n[o-1],i=f(r,e);if(i)return void(n[o-1]=i)}else t(this.observer);n[o]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n":return">";case" ":return" "}}function t(t){return t.replace(u,e)}var n="undefined"==typeof HTMLTemplateElement;/Trident/.test(navigator.userAgent)&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType===Node.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}();var o=function(){if(!n){var e=document.createElement("template"),t=document.createElement("template");t.content.appendChild(document.createElement("div")),e.content.appendChild(t);var o=e.cloneNode(!0);return 0===o.content.childNodes.length||0===o.content.firstChild.content.childNodes.length}}(),r="template",i=function(){};if(n){var a=document.implementation.createHTMLDocument("template"),s=!0,c=document.createElement("style");c.textContent=r+"{display:none;}";var d=document.head;d.insertBefore(c,d.firstElementChild),i.prototype=Object.create(HTMLElement.prototype),i.decorate=function(e){if(!e.content){e.content=a.createDocumentFragment();for(var n;n=e.firstChild;)e.content.appendChild(n);if(e.cloneNode=function(e){return i.cloneNode(this,e)},s)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(a.body.innerHTML=e,i.bootstrap(a);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;a.body.firstChild;)this.content.appendChild(a.body.firstChild)},configurable:!0})}catch(o){s=!1}i.bootstrap(e.content)}},i.bootstrap=function(e){for(var t,n=e.querySelectorAll(r),o=0,a=n.length;a>o&&(t=n[o]);o++)i.decorate(t)},document.addEventListener("DOMContentLoaded",function(){i.bootstrap(document)});var l=document.createElement;document.createElement=function(){"use strict";var e=l.apply(document,arguments);return"template"===e.localName&&i.decorate(e),e};var u=/[&\u00A0<>]/g}if(n||o){var h=Node.prototype.cloneNode;i.cloneNode=function(e,t){var n=h.call(e,!1);return this.decorate&&this.decorate(n),t&&(n.content.appendChild(h.call(e.content,!0)),this.fixClonedDom(n.content,e.content)),n},i.fixClonedDom=function(e,t){if(t.querySelectorAll)for(var n,o,i=t.querySelectorAll(r),a=e.querySelectorAll(r),s=0,c=a.length;c>s;s++)o=i[s],n=a[s],this.decorate&&this.decorate(o),n.parentNode.replaceChild(o.cloneNode(!0),n)};var f=document.importNode;Node.prototype.cloneNode=function(e){var t=h.call(this,e);return e&&i.fixClonedDom(t,this),t},document.importNode=function(e,t){if(e.localName===r)return i.cloneNode(e,t);var n=f.call(document,e,t);return t&&i.fixClonedDom(n,e),n},o&&(HTMLTemplateElement.prototype.cloneNode=function(e){return i.cloneNode(this,e)})}n&&(window.HTMLTemplateElement=i)}(),function(e){"use strict";if(!window.performance){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}());var n=function(){var e=document.createEvent("Event");return e.initEvent("foo",!0,!0),e.preventDefault(),e.defaultPrevented}();if(!n){var o=Event.prototype.preventDefault;Event.prototype.preventDefault=function(){this.cancelable&&(o.call(this),Object.defineProperty(this,"defaultPrevented",{get:function(){return!0},configurable:!0}))}}var r=/Trident/.test(navigator.userAgent);if((!window.CustomEvent||r&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n},window.CustomEvent.prototype=window.Event.prototype),!window.Event||r&&"function"!=typeof window.Event){var i=window.Event;window.Event=function(e,t){t=t||{};var n=document.createEvent("Event");return n.initEvent(e,Boolean(t.bubbles),Boolean(t.cancelable)),n},window.Event.prototype=i.prototype}}(window.WebComponents),window.HTMLImports=window.HTMLImports||{flags:{}},function(e){function t(e,t){t=t||p,o(function(){i(e,t)},t)}function n(e){return"complete"===e.readyState||e.readyState===w}function o(e,t){if(n(t))e&&e();else{var r=function(){"complete"!==t.readyState&&t.readyState!==w||(t.removeEventListener(_,r),o(e,t))};t.addEventListener(_,r)}}function r(e){e.target.__loaded=!0}function i(e,t){function n(){c==d&&e&&e({allImports:s,loadedImports:l,errorImports:u})}function o(e){r(e),l.push(this),c++,n()}function i(e){u.push(this),c++,n()}var s=t.querySelectorAll("link[rel=import]"),c=0,d=s.length,l=[],u=[];if(d)for(var h,f=0;d>f&&(h=s[f]);f++)a(h)?(l.push(this),c++,n()):(h.addEventListener("load",o),h.addEventListener("error",i));else n()}function a(e){return u?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?r({target:e}):(e.addEventListener("load",r),e.addEventListener("error",r))}var l="import",u=Boolean(l in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),f=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},p=f(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return f(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(p,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),w=v?"complete":"interactive",_="readystatechange";u&&(new MutationObserver(function(e){for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,o=t.length;o>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=p.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),p.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=l,e.useNative=u,e.rootDocument=p,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},o=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=o}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,o={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,o=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,o),e},resolveUrlsInCssText:function(e,o,r){var i=this.replaceUrls(e,r,o,t);return i=this.replaceUrls(i,r,o,n)},replaceUrls:function(e,t,n,o){return e.replace(o,function(e,o,r,i){var a=r.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,o+"'"+a+"'"+i})}};e.path=o}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,o,r){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}o.call(r,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,o=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};o.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,o){if(n.load&&console.log("fetch",e,o),e)if(e.match(/^data:/)){var r=e.split(","),i=r[0],a=r[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,o,null,a)}.bind(this),0)}else{var s=function(t,n,r){this.receive(e,o,t,n,r)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,o,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,o,r){this.cache[e]=o;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,o,n,r),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=o}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===l}function n(e){var t=o(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function o(e){return e.textContent+r(e)}function r(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,o=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+o+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,l=e.IMPORT_LINK_TYPE,u="link[rel="+l+"]",h={documentSelectors:u,importsSelectors:[u,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,o=function(r){e.removeEventListener("load",o),e.removeEventListener("error",o),t&&t(r),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",o),e.addEventListener("error",o),d&&"style"===e.localName){var r=!1;if(-1==e.textContent.indexOf("@import"))r=!0;else if(e.sheet){r=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(r=r&&Boolean(i.styleSheet))}r&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var o=document.createElement("script");o.__importElement=t,o.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(o,function(t){o.parentNode&&o.parentNode.removeChild(o),e.currentScript=null}),this.addElementToDocument(o)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var o,r=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=r.length;a>i&&(o=r[i]);i++)if(!this.isParsed(o))return this.hasResource(o)?t(o)?this.nextToParseInDoc(o.__doc,o):o:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return!t(e)||void 0!==e.__doc}};e.parser=h,e.IMPORT_SELECTOR=u}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function o(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function r(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var r=n.createElement("base");r.setAttribute("href",t),n.baseURI||o(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(r),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,l=e.Observer,u=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,o,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=o,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:r(o,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}u.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),u.parseNext()},loadedAll:function(){u.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new l,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,o={added:function(e){for(var o,r,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)o||(o=a.ownerDocument,r=t.isParsed(o)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&r&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&r.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&r.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=o.added.bind(o);var r=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(o)}var n=e.initializeModules;e.isIE;if(!e.useNative){n();var o=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],o=function(e){n.push(e)},r=function(){n.forEach(function(t){t(e)})};e.addModule=o,e.initializeModules=r,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void o(e,t)}),o(e,t)}function n(e,t,o){var r=e.firstElementChild;if(!r)for(r=e.firstChild;r&&r.nodeType!==Node.ELEMENT_NODE;)r=r.nextSibling;for(;r;)t(r,o)!==!0&&n(r,t,o),r=r.nextElementSibling;return null}function o(e,n){for(var o=e.shadowRoot;o;)t(o,n),o=o.olderShadowRoot}function r(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var o,r=e.querySelectorAll("link[rel="+a+"]"),s=0,c=r.length;c>s&&(o=r[s]);s++)o["import"]&&i(o["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=r,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||o(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function o(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function r(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,o=t.length;o>n&&(e=t[n]);n++)e();L=[]}function a(e){y?r(function(){s(e)}):s(e)}function s(e){ -e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),g(e,function(e){d(e)})}function d(e){y?r(function(){l(e)}):l(e)}function l(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function u(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){_.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function f(e,n){if(_.dom){var o=n[0];if(o&&"childList"===o.type&&o.addedNodes&&o.addedNodes){for(var r=o.addedNodes[0];r&&r!==document&&!r.host;)r=r.parentNode;var i=r&&(r.URL||r._URL||r.host&&r.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=u(e);n.forEach(function(e){"childList"===e.type&&(N(e.addedNodes,function(e){e.localName&&t(e,a)}),N(e.removedNodes,function(e){e.localName&&c(e)}))}),_.dom&&console.groupEnd()}function p(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(f.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),_.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),_.dom&&console.groupEnd()}function w(e){b(e,v)}var _=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=window.MutationObserver._isPolyfilled&&_["throttle-attached"];e.hasPolyfillMutations=y,e.hasThrottledAttached=y;var E=!1,L=[],N=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=w,e.upgradeDocument=v,e.upgradeSubtree=o,e.upgradeAll=t,e.attached=a,e.takeRecords=p}),window.CustomElements.addModule(function(e){function t(t,o){if("template"===t.localName&&window.HTMLTemplateElement&&HTMLTemplateElement.decorate&&HTMLTemplateElement.decorate(t),!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var r=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(r);if(i&&(r&&i.tag==t.localName||!r&&!i["extends"]))return n(t,i,o)}}function n(t,n,r){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),o(t,n),t.__upgraded__=!0,i(t),r&&e.attached(t),e.upgradeSubtree(t,r),a.upgrade&&console.groupEnd(),t}function o(e,t){Object.__proto__?e.__proto__=t.prototype:(r(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function r(e,t,n){for(var o={},r=t;r!==n&&r!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(r),s=0;i=a[s];s++)o[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(r,i)),o[i]=1);r=Object.getPrototypeOf(r)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=o}),window.CustomElements.addModule(function(e){function t(t,o){var c=o||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(r(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c["extends"]&&(c["extends"]=c["extends"].toLowerCase()),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),l(c.__name,c),c.ctor=u(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&v(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){o.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){o.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function o(e,t,n){e=e.toLowerCase();var o=this.getAttribute(e);n.apply(this,arguments);var r=this.getAttribute(e);this.attributeChangedCallback&&r!==o&&this.attributeChangedCallback(e,o,r)}function r(e){for(var t=0;t=0&&g(o,HTMLElement),o)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=(e.isIE,e.upgradeDocumentTree),w=e.upgradeAll,_=e.upgradeWithDefinition,g=e.implementPrototype,b=e.useNative,y=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],E={},L="http://www.w3.org/1999/xhtml",N=document.createElement.bind(document),M=document.createElementNS.bind(document);m=Object.__proto__||b?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=E,e["instanceof"]=m,e.reservedTagList=y,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,o=e.initializeModules;e.isIE;if(n){var r=function(){};e.watchShadow=r,e.upgrade=r,e.upgradeAll=r,e.upgradeDocumentTree=r,e.upgradeSubtree=r,e.takeRecords=r,e["instanceof"]=function(e,t){return e instanceof t}}else o();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file +// @version 0.7.23 +!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents-lite.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,o=e.split("=");o[0]&&(t=o[0].match(/wc-(.+)/))&&(n[t[1]]=o[1]||!0)}),t)for(var o,r=0;o=t.attributes[r];r++)"src"!==o.name&&(n[o.name]=o.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function o(e){return""==e&&n.call(this),e.toLowerCase()}function r(e){var t=e.charCodeAt(0);return t>32&&t<127&&[34,35,60,62,63,96].indexOf(t)==-1?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&t<127&&[34,35,60,62,96].indexOf(t)==-1?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",l=0,u="",w=!1,_=!1,g=[];e:for(;(e[l-1]!=p||0==l)&&!this._isInvalid;){var b=e[l];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}u="",d="no scheme";continue}u+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))u+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}u="",l=0,d="no scheme";continue}if(this._scheme=u,u="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&"\t"!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=r(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[l+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[l+1],E=e[l+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),u+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var o=t[this.name];return o&&o[0]===t?o[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return!(!t||t[0]!==e)&&(t[0]=t[1]=void 0,!0)},has:function(e){var t=e[this.name];return!!t&&t[0]===e}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(o))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function o(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();r(e),n.length&&(e.callback_(n,e),t=!0)}),t&&o()}function r(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var o=v.get(n);if(o)for(var r=0;r0){var r=n[o-1],i=f(r,e);if(i)return void(n[o-1]=i)}else t(this.observer);n[o]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n":return">";case" ":return" "}}function t(t){return t.replace(u,e)}var n="undefined"==typeof HTMLTemplateElement;/Trident/.test(navigator.userAgent)&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType===Node.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}();var o=function(){if(!n){var e=document.createElement("template"),t=document.createElement("template");t.content.appendChild(document.createElement("div")),e.content.appendChild(t);var o=e.cloneNode(!0);return 0===o.content.childNodes.length||0===o.content.firstChild.content.childNodes.length}}(),r="template",i=function(){};if(n){var a=document.implementation.createHTMLDocument("template"),s=!0,c=document.createElement("style");c.textContent=r+"{display:none;}";var d=document.head;d.insertBefore(c,d.firstElementChild),i.prototype=Object.create(HTMLElement.prototype),i.decorate=function(e){if(!e.content){e.content=a.createDocumentFragment();for(var n;n=e.firstChild;)e.content.appendChild(n);if(e.cloneNode=function(e){return i.cloneNode(this,e)},s)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(a.body.innerHTML=e,i.bootstrap(a);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;a.body.firstChild;)this.content.appendChild(a.body.firstChild)},configurable:!0})}catch(o){s=!1}i.bootstrap(e.content)}},i.bootstrap=function(e){for(var t,n=e.querySelectorAll(r),o=0,a=n.length;o]/g}if(n||o){var h=Node.prototype.cloneNode;i.cloneNode=function(e,t){var n=h.call(e,!1);return this.decorate&&this.decorate(n),t&&(n.content.appendChild(h.call(e.content,!0)),this.fixClonedDom(n.content,e.content)),n},i.fixClonedDom=function(e,t){if(t.querySelectorAll)for(var n,o,i=t.querySelectorAll(r),a=e.querySelectorAll(r),s=0,c=a.length;s=200&&e.status<300||304===e.status||0===e.status},load:function(n,o,r){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}o.call(r,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,o=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};o.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,o=e.length;n-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,o,null,a)}.bind(this),0)}else{var s=function(t,n,r){this.receive(e,o,t,n,r)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,o,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,o,r){this.cache[e]=o;for(var i,a=this.pending[e],s=0,c=a.length;s=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,o=function(r){e.removeEventListener("load",o),e.removeEventListener("error",o),t&&t(r),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",o),e.addEventListener("error",o),d&&"style"===e.localName){var r=!1;if(e.textContent.indexOf("@import")==-1)r=!0;else if(e.sheet){r=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;c=0},hasResource:function(e){return!t(e)||void 0!==e.__doc}};e.parser=h,e.IMPORT_SELECTOR=u}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function o(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function r(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var r=n.createElement("base");r.setAttribute("href",t),n.baseURI||o(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(r),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,l=e.Observer,u=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,o,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=o,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:r(o,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}u.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),u.parseNext()},loadedAll:function(){u.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new l,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,o={added:function(e){for(var o,r,i,a,s=0,c=e.length;s=0)){n.push(e);for(var o,r=e.querySelectorAll("link[rel="+a+"]"),s=0,c=r.length;s=0&&g(o,HTMLElement),o)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=(e.isIE,e.upgradeDocumentTree),w=e.upgradeAll,_=e.upgradeWithDefinition,g=e.implementPrototype,b=e.useNative,y=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],E={},L="http://www.w3.org/1999/xhtml",N=document.createElement.bind(document),M=document.createElementNS.bind(document);m=Object.__proto__||b?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=E,e["instanceof"]=m,e.reservedTagList=y,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,o=e.initializeModules;e.isIE;if(n){var r=function(){};e.watchShadow=r,e.upgrade=r,e.upgradeAll=r,e.upgradeDocumentTree=r,e.upgradeSubtree=r,e.takeRecords=r,e["instanceof"]=function(e,t){return e instanceof t}}else o();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz index 0bd0568f231..c5fe12d2303 100644 Binary files a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz and b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz differ diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index cbdfef85942..0dfabdd8a35 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -29,11 +29,13 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' CONF_ENTITIES = 'entities' CONF_VIEW = 'view' +CONF_CONTROL = 'control' ATTR_AUTO = 'auto' ATTR_ORDER = 'order' ATTR_VIEW = 'view' ATTR_VISIBLE = 'visible' +ATTR_CONTROL = 'control' SERVICE_SET_VISIBILITY = 'set_visibility' SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({ @@ -61,6 +63,7 @@ CONFIG_SCHEMA = vol.Schema({ CONF_VIEW: cv.boolean, CONF_NAME: cv.string, CONF_ICON: cv.icon, + CONF_CONTROL: cv.string, }, cv.match_all)) }, extra=vol.ALLOW_EXTRA) @@ -206,11 +209,13 @@ def _async_process_config(hass, config, component): entity_ids = conf.get(CONF_ENTITIES) or [] icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) + control = conf.get(CONF_CONTROL) # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. group = yield from Group.async_create_group( - hass, name, entity_ids, icon=icon, view=view, object_id=object_id) + hass, name, entity_ids, icon=icon, view=view, + control=control, object_id=object_id) groups.append(group) if groups: @@ -221,7 +226,7 @@ class Group(Entity): """Track a group of entity ids.""" def __init__(self, hass, name, order=None, user_defined=True, icon=None, - view=False): + view=False, control=None): """Initialize a group. This Object has factory function for creation. @@ -239,20 +244,22 @@ class Group(Entity): self._assumed_state = False self._async_unsub_state_changed = None self._visible = True + self._control = control @staticmethod def create_group(hass, name, entity_ids=None, user_defined=True, - icon=None, view=False, object_id=None): + icon=None, view=False, control=None, object_id=None): """Initialize a group.""" return run_coroutine_threadsafe( Group.async_create_group(hass, name, entity_ids, user_defined, - icon, view, object_id), + icon, view, control, object_id), hass.loop).result() @staticmethod @asyncio.coroutine def async_create_group(hass, name, entity_ids=None, user_defined=True, - icon=None, view=False, object_id=None): + icon=None, view=False, control=None, + object_id=None): """Initialize a group. This method must be run in the event loop. @@ -260,7 +267,8 @@ class Group(Entity): group = Group( hass, name, order=len(hass.states.async_entity_ids(DOMAIN)), - user_defined=user_defined, icon=icon, view=view) + user_defined=user_defined, icon=icon, view=view, + control=control) group.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id or name, hass=hass) @@ -319,6 +327,8 @@ class Group(Entity): data[ATTR_AUTO] = True if self._view: data[ATTR_VIEW] = True + if self._control: + data[ATTR_CONTROL] = self._control return data @property diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 245e6a12cc2..00adf3701c0 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -825,7 +825,7 @@ class HMDevice(Entity): if have_change: _LOGGER.debug("%s update_ha_state after '%s'", self._name, attribute) - self.update_ha_state() + self.schedule_update_ha_state() def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index de864a0c193..446b1f5f28b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -288,10 +288,16 @@ class HomeAssistantWSGI(object): cors_added.add(route) if self.ssl_certificate: - context = ssl.SSLContext(SSL_VERSION) - context.options |= SSL_OPTS - context.set_ciphers(CIPHERS) - context.load_cert_chain(self.ssl_certificate, self.ssl_key) + try: + context = ssl.SSLContext(SSL_VERSION) + context.options |= SSL_OPTS + context.set_ciphers(CIPHERS) + context.load_cert_chain(self.ssl_certificate, self.ssl_key) + except OSError as error: + _LOGGER.error("Could not read SSL certificate from %s: %s", + self.ssl_certificate, error) + context = None + return else: context = None @@ -305,18 +311,24 @@ class HomeAssistantWSGI(object): self._handler = self.app.make_handler() - self.server = yield from self.hass.loop.create_server( - self._handler, self.server_host, self.server_port, ssl=context) + try: + self.server = yield from self.hass.loop.create_server( + self._handler, self.server_host, self.server_port, ssl=context) + except OSError as error: + _LOGGER.error("Failed to create HTTP server at port %d: %s", + self.server_port, error) self.app._frozen = False # pylint: disable=protected-access @asyncio.coroutine def stop(self): """Stop the wsgi server.""" - self.server.close() - yield from self.server.wait_closed() + if self.server: + self.server.close() + yield from self.server.wait_closed() yield from self.app.shutdown() - yield from self._handler.finish_connections(60.0) + if self._handler: + yield from self._handler.finish_connections(60.0) yield from self.app.cleanup() diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 579e6bade3e..16b2d365976 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -95,7 +95,8 @@ def async_setup(hass, config): attr = 'async_toggle' tasks = [getattr(input_b, attr)() for input_b in target_inputs] - yield from asyncio.wait(tasks, loop=hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 9b563d271f5..bd25a74ae3f 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -113,7 +113,8 @@ def async_setup(hass, config): tasks = [input_select.async_select_option(call.data[ATTR_OPTION]) for input_select in target_inputs] - yield from asyncio.wait(tasks, loop=hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service, @@ -126,7 +127,8 @@ def async_setup(hass, config): tasks = [input_select.async_offset_index(1) for input_select in target_inputs] - yield from asyncio.wait(tasks, loop=hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service, @@ -139,7 +141,8 @@ def async_setup(hass, config): tasks = [input_select.async_offset_index(-1) for input_select in target_inputs] - yield from asyncio.wait(tasks, loop=hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service, diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index eccffb5ae29..d2453a97d14 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -105,7 +105,8 @@ def async_setup(hass, config): tasks = [input_slider.async_select_value(call.data[ATTR_VALUE]) for input_slider in target_inputs] - yield from asyncio.wait(tasks, loop=hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service, diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 1e1d4136142..a5474612496 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -17,8 +17,8 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.9.zip' - '#flux_led==0.9'] +REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.10.zip' + '#flux_led==0.10'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 8fd8a6ef097..259553cc620 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -90,6 +90,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is not None: host = urlparse(discovery_info[1]).hostname + + if "HASS Bridge" in discovery_info[0]: + _LOGGER.info('Emulated hue found, will not add') + return False else: host = config.get(CONF_HOST, None) @@ -138,10 +142,14 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable): configurator.request_done(request_id) lights = {} + lightgroups = {} + skip_groups = False @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_lights(): """Update the Hue light objects with latest info from the bridge.""" + nonlocal skip_groups + try: api = bridge.get_api() except socket.error: @@ -149,9 +157,18 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable): _LOGGER.exception("Cannot reach the bridge") return - api_states = api.get('lights') + api_lights = api.get('lights') - if not isinstance(api_states, dict): + if not isinstance(api_lights, dict): + _LOGGER.error("Got unexpected result from Hue API") + return + + if skip_groups: + api_groups = {} + else: + api_groups = api.get('groups') + + if not isinstance(api_groups, dict): _LOGGER.error("Got unexpected result from Hue API") return @@ -163,7 +180,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable): else: bridge_type = 'hue' - for light_id, info in api_states.items(): + for light_id, info in api_lights.items(): if light_id not in lights: lights[light_id] = HueLight(int(light_id), info, bridge, update_lights, @@ -171,6 +188,23 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable): new_lights.append(lights[light_id]) else: lights[light_id].info = info + lights[light_id].schedule_update_ha_state() + + for lightgroup_id, info in api_groups.items(): + if 'state' not in info: + _LOGGER.warning('Group info does not contain state. ' + 'Please update your hub.') + skip_groups = True + break + + if lightgroup_id not in lightgroups: + lightgroups[lightgroup_id] = HueLight( + int(lightgroup_id), info, bridge, update_lights, + bridge_type, allow_unreachable, True) + new_lights.append(lightgroups[lightgroup_id]) + else: + lightgroups[lightgroup_id].info = info + lightgroups[lightgroup_id].schedule_update_ha_state() if new_lights: add_devices(new_lights) @@ -225,15 +259,20 @@ class HueLight(Light): """Representation of a Hue light.""" def __init__(self, light_id, info, bridge, update_lights, - bridge_type, allow_unreachable): + bridge_type, allow_unreachable, is_group=False): """Initialize the light.""" self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights self.bridge_type = bridge_type - self.allow_unreachable = allow_unreachable + self.is_group = is_group + + if is_group: + self._command_func = self.bridge.set_group + else: + self._command_func = self.bridge.set_light @property def unique_id(self): @@ -243,33 +282,44 @@ class HueLight(Light): @property def name(self): - """Return the mame of the Hue light.""" + """Return the name of the Hue light.""" return self.info.get('name', DEVICE_DEFAULT_NAME) @property def brightness(self): """Return the brightness of this light between 0..255.""" - return self.info['state'].get('bri') + if self.is_group: + return self.info['action'].get('bri') + else: + return self.info['state'].get('bri') @property def xy_color(self): """Return the XY color value.""" - return self.info['state'].get('xy') + if self.is_group: + return self.info['action'].get('xy') + else: + return self.info['state'].get('xy') @property def color_temp(self): """Return the CT color value.""" - return self.info['state'].get('ct') + if self.is_group: + return self.info['action'].get('ct') + else: + return self.info['state'].get('ct') @property def is_on(self): """Return true if device is on.""" - self.update_lights() - - if self.allow_unreachable: - return self.info['state']['on'] + if self.is_group: + return self.info['state']['any_on'] else: - return self.info['state']['reachable'] and self.info['state']['on'] + if self.allow_unreachable: + return self.info['state']['on'] + else: + return self.info['state']['reachable'] and \ + self.info['state']['on'] @property def supported_features(self): @@ -318,7 +368,7 @@ class HueLight(Light): elif self.bridge_type == 'hue': command['effect'] = 'none' - self.bridge.set_light(self.light_id, command) + self._command_func(self.light_id, command) def turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -340,7 +390,7 @@ class HueLight(Light): elif self.bridge_type == 'hue': command['alert'] = 'none' - self.bridge.set_light(self.light_id, command) + self._command_func(self.light_id, command) def update(self): """Synchronize state with bridge.""" diff --git a/homeassistant/components/light/tellduslive.py b/homeassistant/components/light/tellduslive.py new file mode 100644 index 00000000000..31f9eb1d253 --- /dev/null +++ b/homeassistant/components/light/tellduslive.py @@ -0,0 +1,63 @@ +""" +Support for Tellstick switches using Tellstick Net. + +This platform uses the Telldus Live online service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.tellduslive/ + +""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.tellduslive import TelldusLiveEntity + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup lights.""" + if discovery_info is None: + return + add_devices(TelldusLiveLight(hass, light) for light in discovery_info) + + +class TelldusLiveLight(TelldusLiveEntity, Light): + """Representation of a light.""" + + def __init__(self, hass, device_id): + """Initialize the light.""" + super().__init__(hass, device_id) + self._last_brightness = self.brightness + + def changed(self): + """A property of the device might have changed.""" + self._last_brightness = self.brightness + super().changed() + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self.device.dim_level + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def is_on(self): + """Return true if light is on.""" + return self.device.is_on + + def turn_on(self, **kwargs): + """Turn the light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) + self.device.dim(level=brightness) + self.changed() + + def turn_off(self, **kwargs): + """Turn the light off.""" + self.device.turn_off() + self.changed() diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 9afc826e83c..d23d5e2c4d6 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -29,16 +29,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS) - add_devices(TellstickLight(tellcore_id, signal_repetitions) + add_devices(TellstickLight(tellcore_id, hass.data['tellcore_registry'], + signal_repetitions) for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]) class TellstickLight(TellstickDevice, Light): """Representation of a Tellstick light.""" - def __init__(self, tellcore_id, signal_repetitions): + def __init__(self, tellcore_id, tellcore_registry, signal_repetitions): """Initialize the light.""" - super().__init__(tellcore_id, signal_repetitions) + super().__init__(tellcore_id, tellcore_registry, signal_repetitions) self._brightness = 255 @@ -71,7 +72,11 @@ class TellstickLight(TellstickDevice, Light): if brightness is not None: self._brightness = brightness - self._state = (self._brightness > 0) + # _brightness is not defined when called from super + try: + self._state = (self._brightness > 0) + except AttributeError: + self._state = True else: self._state = False diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index fa2ecee4337..3dea75df874 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -162,7 +162,7 @@ MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - ATTR_MEDIA_ENQUEUE: cv.boolean, + vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, }) MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py new file mode 100644 index 00000000000..c39986d7588 --- /dev/null +++ b/homeassistant/components/media_player/aquostv.py @@ -0,0 +1,161 @@ +""" +Support for interface with an Aquos TV. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.aquostv/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_SET, MediaPlayerDevice, PLATFORM_SCHEMA) + +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, + CONF_PORT, CONF_USERNAME, CONF_PASSWORD) + + +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['sharp-aquos-rc==0.2'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Sharp Aquos TV' +DEFAULT_PORT = 10002 +DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = 'password' + +SUPPORT_SHARPTV = SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_OFF | SUPPORT_TURN_ON + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Sharp Aquos TV platform.""" + import sharp_aquos_rc + + name = config.get(CONF_NAME) + port = config.get(CONF_PORT) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + if discovery_info: + _LOGGER.debug('%s', discovery_info) + vals = discovery_info.split(':') + if len(vals) > 1: + port = vals[1] + + host = vals[0] + remote = sharp_aquos_rc.TV(host, + port, + username, + password) + add_devices([SharpAquosTVDevice(name, remote)]) + return True + + host = config.get(CONF_HOST) + remote = sharp_aquos_rc.TV(host, + port, + username, + password) + + add_devices([SharpAquosTVDevice(name, remote)]) + return True + + +# pylint: disable=abstract-method +class SharpAquosTVDevice(MediaPlayerDevice): + """Representation of a Aquos TV.""" + + # pylint: disable=too-many-public-methods + def __init__(self, name, remote): + """Initialize the aquos device.""" + # Save a reference to the imported class + self._name = name + # Assume that the TV is not muted + self._muted = False + # Assume that the TV is in Play mode + self._playing = True + self._state = STATE_UNKNOWN + self._remote = remote + self._volume = 0 + + def update(self): + """Retrieve the latest data.""" + try: + if self._remote.power() == 1: + self._state = STATE_ON + else: + self._state = STATE_OFF + + # Set TV to be able to remotely power on + # self._remote.power_on_command_settings(2) + if self._remote.mute() == 2: + self._muted = False + else: + self._muted = True + self._volume = self._remote.volume() / 60 + except OSError: + self._state = STATE_OFF + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_SHARPTV + + def turn_off(self): + """Turn off tvplayer.""" + self._remote.power(0) + + def volume_up(self): + """Volume up the media player.""" + self._remote.volume(int(self._volume * 60) + 2) + + def volume_down(self): + """Volume down media player.""" + self._remote.volume(int(self._volume * 60) - 2) + + def set_volume_level(self, level): + """Set Volume media player.""" + self._remote.volume(int(level * 60)) + + def mute_volume(self, mute): + """Send mute command.""" + self._remote.mute(0) + + def turn_on(self): + """Turn the media player on.""" + self._remote.power(1) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index dbcecd6097a..004682b402e 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.loader import get_component from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) @@ -40,7 +40,8 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 1d01f0058ec..7e96e0dbed6 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util REQUIREMENTS = ['pychromecast==0.7.6'] @@ -105,6 +106,7 @@ class CastDevice(MediaPlayerDevice): self.cast_status = self.cast.status self.media_status = self.cast.media_controller.status + self.media_status_received = None @property def should_poll(self): @@ -231,6 +233,30 @@ class CastDevice(MediaPlayerDevice): """Flag of media commands that are supported.""" return SUPPORT_CAST + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self.media_status is None or self.media_status_received is None or \ + not (self.media_status.player_is_playing or + self.media_status.player_is_idle): + return None + + position = self.media_status.current_time + + if self.media_status.player_is_playing: + position += (dt_util.utcnow() - + self.media_status_received).total_seconds() + + return position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + return self.media_status_received + def turn_on(self): """Turn on the ChromeCast.""" # The only way we can turn the Chromecast is on is by launching an app @@ -292,4 +318,5 @@ class CastDevice(MediaPlayerDevice): def new_media_status(self, status): """Called when a new media status is received.""" self.media_status = status + self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 1c1687de319..226ddfe4769 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -10,6 +10,7 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING +import homeassistant.util.dt as dt_util # pylint: disable=unused-argument @@ -18,8 +19,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ DemoYoutubePlayer( 'Living Room', 'eyU3bRy2x44', - '♥♥ The Best Fireplace Video (3 hours)'), - DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'), + '♥♥ The Best Fireplace Video (3 hours)', 300), + DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours', + 360000), DemoMusicPlayer(), DemoTVShowPlayer(), ]) @@ -78,32 +80,32 @@ class AbstractDemoPlayer(MediaPlayerDevice): def turn_on(self): """Turn the media player on.""" self._player_state = STATE_PLAYING - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self): """Turn the media player off.""" self._player_state = STATE_OFF - self.update_ha_state() + self.schedule_update_ha_state() def mute_volume(self, mute): """Mute the volume.""" self._volume_muted = mute - self.update_ha_state() + self.schedule_update_ha_state() def set_volume_level(self, volume): """Set the volume level, range 0..1.""" self._volume_level = volume - self.update_ha_state() + self.schedule_update_ha_state() def media_play(self): """Send play command.""" self._player_state = STATE_PLAYING - self.update_ha_state() + self.schedule_update_ha_state() def media_pause(self): """Send pause command.""" self._player_state = STATE_PAUSED - self.update_ha_state() + self.schedule_update_ha_state() class DemoYoutubePlayer(AbstractDemoPlayer): @@ -111,11 +113,14 @@ class DemoYoutubePlayer(AbstractDemoPlayer): # We only implement the methods that we support - def __init__(self, name, youtube_id=None, media_title=None): + def __init__(self, name, youtube_id=None, media_title=None, duration=360): """Initialize the demo device.""" super().__init__(name) self.youtube_id = youtube_id self._media_title = media_title + self._duration = duration + self._progress = int(duration * .15) + self._progress_updated_at = dt_util.utcnow() @property def media_content_id(self): @@ -130,7 +135,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): @property def media_duration(self): """Return the duration of current playing media in seconds.""" - return 360 + return self._duration @property def media_image_url(self): @@ -152,10 +157,39 @@ class DemoYoutubePlayer(AbstractDemoPlayer): """Flag of media commands that are supported.""" return YOUTUBE_PLAYER_SUPPORT + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self._progress is None: + return None + + position = self._progress + + if self._player_state == STATE_PLAYING: + position += (dt_util.utcnow() - + self._progress_updated_at).total_seconds() + + return position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + if self._player_state == STATE_PLAYING: + return self._progress_updated_at + def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" self.youtube_id = media_id - self.update_ha_state() + self.schedule_update_ha_state() + + def media_pause(self): + """Send pause command.""" + self._progress = self.media_position + self._progress_updated_at = dt_util.utcnow() + super().media_pause() class DemoMusicPlayer(AbstractDemoPlayer): @@ -249,20 +283,20 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Send previous track command.""" if self._cur_track > 0: self._cur_track -= 1 - self.update_ha_state() + self.schedule_update_ha_state() def media_next_track(self): """Send next track command.""" if self._cur_track < len(self.tracks) - 1: self._cur_track += 1 - self.update_ha_state() + self.schedule_update_ha_state() def clear_playlist(self): """Clear players playlist.""" self.tracks = [] self._cur_track = 0 self._player_state = STATE_OFF - self.update_ha_state() + self.schedule_update_ha_state() class DemoTVShowPlayer(AbstractDemoPlayer): @@ -344,15 +378,15 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """Send previous track command.""" if self._cur_episode > 1: self._cur_episode -= 1 - self.update_ha_state() + self.schedule_update_ha_state() def media_next_track(self): """Send next track command.""" if self._cur_episode < self._episode_count: self._cur_episode += 1 - self.update_ha_state() + self.schedule_update_ha_state() def select_source(self, source): """Set the input source.""" self._source = source - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index b167ee52808..badbae162a2 100755 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_STOP, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv @@ -22,16 +22,32 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Music station' -SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ - SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF +SUPPORT_DENON = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE \ + +SUPPORT_MEDIA_MODES = SUPPORT_PAUSE | SUPPORT_STOP | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) +NORMAL_INPUTS = {'Cd': 'CD', 'Dvd': 'DVD', 'Blue ray': 'BD', 'TV': 'TV', + 'Satelite / Cable': 'SAT/CBL', 'Game': 'GAME', + 'Game2': 'GAME2', 'Video Aux': 'V.AUX', 'Dock': 'DOCK'} + +MEDIA_MODES = {'Tuner': 'TUNER', 'Media server': 'SERVER', + 'Ipod dock': 'IPOD', 'Net/USB': 'NET/USB', + 'Rapsody': 'RHAPSODY', 'Napster': 'NAPSTER', + 'Pandora': 'PANDORA', 'LastFM': 'LASTFM', + 'Flickr': 'FLICKR', 'Favorites': 'FAVORITES', + 'Internet Radio': 'IRADIO', 'USB/IPOD': 'USB/IPOD'} + +# Sub-modes of 'NET/USB' +# {'USB': 'USB', 'iPod Direct': 'IPD', 'Internet Radio': 'IRP', +# 'Favorites': 'FVP'} + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Denon platform.""" @@ -53,14 +69,39 @@ class DenonDevice(MediaPlayerDevice): self._host = host self._pwstate = 'PWSTANDBY' self._volume = 0 - self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER', - 'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'} + # Initial value 60dB, changed if we get a MVMAX + self._volume_max = 60 + self._source_list = NORMAL_INPUTS.copy() + self._source_list.update(MEDIA_MODES) self._muted = False self._mediasource = '' + self._mediainfo = '' + + self._should_setup_sources = True + + def _setup_sources(self, telnet): + # NSFRN - Network name + self._name = self.telnet_request(telnet, 'NSFRN ?')[len('NSFRN '):] + + # SSFUN - Configured sources with names + self._source_list = {} + for line in self.telnet_request(telnet, 'SSFUN ?', all_lines=True): + source, configured_name = line[len('SSFUN'):].split(" ", 1) + self._source_list[configured_name] = source + + # SSSOD - Deleted sources + for line in self.telnet_request(telnet, 'SSSOD ?', all_lines=True): + source, status = line[len('SSSOD'):].split(" ", 1) + if status == 'DEL': + for pretty_name, name in self._source_list.items(): + if source == name: + del self._source_list[pretty_name] + break @classmethod - def telnet_request(cls, telnet, command): + def telnet_request(cls, telnet, command, all_lines=False): """Execute `command` and return the response.""" + _LOGGER.debug('Sending: "%s"', command) telnet.write(command.encode('ASCII') + b'\r') lines = [] while True: @@ -68,12 +109,16 @@ class DenonDevice(MediaPlayerDevice): if not line: break lines.append(line.decode('ASCII').strip()) + _LOGGER.debug('Recived: "%s"', line) + if all_lines: + return lines return lines[0] def telnet_command(self, command): """Establish a telnet connection and sends `command`.""" telnet = telnetlib.Telnet(self._host) + _LOGGER.debug('Sending: "%s"', command) telnet.write(command.encode('ASCII') + b'\r') telnet.read_very_eager() # skip response telnet.close() @@ -85,12 +130,30 @@ class DenonDevice(MediaPlayerDevice): except OSError: return False + if self._should_setup_sources: + self._setup_sources(telnet) + self._should_setup_sources = False + self._pwstate = self.telnet_request(telnet, 'PW?') - volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):] - self._volume = int(volume_str) / 60 + for line in self.telnet_request(telnet, 'MV?', all_lines=True): + if line.startswith('MVMAX '): + # only grab two digit max, don't care about any half digit + self._volume_max = int(line[len('MVMAX '):len('MVMAX XX')]) + continue + if line.startswith('MV'): + self._volume = int(line[len('MV'):]) self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON') self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):] + if self._mediasource in MEDIA_MODES.values(): + self._mediainfo = "" + answer_codes = ["NSE0", "NSE1X", "NSE2X", "NSE3X", "NSE4", "NSE5", + "NSE6", "NSE7", "NSE8"] + for line in self.telnet_request(telnet, 'NSE', all_lines=True): + self._mediainfo += line[len(answer_codes.pop()):] + '\n' + else: + self._mediainfo = self.source + telnet.close() return True @@ -112,7 +175,7 @@ class DenonDevice(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._volume + return self._volume / self._volume_max @property def is_volume_muted(self): @@ -122,17 +185,27 @@ class DenonDevice(MediaPlayerDevice): @property def source_list(self): """List of available input sources.""" - return list(self._source_list.keys()) + return sorted(list(self._source_list.keys())) @property def media_title(self): - """Current media source.""" - return self._mediasource + """Current media info.""" + return self._mediainfo @property def supported_media_commands(self): """Flag of media commands that are supported.""" - return SUPPORT_DENON + if self._mediasource in MEDIA_MODES.values(): + return SUPPORT_DENON | SUPPORT_MEDIA_MODES + else: + return SUPPORT_DENON + + @property + def source(self): + """Return the current input source.""" + for pretty_name, name in self._source_list.items(): + if self._mediasource == name: + return pretty_name def turn_off(self): """Turn off media player.""" @@ -148,8 +221,8 @@ class DenonDevice(MediaPlayerDevice): def set_volume_level(self, volume): """Set volume level, range 0..1.""" - # 60dB max - self.telnet_command('MV' + str(round(volume * 60)).zfill(2)) + self.telnet_command('MV' + + str(round(volume * self._volume_max)).zfill(2)) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" @@ -163,6 +236,10 @@ class DenonDevice(MediaPlayerDevice): """Pause media player.""" self.telnet_command('NS9B') + def media_stop(self): + """Pause media player.""" + self.telnet_command('NS9C') + def media_next_track(self): """Send the next track command.""" self.telnet_command('NS9D') @@ -177,4 +254,4 @@ class DenonDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - self.telnet_command(self._source_list.get(source)) + self.telnet_command('SI' + self._source_list.get(source)) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 6db223bc6ec..5784fd6c829 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -13,26 +13,27 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON, - MEDIA_TYPE_MUSIC) + MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET) from homeassistant.const import ( CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED, CONF_NAME, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.1.6'] +REQUIREMENTS = ['denonavr==0.2.2'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = None +KEY_DENON_CACHE = 'denonavr_hosts' SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \ SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK + SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -41,11 +42,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Denon platform.""" import denonavr - receiver = denonavr.DenonAVR(config.get(CONF_HOST), config.get(CONF_NAME)) + # Initialize list with receivers to be started + receivers = [] - add_devices([DenonDevice(receiver)]) - _LOGGER.info("Denon receiver at host %s initialized", - config.get(CONF_HOST)) + cache = hass.data.get(KEY_DENON_CACHE) + if cache is None: + cache = hass.data[KEY_DENON_CACHE] = set() + + # Start assignment of host and name + # 1. option: manual setting + if config.get(CONF_HOST) is not None: + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + # Check if host not in cache, append it and save for later starting + if host not in cache: + cache.add(host) + receivers.append( + DenonDevice(denonavr.DenonAVR(host, name))) + _LOGGER.info("Denon receiver at host %s initialized", host) + # 2. option: discovery using netdisco + if discovery_info is not None: + host = discovery_info[0] + name = discovery_info[1] + # Check if host not in cache, append it and save for later starting + if host not in cache: + cache.add(host) + receivers.append( + DenonDevice(denonavr.DenonAVR(host, name))) + _LOGGER.info("Denon receiver at host %s initialized", host) + # 3. option: discovery using denonavr library + if config.get(CONF_HOST) is None and discovery_info is None: + d_receivers = denonavr.discover() + # More than one receiver could be discovered by that method + if d_receivers is not None: + for d_receiver in d_receivers: + host = d_receiver["host"] + name = d_receiver["friendlyName"] + # Check if host not in cache, append it and save for later + # starting + if host not in cache: + cache.add(host) + receivers.append( + DenonDevice(denonavr.DenonAVR(host, name))) + _LOGGER.info("Denon receiver at host %s initialized", host) + + # Add all freshly discovered receivers + if receivers: + add_devices(receivers) class DenonDevice(MediaPlayerDevice): @@ -107,7 +150,8 @@ class DenonDevice(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - # Volume is send in a format like -50.0. Minimum is around -80.0 + # Volume is sent in a format like -50.0. Minimum is -80.0, + # maximum is 18.0 return (float(self._volume) + 80) / 100 @property @@ -240,6 +284,22 @@ class DenonDevice(MediaPlayerDevice): """Volume down media player.""" return self._receiver.volume_down() + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + # Volume has to be sent in a format like -50.0. Minimum is -80.0, + # maximum is 18.0 + volume_denon = float((volume * 100) - 80) + if volume_denon > 18: + volume_denon = float(18) + try: + if self._receiver.set_volume(volume_denon): + self._volume = volume_denon + return True + else: + return False + except ValueError: + return False + def mute_volume(self, mute): """Send mute command.""" return self._receiver.mute(mute) diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 5349e74ed40..3fae52dd052 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -20,12 +20,15 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) from homeassistant.helpers.event import (track_utc_time_change) from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyemby==0.1'] +REQUIREMENTS = ['pyemby==0.2'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +MEDIA_TYPE_TRAILER = 'trailer' + DEFAULT_PORT = 8096 _LOGGER = logging.getLogger(__name__) @@ -119,6 +122,8 @@ class EmbyClient(MediaPlayerDevice): self.update_sessions = update_sessions self.client = client self.set_device(device) + self.media_status_last_position = None + self.media_status_received = None def set_device(self, device): """Set the device property.""" @@ -178,6 +183,17 @@ class EmbyClient(MediaPlayerDevice): """Get the latest details.""" self.update_devices(no_throttle=True) self.update_sessions(no_throttle=True) + # Check if we should update progress + try: + position = self.session['PlayState']['PositionTicks'] + except (KeyError, TypeError): + self.media_status_last_position = None + self.media_status_received = None + else: + position = int(position) / 10000000 + if position != self.media_status_last_position: + self.media_status_last_position = position + self.media_status_received = dt_util.utcnow() def play_percent(self): """Return current media percent complete.""" @@ -220,6 +236,8 @@ class EmbyClient(MediaPlayerDevice): return MEDIA_TYPE_TVSHOW elif media_type == 'Movie': return MEDIA_TYPE_VIDEO + elif media_type == 'Trailer': + return MEDIA_TYPE_TRAILER return None except KeyError: return None @@ -233,19 +251,32 @@ class EmbyClient(MediaPlayerDevice): except KeyError: return None + @property + def media_position(self): + """Position of current playing media in seconds.""" + return self.media_status_last_position + + @property + def media_position_updated_at(self): + """ + When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + return self.media_status_received + @property def media_image_url(self): """Image url of current playing media.""" if self.now_playing_item is not None: try: return self.client.get_image( - self.now_playing_item['ThumbItemId'], 'Thumb', - self.play_percent()) + self.now_playing_item['ThumbItemId'], 'Thumb', 0) except KeyError: try: return self.client.get_image( - self.now_playing_item['PrimaryImageItemId'], 'Primary', - self.play_percent()) + self.now_playing_item[ + 'PrimaryImageItemId'], 'Primary', 0) except KeyError: return None diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 68161deea2f..9676fe451c7 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -76,11 +76,17 @@ class KodiDevice(MediaPlayerDevice): import jsonrpc_requests self._name = name self._url = url + self._basic_auth_url = None kwargs = {'timeout': 5} if auth is not None: kwargs['auth'] = auth + scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url) + self._basic_auth_url = \ + urllib.parse.urlunsplit((scheme, '{}:{}@{}'.format + (auth[0], auth[1], netloc), + path, query, fragment)) self._server = jsonrpc_requests.Server( '{}/jsonrpc'.format(self._url), **kwargs) @@ -195,6 +201,11 @@ class KodiDevice(MediaPlayerDevice): url_components = urllib.parse.urlparse(self._item['thumbnail']) if url_components.scheme == 'image': + if self._basic_auth_url is not None: + return '{}/image/{}'.format( + self._basic_auth_url, + urllib.parse.quote_plus(self._item['thumbnail'])) + return '{}/image/{}'.format( self._url, urllib.parse.quote_plus(self._item['thumbnail'])) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 5829d119f95..28f8dd1bf6b 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -89,7 +89,7 @@ class OnkyoDevice(MediaPlayerDevice): except (ValueError, OSError, AttributeError, AssertionError): if self._receiver.command_socket: self._receiver.command_socket = None - _LOGGER.info('Reseting connection to %s.', self._name) + _LOGGER.info('Resetting connection to %s.', self._name) else: _LOGGER.info('%s is disconnected. Attempting to reconnect.', self._name) @@ -173,7 +173,7 @@ class OnkyoDevice(MediaPlayerDevice): def turn_on(self): """Turn the media player on.""" - self._receiver.power_on() + self.command('system-power on') def select_source(self, source): """Set the input source.""" diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 987364bbf63..b345fcf4884 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -10,16 +10,20 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['panasonic_viera==0.2'] +REQUIREMENTS = ['panasonic_viera==0.2', + 'wakeonlan==0.2.2'] _LOGGER = logging.getLogger(__name__) +CONF_MAC = 'mac' + DEFAULT_NAME = 'Panasonic Viera TV' DEFAULT_PORT = 55000 @@ -30,6 +34,7 @@ SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) @@ -40,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Panasonic Viera TV platform.""" from panasonic_viera import RemoteControl + mac = config.get(CONF_MAC) name = config.get(CONF_NAME) port = config.get(CONF_PORT) @@ -51,29 +57,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): host = vals[0] remote = RemoteControl(host, port) - add_devices([PanasonicVieraTVDevice(name, remote)]) + add_devices([PanasonicVieraTVDevice(mac, name, remote)]) return True host = config.get(CONF_HOST) remote = RemoteControl(host, port) - try: - remote.get_mute() - except OSError as error: - _LOGGER.error('Panasonic Viera TV is not available at %s:%d: %s', - host, port, error) - return False - - add_devices([PanasonicVieraTVDevice(name, remote)]) + add_devices([PanasonicVieraTVDevice(mac, name, remote)]) return True class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, name, remote): + def __init__(self, mac, name, remote): """Initialize the Panasonic device.""" + from wakeonlan import wol # Save a reference to the imported class + self._wol = wol + self._mac = mac self._name = name self._muted = False self._playing = True @@ -123,8 +125,15 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): @property def supported_media_commands(self): """Flag of media commands that are supported.""" + if self._mac: + return SUPPORT_VIERATV | SUPPORT_TURN_ON return SUPPORT_VIERATV + def turn_on(self): + """Turn on the media player.""" + if self._mac: + self._wol.send_magic_packet(self._mac) + def turn_off(self): """Turn off media player.""" self.send_key('NRC_POWER-ONOFF') diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index a43e8b551a3..c7705393381 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.samsungtv/ """ import logging +import socket import voluptuous as vol @@ -26,6 +27,8 @@ DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 0 +KNOWN_DEVICES_KEY = 'samsungtv_known_devices' + SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF @@ -41,25 +44,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Samsung TV platform.""" - name = config.get(CONF_NAME) + known_devices = hass.data.get(KNOWN_DEVICES_KEY) + if known_devices is None: + known_devices = set() + hass.data[KNOWN_DEVICES_KEY] = known_devices - # Generate a configuration for the Samsung library - remote_config = { - 'name': 'HomeAssistant', - 'description': config.get(CONF_NAME), - 'id': 'ha.component.samsung', - 'port': config.get(CONF_PORT), - 'host': config.get(CONF_HOST), - 'timeout': config.get(CONF_TIMEOUT), - } + # Is this a manual configuration? + if config.get(CONF_HOST) is not None: + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + timeout = config.get(CONF_TIMEOUT) + elif discovery_info is not None: + tv_name, model, host = discovery_info + name = "{} ({})".format(tv_name, model) + port = DEFAULT_PORT + timeout = DEFAULT_TIMEOUT + else: + _LOGGER.warning( + 'Internal error on samsungtv component. Cannot determine device') + return - add_devices([SamsungTVDevice(name, remote_config)]) + # Only add a device once, so discovered devices do not override manual + # config. + ip_addr = socket.gethostbyname(host) + if ip_addr not in known_devices: + known_devices.add(ip_addr) + add_devices([SamsungTVDevice(host, port, name, timeout)]) + _LOGGER.info("Samsung TV %s:%d added as '%s'", host, port, name) + else: + _LOGGER.info("Ignoring duplicate Samsung TV %s:%d", host, port) class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" - def __init__(self, name, config): + def __init__(self, host, port, name, timeout): """Initialize the Samsung device.""" from samsungctl import Remote # Save a reference to the imported class @@ -71,7 +91,15 @@ class SamsungTVDevice(MediaPlayerDevice): self._playing = True self._state = STATE_UNKNOWN self._remote = None - self._config = config + # Generate a configuration for the Samsung library + self._config = { + 'name': 'HomeAssistant', + 'description': name, + 'id': 'ha.component.samsung', + 'port': port, + 'host': host, + 'timeout': timeout, + } def update(self): """Retrieve the latest data.""" diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 1fa1a39633d..94649b3a597 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -15,7 +15,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP) from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID, CONF_HOSTS) @@ -36,9 +36,10 @@ _SOCO_LOGGER.setLevel(logging.ERROR) _REQUESTS_LOGGER = logging.getLogger('requests') _REQUESTS_LOGGER.setLevel(logging.ERROR) -SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\ - SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE +SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ + SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\ + SUPPORT_SELECT_SOURCE SERVICE_GROUP_PLAYERS = 'sonos_group_players' SERVICE_UNJOIN = 'sonos_unjoin' @@ -289,6 +290,7 @@ class SonosDevice(MediaPlayerDevice): self._media_next_title = None self._support_previous_track = False self._support_next_track = False + self._support_stop = False self._support_pause = False self._current_track_uri = None self._current_track_is_radio_stream = False @@ -296,6 +298,8 @@ class SonosDevice(MediaPlayerDevice): self._last_avtransport_event = None self._is_playing_line_in = None self._is_playing_tv = None + self._favorite_sources = None + self._source_name = None self.soco_snapshot = Snapshot(self._player) @property @@ -423,6 +427,7 @@ class SonosDevice(MediaPlayerDevice): media_position = None media_position_updated_at = None + source_name = None is_radio_stream = \ current_media_uri.startswith('x-sonosapi-stream:') or \ @@ -433,6 +438,7 @@ class SonosDevice(MediaPlayerDevice): support_previous_track = False support_next_track = False + support_stop = False support_pause = False if is_playing_tv: @@ -440,6 +446,8 @@ class SonosDevice(MediaPlayerDevice): else: media_artist = SUPPORT_SOURCE_LINEIN + source_name = media_artist + media_album_name = None media_title = None media_image_url = None @@ -450,8 +458,19 @@ class SonosDevice(MediaPlayerDevice): ) support_previous_track = False support_next_track = False + support_stop = False support_pause = False + source_name = 'Radio' + # Check if currently playing radio station is in favorites + favs = self._player.get_sonos_favorites()['favorites'] + favc = [ + fav for fav in favs if fav['uri'] == current_media_uri + ] + if len(favc) == 1: + src = favc.pop() + source_name = src['title'] + # for radio streams we set the radio station name as the # title. if media_artist and media_title: @@ -506,6 +525,7 @@ class SonosDevice(MediaPlayerDevice): ) support_previous_track = True support_next_track = True + support_stop = True support_pause = True position_info = self._player.avTransport.GetPositionInfo( @@ -583,9 +603,11 @@ class SonosDevice(MediaPlayerDevice): self._current_track_is_radio_stream = is_radio_stream self._support_previous_track = support_previous_track self._support_next_track = support_next_track + self._support_stop = support_stop self._support_pause = support_pause self._is_playing_tv = is_playing_tv self._is_playing_line_in = is_playing_line_in + self._source_name = source_name # update state of the whole group # pylint: disable=protected-access @@ -595,6 +617,8 @@ class SonosDevice(MediaPlayerDevice): if self._queue is None and self.entity_id is not None: self._subscribe_to_player_events() + favs = self._player.get_sonos_favorites().get('favorites', []) + self._favorite_sources = [fav['title'] for fav in favs] else: self._player_volume = None self._player_volume_muted = None @@ -614,9 +638,12 @@ class SonosDevice(MediaPlayerDevice): self._current_track_is_radio_stream = False self._support_previous_track = False self._support_next_track = False + self._support_stop = False self._support_pause = False self._is_playing_tv = False self._is_playing_line_in = False + self._favorite_sources = None + self._source_name = None self._last_avtransport_event = None @@ -764,16 +791,15 @@ class SonosDevice(MediaPlayerDevice): supported = SUPPORT_SONOS - if not self.source_list: - # some devices do not allow source selection - supported = supported ^ SUPPORT_SELECT_SOURCE - if not self._support_previous_track: supported = supported ^ SUPPORT_PREVIOUS_TRACK if not self._support_next_track: supported = supported ^ SUPPORT_NEXT_TRACK + if not self._support_stop: + supported = supported ^ SUPPORT_STOP + if not self._support_pause: supported = supported ^ SUPPORT_PAUSE @@ -798,19 +824,31 @@ class SonosDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" if source == SUPPORT_SOURCE_LINEIN: + self._source_name = SUPPORT_SOURCE_LINEIN self._player.switch_to_line_in() elif source == SUPPORT_SOURCE_TV: + self._source_name = SUPPORT_SOURCE_TV self._player.switch_to_tv() + else: + favorites = self._player.get_sonos_favorites()['favorites'] + fav = [fav for fav in favorites if fav['title'] == source] + if len(fav) == 1: + src = fav.pop() + self._source_name = src['title'] + self._player.play_uri(src['uri'], src['meta'], src['title']) @property def source_list(self): """List of available input sources.""" model_name = self._speaker_info['model_name'] + sources = self._favorite_sources + if 'PLAY:5' in model_name: - return [SUPPORT_SOURCE_LINEIN] + sources += [SUPPORT_SOURCE_LINEIN] elif 'PLAYBAR' in model_name: - return [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV] + sources += [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV] + return sources @property def source(self): @@ -818,12 +856,7 @@ class SonosDevice(MediaPlayerDevice): if self._coordinator: return self._coordinator.source else: - if self._is_playing_line_in: - return SUPPORT_SOURCE_LINEIN - elif self._is_playing_tv: - return SUPPORT_SOURCE_TV - - return None + return self._source_name def turn_off(self): """Turn off media player.""" @@ -836,6 +869,13 @@ class SonosDevice(MediaPlayerDevice): else: self._player.play() + def media_stop(self): + """Send stop command.""" + if self._coordinator: + self._coordinator.media_stop() + else: + self._player.stop() + def media_pause(self): """Send pause command.""" if self._coordinator: diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 9923782872a..85a3eb42aa5 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, - ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, + ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST, ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, @@ -38,6 +38,7 @@ CONF_COMMANDS = 'commands' CONF_PLATFORM = 'platform' CONF_SERVICE = 'service' CONF_SERVICE_DATA = 'service_data' +ATTR_DATA = 'data' CONF_STATE = 'state' OFF_STATES = [STATE_IDLE, STATE_OFF] @@ -178,14 +179,15 @@ class UniversalMediaPlayer(MediaPlayerDevice): def _call_service(self, service_name, service_data=None, allow_override=False): """Call either a specified or active child's service.""" - if allow_override and service_name in self._cmds: - call_from_config( - self.hass, self._cmds[service_name], blocking=True) - return - if service_data is None: service_data = {} + if allow_override and service_name in self._cmds: + call_from_config( + self.hass, self._cmds[service_name], + variables=service_data, blocking=True) + return + active_child = self._child_state service_data[ATTR_ENTITY_ID] = active_child.entity_id @@ -233,7 +235,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def volume_level(self): """Volume level of entity specified in attributes or active child.""" - return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL) + return self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL) @property def is_volume_muted(self): @@ -261,6 +263,17 @@ class UniversalMediaPlayer(MediaPlayerDevice): """Image url of current playing media.""" return self._child_attr(ATTR_ENTITY_PICTURE) + @property + def entity_picture(self): + """ + Return image of the media playing. + + The universal media player doesn't use the parent class logic, since + the url is coming from child entity pictures which have already been + sent through the API proxy. + """ + return self.media_image_url + @property def media_title(self): """Title of current playing media.""" @@ -322,9 +335,14 @@ class UniversalMediaPlayer(MediaPlayerDevice): return self._child_attr(ATTR_APP_NAME) @property - def current_source(self): + def source(self): """"Return the current input source of the device.""" - return self._child_attr(ATTR_INPUT_SOURCE) + return self._override_or_child_attr(ATTR_INPUT_SOURCE) + + @property + def source_list(self): + """List of available input sources.""" + return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST) @property def supported_media_commands(self): @@ -340,6 +358,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): SERVICE_VOLUME_DOWN]]): flags |= SUPPORT_VOLUME_STEP flags &= ~SUPPORT_VOLUME_SET + elif SERVICE_VOLUME_SET in self._cmds: + flags |= SUPPORT_VOLUME_SET if SERVICE_VOLUME_MUTE in self._cmds and \ ATTR_MEDIA_VOLUME_MUTED in self._attrs: @@ -376,7 +396,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): def set_volume_level(self, volume_level): """Set volume level, range 0..1.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} - self._call_service(SERVICE_VOLUME_SET, data) + self._call_service(SERVICE_VOLUME_SET, data, allow_override=True) def media_play(self): """Send play commmand.""" @@ -424,7 +444,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): def select_source(self, source): """Set the input source.""" data = {ATTR_INPUT_SOURCE: source} - self._call_service(SERVICE_SELECT_SOURCE, data) + self._call_service(SERVICE_SELECT_SOURCE, data, allow_override=True) def clear_playlist(self): """Clear players playlist.""" diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py new file mode 100644 index 00000000000..ee4fef3cfde --- /dev/null +++ b/homeassistant/components/media_player/vlc.py @@ -0,0 +1,152 @@ +""" +Provide functionality to interact with vlc devices on the network. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.vlc/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC) +from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED, + STATE_PLAYING) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +REQUIREMENTS = ['python-vlc==1.1.2'] + +_LOGGER = logging.getLogger(__name__) + + +SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PLAY_MEDIA + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the vlc platform.""" + add_devices([VlcDevice(config.get(CONF_NAME))]) + + +class VlcDevice(MediaPlayerDevice): + """Representation of a vlc player.""" + + def __init__(self, name): + """Initialize the vlc device.""" + import vlc + self._instance = vlc.Instance() + self._vlc = self._instance.media_player_new() + self._name = name + self._volume = None + self._muted = None + self._state = None + self._media_position_updated_at = None + self._media_position = None + self._media_duration = None + + def update(self): + """Get the latest details from the device.""" + import vlc + status = self._vlc.get_state() + if status == vlc.State.Playing: + self._state = STATE_PLAYING + elif status == vlc.State.Paused: + self._state = STATE_PAUSED + else: + self._state = STATE_IDLE + self._media_duration = self._vlc.get_length()/1000 + self._media_position = self._vlc.get_position() * self._media_duration + self._media_position_updated_at = dt_util.utcnow() + + self._volume = self._vlc.audio_get_volume() / 100 + self._muted = (self._vlc.audio_get_mute() == 1) + + return True + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_VLC + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._media_duration + + @property + def media_position(self): + """Position of current playing media in seconds.""" + return self._media_position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid.""" + return self._media_position_updated_at + + def media_seek(self, position): + """Seek the media to a specific location.""" + track_length = self._vlc.get_length()/1000 + self._vlc.set_position(position/track_length) + + def mute_volume(self, mute): + """Mute the volume.""" + self._vlc.audio_set_mute(mute) + self._muted = mute + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._vlc.audio_set_volume(int(volume * 100)) + self._volume = volume + + def media_play(self): + """Send play commmand.""" + self._vlc.play() + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self._vlc.pause() + self._state = STATE_PAUSED + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL or file.""" + if not media_type == MEDIA_TYPE_MUSIC: + _LOGGER.error( + "Invalid media type %s. Only %s is supported", + media_type, MEDIA_TYPE_MUSIC) + return + self._vlc.set_media(self._instance.media_new(media_id)) + self._vlc.play() + self._state = STATE_PLAYING diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index e131cad97cd..a2f49cc3f21 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -1,5 +1,5 @@ """ -Support for interface with an LG WebOS TV. +Support for interface with an LG webOS Smart TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.webostv/ @@ -12,53 +12,48 @@ import voluptuous as vol import homeassistant.util as util from homeassistant.components.media_player import ( + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( - CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED, + CONF_HOST, CONF_MAC, CONF_CUSTOMIZE, STATE_OFF, + STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN, CONF_NAME) from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' '/archive/v0.1.2.zip' - '#pylgtv==0.1.2'] + '#pylgtv==0.1.2', + 'websockets==3.2', + 'wakeonlan==0.2.2'] -_CONFIGURING = {} +_CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' -DEFAULT_NAME = 'LG WebOS Smart TV' +DEFAULT_NAME = 'LG webOS Smart TV' -SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA +SUPPORT_WEBOSTV = SUPPORT_TURN_OFF | \ + SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -WEBOS_APP_LIVETV = 'com.webos.app.livetv' -WEBOS_APP_YOUTUBE = 'youtube.leanback.v4' -WEBOS_APP_MAKO = 'makotv' - -WEBOS_APPS_SHORT = { - 'livetv': WEBOS_APP_LIVETV, - 'youtube': WEBOS_APP_YOUTUBE, - 'makotv': WEBOS_APP_MAKO -} - CUSTOMIZE_SCHEMA = vol.Schema({ vol.Optional(CONF_SOURCES): - vol.All(cv.ensure_list, [vol.In(WEBOS_APPS_SHORT)]), + vol.All(cv.ensure_list, [cv.string]), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, }) @@ -79,15 +74,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if host in _CONFIGURING: return + mac = config.get(CONF_MAC) name = config.get(CONF_NAME) customize = config.get(CONF_CUSTOMIZE) - setup_tv(host, name, customize, hass, add_devices) + setup_tv(host, mac, name, customize, hass, add_devices) -def setup_tv(host, name, customize, hass, add_devices): - """Setup a phue bridge based on host parameter.""" +def setup_tv(host, mac, name, customize, hass, add_devices): + """Setup a LG WebOS TV based on host parameter.""" from pylgtv import WebOsClient from pylgtv import PyLGTVPairException + from websockets.exceptions import ConnectionClosed client = WebOsClient(host) @@ -98,15 +95,16 @@ def setup_tv(host, name, customize, hass, add_devices): client.register() except PyLGTVPairException: _LOGGER.warning( - "Connected to LG WebOS TV %s but not paired", host) + "Connected to LG webOS TV %s but not paired", host) return - except OSError: + except (OSError, ConnectionClosed): _LOGGER.error("Unable to connect to host %s", host) return else: # Not registered, request configuration. - _LOGGER.warning("LG WebOS TV %s needs to be paired", host) - request_configuration(host, name, customize, hass, add_devices) + _LOGGER.warning("LG webOS TV %s needs to be paired", host) + request_configuration( + host, mac, name, customize, hass, add_devices) return # If we came here and configuring this host, mark as done. @@ -115,10 +113,11 @@ def setup_tv(host, name, customize, hass, add_devices): configurator = get_component('configurator') configurator.request_done(request_id) - add_devices([LgWebOSDevice(host, name, customize)]) + add_devices([LgWebOSDevice(host, mac, name, customize)], True) -def request_configuration(host, name, customize, hass, add_devices): +def request_configuration( + host, mac, name, customize, hass, add_devices): """Request configuration steps from the user.""" configurator = get_component('configurator') @@ -131,10 +130,10 @@ def request_configuration(host, name, customize, hass, add_devices): # pylint: disable=unused-argument def lgtv_configuration_callback(data): """The actions to do when our configuration callback is called.""" - setup_tv(host, name, customize, hass, add_devices) + setup_tv(host, mac, name, customize, hass, add_devices) _CONFIGURING[host] = configurator.request_config( - hass, 'LG WebOS TV', lgtv_configuration_callback, + hass, name, lgtv_configuration_callback, description='Click start and accept the pairing request on your TV.', description_image='/static/images/config_webos.png', submit_caption='Start pairing request' @@ -144,10 +143,13 @@ def request_configuration(host, name, customize, hass, add_devices): class LgWebOSDevice(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - def __init__(self, host, name, customize): + def __init__(self, host, mac, name, customize): """Initialize the webos device.""" from pylgtv import WebOsClient + from wakeonlan import wol self._client = WebOsClient(host) + self._wol = wol + self._mac = mac self._customize = customize self._name = name @@ -158,15 +160,14 @@ class LgWebOSDevice(MediaPlayerDevice): self._volume = 0 self._current_source = None self._current_source_id = None - self._source_list = None self._state = STATE_UNKNOWN - self._app_list = None - - self.update() + self._source_list = {} + self._app_list = {} @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data.""" + from websockets.exceptions import ConnectionClosed try: self._state = STATE_PLAYING self._muted = self._client.get_muted() @@ -175,20 +176,16 @@ class LgWebOSDevice(MediaPlayerDevice): self._source_list = {} self._app_list = {} - custom_sources = [] - for source in self._customize.get(CONF_SOURCES, []): - app_id = WEBOS_APPS_SHORT.get(source, None) - if app_id: - custom_sources.append(app_id) - else: - custom_sources.append(source) + custom_sources = self._customize.get(CONF_SOURCES, []) for app in self._client.get_apps(): self._app_list[app['id']] = app if app['id'] == self._current_source_id: self._current_source = app['title'] self._source_list[app['title']] = app - if app['id'] in custom_sources: + elif (app['id'] in custom_sources or + any(word in app['title'] for word in custom_sources) or + any(word in app['id'] for word in custom_sources)): self._source_list[app['title']] = app for source in self._client.get_inputs(): @@ -197,7 +194,7 @@ class LgWebOSDevice(MediaPlayerDevice): app = self._app_list[source['appId']] self._source_list[app['title']] = app - except OSError: + except (OSError, ConnectionClosed): self._state = STATE_OFF @property @@ -245,12 +242,23 @@ class LgWebOSDevice(MediaPlayerDevice): @property def supported_media_commands(self): """Flag of media commands that are supported.""" + if self._mac: + return SUPPORT_WEBOSTV | SUPPORT_TURN_ON return SUPPORT_WEBOSTV def turn_off(self): """Turn off media player.""" + from websockets.exceptions import ConnectionClosed self._state = STATE_OFF - self._client.power_off() + try: + self._client.power_off() + except (OSError, ConnectionClosed): + pass + + def turn_on(self): + """Turn on the media player.""" + if self._mac: + self._wol.send_magic_packet(self._mac) def volume_up(self): """Volume up the media player.""" diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index d6e0101e4e0..3bb98a00b87 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -18,7 +18,7 @@ from homeassistant.util import Throttle REQUIREMENTS = [ 'https://github.com/jabesq/netatmo-api-python/archive/' - 'v0.7.0.zip#lnetatmo==0.7.0'] + 'v0.8.0.zip#lnetatmo==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,7 @@ NETATMO_AUTH = None DEFAULT_DISCOVERY = True MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -72,10 +73,11 @@ class WelcomeData(object): self.auth = auth self.welcomedata = None self.camera_names = [] + self.module_names = [] self.home = home def get_camera_names(self): - """Return all module available on the API as a list.""" + """Return all camera available on the API as a list.""" self.camera_names = [] self.update() if not self.home: @@ -87,8 +89,24 @@ class WelcomeData(object): self.camera_names.append(camera['name']) return self.camera_names + def get_module_names(self, camera_name): + """Return all module available on the API as a list.""" + self.module_names = [] + self.update() + cam_id = self.welcomedata.cameraByName(camera=camera_name, + home=self.home)['id'] + for module in self.welcomedata.modules.values(): + if cam_id == module['cam_id']: + self.module_names.append(module['name']) + return self.module_names + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" import lnetatmo - self.welcomedata = lnetatmo.WelcomeData(self.auth) + self.welcomedata = lnetatmo.WelcomeData(self.auth, size=100) + + @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) + def update_event(self): + """Call the Netatmo API to update the list of events.""" + self.welcomedata.updateEvent(home=self.home) diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index 2ba9e7c72be..cd44b1f0cd3 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -77,7 +77,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.In(TRANSPARENCIES.keys()), vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(COLORS.keys()), - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), vol.Optional(CONF_INTERRUPT, default=DEFAULT_INTERRUPT): cv.boolean, }) diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 3171509b008..6ef9bc32990 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -9,6 +9,7 @@ import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage +import email.utils import voluptuous as vol @@ -18,6 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT) import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -134,6 +136,8 @@ class MailNotificationService(BaseNotificationService): msg['To'] = self.recipient msg['From'] = self._sender msg['X-Mailer'] = 'HomeAssistant' + msg['Date'] = email.utils.format_datetime(dt_util.now()) + msg['Message-Id'] = email.utils.make_msgid() return self._send_email(msg) diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 11719f8758a..c133506e775 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -19,7 +19,7 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-telegram-bot==5.2.0'] +REQUIREMENTS = ['python-telegram-bot==5.3.0'] ATTR_PHOTO = 'photo' ATTR_DOCUMENT = 'document' diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 5eb95ba16d1..c0012b4ac92 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -1,23 +1,25 @@ -"""Support for collecting data from the ARWN project. - -For more details about this platform, please refer to the -documentation at https://home-assistant.io/components/sensor.arwn/ +""" +Support for collecting data from the ARWN project. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.arwn/ """ import json import logging -from homeassistant.helpers.entity import Entity + import homeassistant.components.mqtt as mqtt from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS) +from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -DEPENDENCIES = ['mqtt'] +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mqtt'] +DOMAIN = 'arwn' -DOMAIN = "arwn" -TOPIC = 'arwn/#' SENSORS = {} -_LOGGER = logging.getLogger(__name__) +TOPIC = 'arwn/#' def discover_sensors(topic, payload): @@ -25,23 +27,23 @@ def discover_sensors(topic, payload): parts = topic.split('/') unit = payload.get('units', '') domain = parts[1] - if domain == "temperature": + if domain == 'temperature': name = parts[2] - if unit == "F": + if unit == 'F': unit = TEMP_FAHRENHEIT else: unit = TEMP_CELSIUS - return (ArwnSensor(name, 'temp', unit),) - if domain == "barometer": - return (ArwnSensor("Barometer", 'pressure', unit),) - if domain == "wind": - return (ArwnSensor("Wind Speed", 'speed', unit), - ArwnSensor("Wind Gust", 'gust', unit), - ArwnSensor("Wind Direction", 'direction', '°')) + return ArwnSensor(name, 'temp', unit) + if domain == 'barometer': + return ArwnSensor('Barometer', 'pressure', unit) + if domain == 'wind': + return (ArwnSensor('Wind Speed', 'speed', unit), + ArwnSensor('Wind Gust', 'gust', unit), + ArwnSensor('Wind Direction', 'direction', '°')) def _slug(name): - return "sensor.arwn_%s" % slugify(name) + return 'sensor.arwn_{}'.format(slugify(name)) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -84,7 +86,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ArwnSensor(Entity): - """Represents an ARWN sensor.""" + """Representation of an ARWN sensor.""" def __init__(self, name, state_key, units): """Initialize the sensor.""" diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py new file mode 100644 index 00000000000..53aac3d353a --- /dev/null +++ b/homeassistant/components/sensor/broadlink.py @@ -0,0 +1,130 @@ + +""" +Support for the Broadlink RM2 Pro (only temperature) and A1 devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.broadlink/ +""" +from datetime import timedelta +import binascii +import logging +import socket +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_HOST, CONF_MAC, + CONF_MONITORED_CONDITIONS, + CONF_NAME, TEMP_CELSIUS, CONF_TIMEOUT) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['broadlink==0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_UPDATE_INTERVAL = 'update_interval' +DEVICE_DEFAULT_NAME = 'Broadlink sensor' +DEFAULT_TIMEOUT = 10 + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_CELSIUS], + 'air_quality': ['Air Quality', ' '], + 'humidity': ['Humidity', '%'], + 'light': ['Light', ' '], + 'noise': ['Noise', ' '] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( + vol.All(cv.time_period, cv.positive_timedelta)), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Broadlink device sensors.""" + mac = config.get(CONF_MAC).encode().replace(b':', b'') + mac_addr = binascii.unhexlify(mac) + broadlink_data = BroadlinkData( + config.get(CONF_UPDATE_INTERVAL), + config.get(CONF_HOST), + mac_addr, config.get(CONF_TIMEOUT)) + + dev = [] + for variable in config[CONF_MONITORED_CONDITIONS]: + dev.append(BroadlinkSensor( + config.get(CONF_NAME), + broadlink_data, + variable)) + add_devices(dev) + + +class BroadlinkSensor(Entity): + """Representation of a Broadlink device sensor.""" + + def __init__(self, name, broadlink_data, sensor_type): + """Initialize the sensor.""" + self._name = "%s %s" % (name, SENSOR_TYPES[sensor_type][0]) + self._state = None + self._type = sensor_type + self._broadlink_data = broadlink_data + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data from the sensor.""" + self._broadlink_data.update() + if self._broadlink_data.data is None: + return + self._state = self._broadlink_data.data[self._type] + + +class BroadlinkData(object): + """Representation of a Broadlink data object.""" + + def __init__(self, interval, ip_addr, mac_addr, timeout): + """Initialize the data object.""" + import broadlink + self.data = None + self._device = broadlink.a1((ip_addr, 80), mac_addr) + self._device.timeout = timeout + self.update = Throttle(interval)(self._update) + try: + self._device.auth() + except socket.timeout: + _LOGGER.error("Failed to connect to device.") + + def _update(self, retry=2): + try: + self.data = self._device.check_sensors_raw() + except socket.timeout as error: + if retry < 1: + _LOGGER.error(error) + return + try: + self._device.auth() + except socket.timeout: + pass + return self._update(max(0, retry-1)) diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 61545fa3944..eee43328f29 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -77,7 +77,7 @@ class CoinMarketCapSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._ticker.get('price_usd') + return round(float(self._ticker.get('price_usd')), 2) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index c57b4ba30e8..173990a6a2f 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -104,19 +104,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): else: units = 'us' - # Create a data fetcher to support all of the configured sensors. Then make - # the first call to init the data and confirm we can connect. - try: - forecast_data = DarkSkyData( - api_key=config.get(CONF_API_KEY, None), - latitude=hass.config.latitude, - longitude=hass.config.longitude, - units=units, - interval=config.get(CONF_UPDATE_INTERVAL)) - forecast_data.update() - forecast_data.update_currently() - except ValueError as error: - _LOGGER.error(error) + forecast_data = DarkSkyData( + api_key=config.get(CONF_API_KEY, None), + latitude=hass.config.latitude, + longitude=hass.config.longitude, + units=units, + interval=config.get(CONF_UPDATE_INTERVAL)) + forecast_data.update() + forecast_data.update_currently() + + # If connection failed don't setup platform. + if forecast_data.data is None: return False name = config.get(CONF_NAME) @@ -227,7 +225,10 @@ class DarkSkySensor(Entity): If the sensor type is unknown, the current state is returned. """ lookup_type = convert_to_camel(self.type) - state = getattr(data, lookup_type, 0) + state = getattr(data, lookup_type, None) + + if state is None: + return state # Some state data needs to be rounded to whole values or converted to # percentages @@ -284,21 +285,22 @@ class DarkSkyData(object): self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, units=self.units) except (ConnectError, HTTPError, Timeout, ValueError) as error: - raise ValueError("Unable to init Dark Sky. %s", error) - self.unit_system = self.data.json['flags']['units'] + _LOGGER.error("Unable to connect to Dark Sky. %s", error) + self.data = None + self.unit_system = self.data and self.data.json['flags']['units'] def _update_currently(self): """Update currently data.""" - self.data_currently = self.data.currently() + self.data_currently = self.data and self.data.currently() def _update_minutely(self): """Update minutely data.""" - self.data_minutely = self.data.minutely() + self.data_minutely = self.data and self.data.minutely() def _update_hourly(self): """Update hourly data.""" - self.data_hourly = self.data.hourly() + self.data_hourly = self.data and self.data.hourly() def _update_daily(self): """Update daily data.""" - self.data_daily = self.data.daily() + self.data_daily = self.data and self.data.daily() diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 8d27f7188d2..0c42033006c 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -26,15 +26,15 @@ stores/caches the latest telegram and notifies the Entities that the telegram has been updated. """ import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import voluptuous as vol _LOGGER = logging.getLogger(__name__) @@ -65,30 +65,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Suppress logging logging.getLogger('dsmr_parser').setLevel(logging.ERROR) - from dsmr_parser import obis_references as obis + from dsmr_parser import obis_references as obis_ref from dsmr_parser.protocol import create_dsmr_reader dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities obis_mapping = [ - ['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE], - ['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY], - ['Power Tariff', obis.ELECTRICITY_ACTIVE_TARIFF], - ['Power Consumption (low)', obis.ELECTRICITY_USED_TARIFF_1], - ['Power Consumption (normal)', obis.ELECTRICITY_USED_TARIFF_2], - ['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1], - ['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2], + ['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE], + ['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY], + ['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF], + ['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1], + ['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2], + ['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], + ['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], ] - # Protocol version specific obis - if dsmr_version == '4': - obis_mapping.append(['Gas Consumption', obis.HOURLY_GAS_METER_READING]) - else: - obis_mapping.append(['Gas Consumption', obis.GAS_METER_READING]) # Generate device entities devices = [DSMREntity(name, obis) for name, obis in obis_mapping] + # Protocol version specific obis + if dsmr_version == '4': + gas_obis = obis_ref.HOURLY_GAS_METER_READING + else: + gas_obis = obis_ref.GAS_METER_READING + + # add gas meter reading and derivative for usage + devices += [ + DSMREntity('Gas Consumption', gas_obis), + DerivativeDSMREntity('Hourly Gas Consumption', gas_obis), + ] + yield from async_add_devices(devices) def update_entities_telegram(telegram): @@ -151,7 +158,10 @@ class DSMREntity(Entity): if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) else: - return value + if value: + return value + else: + return STATE_UNKNOWN @property def unit_of_measurement(self): @@ -168,4 +178,55 @@ class DSMREntity(Entity): elif value == '0001': return 'low' else: - return None + return STATE_UNKNOWN + + +class DerivativeDSMREntity(DSMREntity): + """Calculated derivative for values where the DSMR doesn't offer one. + + Gas readings are only reported per hour and don't offer a rate only + the current meter reading. This entity converts subsequents readings + into a hourly rate. + """ + + _previous_reading = None + _previous_timestamp = None + _state = STATE_UNKNOWN + + @property + def state(self): + """Return the calculated current hourly rate.""" + return self._state + + @asyncio.coroutine + def async_update(self): + """Recalculate hourly rate if timestamp has changed. + + DSMR updates gas meter reading every hour. Along with the + new value a timestamp is provided for the reading. Test + if the last known timestamp differs from the current one + then calculate a new rate for the previous hour. + """ + # check if the timestamp for the object differs from the previous one + timestamp = self.get_dsmr_object_attr('datetime') + if timestamp and timestamp != self._previous_timestamp: + current_reading = self.get_dsmr_object_attr('value') + + if self._previous_reading is None: + # can't calculate rate without previous datapoint + # just store current point + pass + else: + # recalculate the rate + diff = current_reading - self._previous_reading + self._state = diff + + self._previous_reading = current_reading + self._previous_timestamp = timestamp + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, per hour, if any.""" + unit = self.get_dsmr_object_attr('unit') + if unit: + return unit + '/h' diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 421940b9c7d..7029be9fca2 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,7 +14,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['eliqonline==1.0.12'] +REQUIREMENTS = ['eliqonline==1.0.13'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index 0390ea0e9d6..a055d1795e1 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -22,6 +22,7 @@ CONF_SECOND = 'second' CONF_MINUTE = 'minute' CONF_HOUR = 'hour' CONF_DAY = 'day' +CONF_MANUAL = 'manual' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SECOND, default=[0]): @@ -32,6 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), vol.Optional(CONF_DAY): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, }) @@ -104,11 +106,12 @@ class SpeedtestData(object): def __init__(self, hass, config): """Initialize the data object.""" self.data = None - track_time_change(hass, self.update, - second=config.get(CONF_SECOND), - minute=config.get(CONF_MINUTE), - hour=config.get(CONF_HOUR), - day=config.get(CONF_DAY)) + if not config.get(CONF_MANUAL): + track_time_change(hass, self.update, + second=config.get(CONF_SECOND), + minute=config.get(CONF_MINUTE), + hour=config.get(CONF_HOUR), + day=config.get(CONF_DAY)) def update(self, now): """Get the latest data from fast.com.""" diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index e76b8ed07ed..5769860284c 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -95,6 +95,9 @@ def get_next_departure(sched, start_station_id, end_station_id): for row in result: item = row + if item == {}: + return None + today = datetime.datetime.today().strftime('%Y-%m-%d') departure_time_string = '{} {}'.format(today, item[2]) arrival_time_string = '{} {}'.format(today, item[3]) @@ -221,6 +224,13 @@ class GTFSDepartureSensor(Entity): with self.lock: self._departure = get_next_departure(self._pygtfs, self.origin, self.destination) + if not self._departure: + self._state = 0 + self._attributes = {'Info': 'No more bus today'} + if self._name == '': + self._name = (self._custom_name or "GTFS Sensor") + return + self._state = self._departure['minutes_until_departure'] origin_station = self._departure['origin_station'] diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index b9dde96a98c..f7bbf41cff9 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -18,7 +18,8 @@ from homeassistant.const import ( DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', - 'operation_mode'] + 'operation_mode', + 'hvac_state'] SENSOR_TYPES_DEPRECATED = ['last_ip', 'local_ip', diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index c3503207d8b..20c0f94a500 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -38,14 +38,19 @@ SENSOR_TYPES = { 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], 'battery_vp': ['Battery', '', 'mdi:battery'], + 'battery_lvl': ['Battery_lvl', '', 'mdi:battery'], 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], 'WindAngle': ['Angle', '', 'mdi:compass'], + 'WindAngle_value': ['Angle Value', 'º', 'mdi:compass'], 'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'], 'GustAngle': ['Gust Angle', '', 'mdi:compass'], + 'GustAngle_value': ['Gust Angle Value', 'º', 'mdi:compass'], 'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], 'rf_status': ['Radio', '', 'mdi:signal'], - 'wifi_status': ['Wifi', '', 'mdi:wifi'] + 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal'], + 'wifi_status': ['Wifi', '', 'mdi:wifi'], + 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi'] } MODULE_SCHEMA = vol.Schema({ @@ -103,6 +108,7 @@ class NetAtmoSensor(Entity): self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] + self.module_id = module_id[1] self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format(self._name, module_id, self.type) @@ -154,21 +160,58 @@ class NetAtmoSensor(Entity): self._state = data['CO2'] elif self.type == 'pressure': self._state = round(data['Pressure'], 1) - elif self.type == 'battery_vp': + elif self.type == 'battery_lvl': + self._state = data['battery_vp'] + elif self.type == 'battery_vp' and self.module_id == '6': + if data['battery_vp'] >= 5590: + self._state = "Full" + elif data['battery_vp'] >= 5180: + self._state = "High" + elif data['battery_vp'] >= 4770: + self._state = "Medium" + elif data['battery_vp'] >= 4360: + self._state = "Low" + elif data['battery_vp'] < 4360: + self._state = "Very Low" + elif self.type == 'battery_vp' and self.module_id == '5': if data['battery_vp'] >= 5500: self._state = "Full" - elif data['battery_vp'] >= 5100: + elif data['battery_vp'] >= 5000: self._state = "High" - elif data['battery_vp'] >= 4600: + elif data['battery_vp'] >= 4500: self._state = "Medium" - elif data['battery_vp'] >= 4100: + elif data['battery_vp'] >= 4000: self._state = "Low" - elif data['battery_vp'] < 4100: + elif data['battery_vp'] < 4000: + self._state = "Very Low" + elif self.type == 'battery_vp' and self.module_id == '3': + if data['battery_vp'] >= 5640: + self._state = "Full" + elif data['battery_vp'] >= 5280: + self._state = "High" + elif data['battery_vp'] >= 4920: + self._state = "Medium" + elif data['battery_vp'] >= 4560: + self._state = "Low" + elif data['battery_vp'] < 4560: + self._state = "Very Low" + elif self.type == 'battery_vp' and self.module_id == '2': + if data['battery_vp'] >= 5500: + self._state = "Full" + elif data['battery_vp'] >= 5000: + self._state = "High" + elif data['battery_vp'] >= 4500: + self._state = "Medium" + elif data['battery_vp'] >= 4000: + self._state = "Low" + elif data['battery_vp'] < 4000: self._state = "Very Low" elif self.type == 'min_temp': self._state = data['min_temp'] elif self.type == 'max_temp': self._state = data['max_temp'] + elif self.type == 'WindAngle_value': + self._state = data['WindAngle'] elif self.type == 'WindAngle': if data['WindAngle'] >= 330: self._state = "North (%d\xb0)" % data['WindAngle'] @@ -190,6 +233,8 @@ class NetAtmoSensor(Entity): self._state = "North (%d\xb0)" % data['WindAngle'] elif self.type == 'WindStrength': self._state = data['WindStrength'] + elif self.type == 'GustAngle_value': + self._state = data['GustAngle'] elif self.type == 'GustAngle': if data['GustAngle'] >= 330: self._state = "North (%d\xb0)" % data['GustAngle'] @@ -211,6 +256,8 @@ class NetAtmoSensor(Entity): self._state = "North (%d\xb0)" % data['GustAngle'] elif self.type == 'GustStrength': self._state = data['GustStrength'] + elif self.type == 'rf_status_lvl': + self._state = data['rf_status'] elif self.type == 'rf_status': if data['rf_status'] >= 90: self._state = "Low" @@ -220,13 +267,17 @@ class NetAtmoSensor(Entity): self._state = "High" elif data['rf_status'] <= 59: self._state = "Full" + elif self.type == 'wifi_status_lvl': + self._state = data['wifi_status'] elif self.type == 'wifi_status': if data['wifi_status'] >= 86: - self._state = "Bad" + self._state = "Low" elif data['wifi_status'] >= 71: - self._state = "Middle" - elif data['wifi_status'] <= 70: - self._state = "Good" + self._state = "Medium" + elif data['wifi_status'] >= 56: + self._state = "High" + elif data['wifi_status'] <= 55: + self._state = "Full" class NetAtmoData(object): @@ -248,7 +299,7 @@ class NetAtmoData(object): def update(self): """Call the Netatmo API to update the data.""" import lnetatmo - self.station_data = lnetatmo.DeviceList(self.auth) + self.station_data = lnetatmo.WeatherStationData(self.auth) if self.station is not None: self.data = self.station_data.lastData(station=self.station, diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py new file mode 100644 index 00000000000..3a87eeb5ceb --- /dev/null +++ b/homeassistant/components/sensor/netdata.py @@ -0,0 +1,147 @@ +""" +Support gathering system information of hosts which are running netdata. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.netdata/ +""" +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_PORT, STATE_UNKNOWN, CONF_NAME, CONF_RESOURCES) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'api/v1' +_REALTIME = 'before=0&after=-1&options=seconds' + +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'Netdata' +DEFAULT_PORT = '19999' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +SENSOR_TYPES = { + 'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1], + 'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1], + 'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1], + 'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1], + 'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1], + 'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1], + 'processes_running': ['Processes Running', 'Count', 'system.processes', + 'running', 0], + 'processes_blocked': ['Processes Blocked', 'Count', 'system.processes', + 'blocked', 0], + 'system_load': ['System Load', '15 min', 'system.processes', 'running', 2], + 'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0], + 'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_RESOURCES, default=['memory_free']): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +# pylint: disable=unused-variable +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Netdata sensor.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + url = 'http://{}:{}'.format(host, port) + version_url = '{}/version.txt'.format(url) + data_url = '{}/{}/data?chart='.format(url, _RESOURCE) + resources = config.get(CONF_RESOURCES) + + try: + response = requests.get(version_url, timeout=10) + if not response.ok: + _LOGGER.error("Response status is '%s'", response.status_code) + return False + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint: %s", url) + return False + + values = {} + for key, value in sorted(SENSOR_TYPES.items()): + if key in resources: + values.setdefault(value[2], []).append(key) + + dev = [] + for chart in values: + rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME) + rest = NetdataData(rest_url) + for sensor_type in values[chart]: + dev.append(NetdataSensor(rest, name, sensor_type)) + + add_devices(dev) + + +class NetdataSensor(Entity): + """Implementation of a Netdata sensor.""" + + def __init__(self, rest, name, sensor_type): + """Initialize the sensor.""" + self.rest = rest + self.type = sensor_type + self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0]) + self._precision = SENSOR_TYPES[self.type][4] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self.update() + + @property + def name(self): + """The name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the resources.""" + value = self.rest.data + + if value is not None: + netdata_id = SENSOR_TYPES[self.type][3] + if netdata_id in value: + return "{0:.{1}f}".format(value[netdata_id], self._precision) + else: + return STATE_UNKNOWN + + def update(self): + """Get the latest data from Netdata REST API.""" + self.rest.update() + + +class NetdataData(object): + """The class for handling the data retrieval.""" + + def __init__(self, resource): + """Initialize the data object.""" + self._resource = resource + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from the Netdata REST API.""" + try: + response = requests.get(self._resource, timeout=5) + det = response.json() + self.data = {k: v for k, v in zip(det['labels'], det['data'][0])} + + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to host/endpoint: %s", self._resource) + self.data = None diff --git a/homeassistant/components/sensor/sensehat.py b/homeassistant/components/sensor/sensehat.py new file mode 100644 index 00000000000..50cd233b39b --- /dev/null +++ b/homeassistant/components/sensor/sensehat.py @@ -0,0 +1,134 @@ +""" +Support for Sense HAT sensors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.sensehat +""" +import os +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['sense-hat==2.2.0'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'sensehat' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +SENSOR_TYPES = { + 'temperature': ['temperature', TEMP_CELSIUS], + 'humidity': ['humidity', "%"], + 'pressure': ['pressure', "mb"], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_TYPES): + [vol.In(SENSOR_TYPES)], + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def get_cpu_temp(): + """Get CPU temperature.""" + res = os.popen("vcgencmd measure_temp").readline() + t_cpu = float(res.replace("temp=", "").replace("'C\n", "")) + return t_cpu + + +def get_average(temp_base): + """Use moving average to get better readings.""" + if not hasattr(get_average, "temp"): + get_average.temp = [temp_base, temp_base, temp_base] + get_average.temp[2] = get_average.temp[1] + get_average.temp[1] = get_average.temp[0] + get_average.temp[0] = temp_base + temp_avg = (get_average.temp[0]+get_average.temp[1]+get_average.temp[2])/3 + return temp_avg + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + data = SenseHatData() + dev = [] + + for variable in config[CONF_DISPLAY_OPTIONS]: + dev.append(SenseHatSensor(data, variable)) + + add_devices(dev) + + +class SenseHatSensor(Entity): + """Representation of a sensehat sensor.""" + + def __init__(self, data, sensor_types): + """Initialize the sensor.""" + self.data = data + self._name = SENSOR_TYPES[sensor_types][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_types][1] + self.type = sensor_types + self._state = None + """updating data.""" + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + if not self.data.humidity: + _LOGGER.error("Don't receive data!") + return + + if self.type == 'temperature': + self._state = self.data.temperature + if self.type == 'humidity': + self._state = self.data.humidity + if self.type == 'pressure': + self._state = self.data.pressure + + +class SenseHatData(object): + """Get the latest data and update.""" + + def __init__(self): + """Initialize the data object.""" + self.temperature = None + self.humidity = None + self.pressure = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from sensehat.""" + from sense_hat import SenseHat + sense = SenseHat() + temp_from_h = sense.get_temperature_from_humidity() + temp_from_p = sense.get_temperature_from_pressure() + t_cpu = get_cpu_temp() + t_total = (temp_from_h+temp_from_p)/2 + t_correct = t_total - ((t_cpu-t_total)/1.5) + t_correct = get_average(t_correct) + self.temperature = t_correct + self.humidity = sense.get_humidity() + self.pressure = sense.get_pressure() diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 3087c74d474..dff6f1c9dde 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -31,6 +31,7 @@ CONF_MINUTE = 'minute' CONF_HOUR = 'hour' CONF_DAY = 'day' CONF_SERVER_ID = 'server_id' +CONF_MANUAL = 'manual' SENSOR_TYPES = { 'ping': ['Ping', 'ms'], @@ -50,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), vol.Optional(CONF_DAY): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, }) @@ -135,11 +137,12 @@ class SpeedtestData(object): """Initialize the data object.""" self.data = None self._server_id = config.get(CONF_SERVER_ID) - track_time_change(hass, self.update, - second=config.get(CONF_SECOND), - minute=config.get(CONF_MINUTE), - hour=config.get(CONF_HOUR), - day=config.get(CONF_DAY)) + if not config.get(CONF_MANUAL): + track_time_change(hass, self.update, + second=config.get(CONF_SECOND), + minute=config.get(CONF_MINUTE), + hour=config.get(CONF_HOUR), + day=config.get(CONF_DAY)) def update(self, now): """Get the latest data from speedtest.net.""" diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index 6dc23a71d9b..8ee0d6eae18 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -107,7 +107,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # Handle all Volumes volumes = config['volumes'] if volumes is None: - volumes = api.storage().volumes + volumes = api.storage.volumes for volume in volumes: sensors += [SynoNasStorageSensor(api, variable, @@ -119,7 +119,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # Handle all Disks disks = config['disks'] if disks is None: - disks = api.storage().disks + disks = api.storage.disks for disk in disks: sensors += [SynoNasStorageSensor(api, variable, diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 14eadb48984..b59c5d0cd43 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -27,14 +27,14 @@ SENSOR_TYPES = { 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'], 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'], 'processor_use': ['CPU Use', '%', 'mdi:memory'], - 'process': ['Process', '', 'mdi:memory'], + 'process': ['Process', ' ', 'mdi:memory'], 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'], 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], 'network_out': ['Sent', 'MiB', 'mdi:server-network'], 'network_in': ['Received', 'MiB', 'mdi:server-network'], - 'packets_out': ['Packets sent', '', 'mdi:server-network'], - 'packets_in': ['Packets received', '', 'mdi:server-network'], + 'packets_out': ['Packets sent', ' ', 'mdi:server-network'], + 'packets_in': ['Packets received', ' ', 'mdi:server-network'], 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], 'last_boot': ['Last Boot', '', 'mdi:clock'], diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 915bbac429f..abc5843ad91 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -6,37 +6,32 @@ https://home-assistant.io/components/sensor.tellduslive/ """ import logging -from datetime import datetime -from homeassistant.components import tellduslive -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME, TEMP_CELSIUS) -from homeassistant.helpers.entity import Entity - -ATTR_LAST_UPDATED = "time_last_updated" +from homeassistant.components.tellduslive import TelldusLiveEntity +from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) -SENSOR_TYPE_TEMP = "temp" -SENSOR_TYPE_HUMIDITY = "humidity" -SENSOR_TYPE_RAINRATE = "rrate" -SENSOR_TYPE_RAINTOTAL = "rtot" -SENSOR_TYPE_WINDDIRECTION = "wdir" -SENSOR_TYPE_WINDAVERAGE = "wavg" -SENSOR_TYPE_WINDGUST = "wgust" -SENSOR_TYPE_WATT = "watt" -SENSOR_TYPE_LUMINANCE = "lum" +SENSOR_TYPE_TEMP = 'temp' +SENSOR_TYPE_HUMIDITY = 'humidity' +SENSOR_TYPE_RAINRATE = 'rrate' +SENSOR_TYPE_RAINTOTAL = 'rtot' +SENSOR_TYPE_WINDDIRECTION = 'wdir' +SENSOR_TYPE_WINDAVERAGE = 'wavg' +SENSOR_TYPE_WINDGUST = 'wgust' +SENSOR_TYPE_WATT = 'watt' +SENSOR_TYPE_LUMINANCE = 'lum' SENSOR_TYPES = { - SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, "mdi:thermometer"], - SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], - SENSOR_TYPE_WATT: ['Watt', 'W', ""], - SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ""], + SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', 'mdi:water'], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', 'mdi:water'], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water'], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ''], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ''], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ''], + SENSOR_TYPE_WATT: ['Watt', 'W', ''], + SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ''], } @@ -44,114 +39,75 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick sensors.""" if discovery_info is None: return - add_devices(TelldusLiveSensor(sensor) for sensor in discovery_info) + add_devices(TelldusLiveSensor(hass, sensor) for sensor in discovery_info) -class TelldusLiveSensor(Entity): +class TelldusLiveSensor(TelldusLiveEntity): """Representation of a Telldus Live sensor.""" - def __init__(self, sensor_id): - """Initialize the sensor.""" - self._id = sensor_id - self.update() - _LOGGER.debug("created sensor %s", self) - - def update(self): - """Update sensor values.""" - tellduslive.NETWORK.update_sensors() - self._sensor = tellduslive.NETWORK.get_sensor(self._id) + @property + def device_id(self): + """Return id of the device.""" + return self._id[0] @property - def _sensor_name(self): - """Return the name of the sensor.""" - return self._sensor["name"] - - @property - def _sensor_value(self): - """Return the value the sensor.""" - return self._sensor["data"]["value"] - - @property - def _sensor_type(self): + def _type(self): """Return the type of the sensor.""" - return self._sensor["data"]["name"] + return self._id[1] @property - def _battery_level(self): - """Return the battery level of a sensor.""" - sensor_battery_level = self._sensor.get("battery") - return round(sensor_battery_level * 100 / 255) \ - if sensor_battery_level else None - - @property - def _last_updated(self): - """Return the last update.""" - sensor_last_updated = self._sensor.get("lastUpdated") - return str(datetime.fromtimestamp(sensor_last_updated)) \ - if sensor_last_updated else None + def _value(self): + """Return value of the sensor.""" + return self.device.value(self._id[1:]) @property def _value_as_temperature(self): """Return the value as temperature.""" - return round(float(self._sensor_value), 1) + return round(float(self._value), 1) @property def _value_as_luminance(self): """Return the value as luminance.""" - return round(float(self._sensor_value), 1) + return round(float(self._value), 1) @property def _value_as_humidity(self): """Return the value as humidity.""" - return int(round(float(self._sensor_value))) + return int(round(float(self._value))) @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._sensor_name or DEVICE_DEFAULT_NAME, - self.quantity_name or "") - - @property - def available(self): - """Return true if the sensor is available.""" - return not self._sensor.get("offline", False) + return '{} {}'.format( + super().name, + self.quantity_name or '') @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == SENSOR_TYPE_TEMP: + if self._type == SENSOR_TYPE_TEMP: return self._value_as_temperature - elif self._sensor_type == SENSOR_TYPE_HUMIDITY: + elif self._type == SENSOR_TYPE_HUMIDITY: return self._value_as_humidity - elif self._sensor_type == SENSOR_TYPE_LUMINANCE: + elif self._type == SENSOR_TYPE_LUMINANCE: return self._value_as_luminance else: - return self._sensor_value - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = {} - if self._battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self._battery_level - if self._last_updated is not None: - attrs[ATTR_LAST_UPDATED] = self._last_updated - return attrs + return self._value @property def quantity_name(self): """Name of quantity.""" - return SENSOR_TYPES[self._sensor_type][0] \ - if self._sensor_type in SENSOR_TYPES else None + return SENSOR_TYPES[self._type][0] \ + if self._type in SENSOR_TYPES else None @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._sensor_type][1] \ - if self._sensor_type in SENSOR_TYPES else None + return SENSOR_TYPES[self._type][1] \ + if self._type in SENSOR_TYPES else None @property def icon(self): """Return the icon.""" - return SENSOR_TYPES[self._sensor_type][2] \ - if self._sensor_type in SENSOR_TYPES else None + return SENSOR_TYPES[self._type][2] \ + if self._type in SENSOR_TYPES else None diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 3194afbe94e..67e19f225d5 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -19,12 +19,15 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/' -_ALERTS = 'http://api.wunderground.com/api/{}/alerts/q/' +_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/{}/q/' +_ALERTS = 'http://api.wunderground.com/api/{}/alerts/{}/q/' _LOGGER = logging.getLogger(__name__) CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" CONF_PWS_ID = 'pws_id' +CONF_LANG = 'lang' + +DEFAULT_LANG = 'EN' MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5) @@ -80,9 +83,29 @@ ALERTS_ATTRS = [ 'message', ] +# Language Supported Codes +LANG_CODES = [ + 'AF', 'AL', 'AR', 'HY', 'AZ', 'EU', + 'BY', 'BU', 'LI', 'MY', 'CA', 'CN', + 'TW', 'CR', 'CZ', 'DK', 'DV', 'NL', + 'EN', 'EO', 'ET', 'FA', 'FI', 'FR', + 'FC', 'GZ', 'DL', 'KA', 'GR', 'GU', + 'HT', 'IL', 'HI', 'HU', 'IS', 'IO', + 'ID', 'IR', 'IT', 'JP', 'JW', 'KM', + 'KR', 'KU', 'LA', 'LV', 'LT', 'ND', + 'MK', 'MT', 'GM', 'MI', 'MR', 'MN', + 'NO', 'OC', 'PS', 'GN', 'PL', 'BR', + 'PA', 'PU', 'RO', 'RU', 'SR', 'SK', + 'SL', 'SP', 'SI', 'SW', 'CH', 'TL', + 'TT', 'TH', 'UA', 'UZ', 'VU', 'CY', + 'SN', 'JI', 'YI', +] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, + vol.Optional(CONF_LANG, default=DEFAULT_LANG): + vol.All(vol.In(LANG_CODES)), vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) @@ -92,7 +115,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the WUnderground sensor.""" rest = WUndergroundData(hass, config.get(CONF_API_KEY), - config.get(CONF_PWS_ID, None)) + config.get(CONF_PWS_ID), + config.get(CONF_LANG)) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: sensors.append(WUndergroundSensor(rest, variable)) @@ -172,7 +196,7 @@ class WUndergroundSensor(Entity): @property def entity_picture(self): """Return the entity picture.""" - if self._condition == 'weather': + if self.rest.data and self._condition == 'weather': url = self.rest.data['icon_url'] return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) @@ -192,18 +216,19 @@ class WUndergroundSensor(Entity): class WUndergroundData(object): """Get data from WUnderground.""" - def __init__(self, hass, api_key, pws_id=None): + def __init__(self, hass, api_key, pws_id, lang): """Initialize the data object.""" self._hass = hass self._api_key = api_key self._pws_id = pws_id + self._lang = 'lang:{}'.format(lang) self._latitude = hass.config.latitude self._longitude = hass.config.longitude self.data = None self.alerts = None def _build_url(self, baseurl=_RESOURCE): - url = baseurl.format(self._api_key) + url = baseurl.format(self._api_key, self._lang) if self._pws_id: url = url + 'pws:{}'.format(self._pws_id) else: diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py new file mode 100644 index 00000000000..6bb9dd0748d --- /dev/null +++ b/homeassistant/components/sensor/zamg.py @@ -0,0 +1,178 @@ +""" +Sensor for data from Austrian "Zentralanstalt für Meteorologie und Geodynamik". + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.zamg/ +""" +import csv +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED) +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, CONF_NAME, __version__) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +ATTR_STATION = 'station' +ATTR_UPDATED = 'updated' +ATTRIBUTION = 'Data provided by ZAMG' + +CONF_STATION_ID = 'station_id' + +DEFAULT_NAME = 'zamg' + +# Data source only updates once per hour, so throttle to 30 min to have +# reasonably recent data +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +VALID_STATION_IDS = ( + '11010', '11012', '11022', '11035', '11036', '11101', '11121', '11126', + '11130', '11150', '11155', '11157', '11171', '11190', '11204' +) + +SENSOR_TYPES = { + ATTR_WEATHER_PRESSURE: ('Pressure', 'hPa', 'LDstat hPa', float), + 'pressure_sealevel': ('Pressure at Sea Level', 'hPa', 'LDred hPa', float), + ATTR_WEATHER_HUMIDITY: ('Humidity', '%', 'RF %', int), + ATTR_WEATHER_WIND_SPEED: ('Wind Speed', 'km/h', 'WG km/h', float), + ATTR_WEATHER_WIND_BEARING: ('Wind Bearing', '°', 'WR °', int), + 'wind_max_speed': ('Top Wind Speed', 'km/h', 'WSG km/h', float), + 'wind_max_bearing': ('Top Wind Bearing', '°', 'WSR °', int), + 'sun_last_hour': ('Sun Last Hour', '%', 'SO %', int), + ATTR_WEATHER_TEMPERATURE: ('Temperature', '°C', 'T °C', float), + 'precipitation': ('Precipitation', 'l/m²', 'N l/m²', float), + 'dewpoint': ('Dew Point', '°C', 'TP °C', float), + # The following probably not useful for general consumption, + # but we need them to fill in internal attributes + 'station_name': ('Station Name', None, 'Name', str), + 'station_elevation': ('Station Elevation', 'm', 'Höhe m', int), + 'update_date': ('Update Date', None, 'Datum', str), + 'update_time': ('Update Time', None, 'Zeit', str), +} + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_STATION_ID): + vol.All(cv.string, vol.In(VALID_STATION_IDS)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the ZAMG sensor platform.""" + station_id = config.get(CONF_STATION_ID) + name = config.get(CONF_NAME) + + logger = logging.getLogger(__name__) + probe = ZamgData(station_id=station_id, logger=logger) + + sensors = [ZamgSensor(probe, variable, name) + for variable in config[CONF_MONITORED_CONDITIONS]] + + add_devices(sensors, True) + + +class ZamgSensor(Entity): + """Implementation of a ZAMG sensor.""" + + def __init__(self, probe, variable, name): + """Initialize the sensor.""" + self.probe = probe + self.client_name = name + self.variable = variable + self.update() + + def update(self): + """Delegate update to probe.""" + self.probe.update() + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format(self.client_name, self.variable) + + @property + def state(self): + """Return the state of the sensor.""" + return self.probe.get_data(self.variable) + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self.variable][1] + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION, + ATTR_STATION: self.probe.get_data('station_name'), + ATTR_UPDATED: '{} {}'.format(self.probe.get_data('update_date'), + self.probe.get_data('update_time')), + } + + +class ZamgData(object): + """The class for handling the data retrieval.""" + + API_URL = 'http://www.zamg.ac.at/ogd/' + API_FIELDS = { + v[2]: (k, v[3]) + for k, v in SENSOR_TYPES.items() + } + API_HEADERS = { + 'User-Agent': '{} {}'.format('home-assistant.zamg/', __version__), + } + + def __init__(self, logger, station_id): + """Initialize the probe.""" + self._logger = logger + self._station_id = station_id + self.data = {} + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from ZAMG.""" + try: + response = requests.get( + self.API_URL, headers=self.API_HEADERS, timeout=15) + except requests.exceptions.RequestException: + self._logger.exception("While fetching data from server") + return + + if response.status_code != 200: + self._logger.error("API call returned with status %s", + response.status_code) + return + + content_type = response.headers.get('Content-Type', 'whatever') + if content_type != 'text/csv': + self._logger.error("Expected text/csv but got %s", content_type) + return + + response.encoding = 'UTF8' + content = response.text + data = (line for line in content.split('\n')) + reader = csv.DictReader(data, delimiter=';', quotechar='"') + for row in reader: + if row.get("Station", None) == self._station_id: + self.data = { + self.API_FIELDS.get(k)[0]: + self.API_FIELDS.get(k)[1](v.replace(',', '.')) + for k, v in row.items() + if v and k in self.API_FIELDS + } + break + + def get_data(self, variable): + """Generic accessor for data.""" + return self.data.get(variable) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 46a3a46ced6..54c0e18a3ee 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -144,3 +144,12 @@ openalpr: restart: description: Restart ffmpeg process of device. + +verisure: + capture_smartcam: + description: Capture a new image from a smartcam. + + fields: + device_serial: + description: The serial number of the smartcam you want to capture an image from. + example: '2DEU AT5Z' diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 8a98bc0c6df..92349be6376 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -17,8 +17,7 @@ from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv import homeassistant.util as util - -REQUIREMENTS = ['astral==1.3.2'] +REQUIREMENTS = ['astral==1.3.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py new file mode 100644 index 00000000000..ee71de3a22e --- /dev/null +++ b/homeassistant/components/switch/broadlink.py @@ -0,0 +1,158 @@ +""" +Support for Broadlink RM devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.broadlink/ +""" +from datetime import timedelta +from base64 import b64encode, b64decode +import asyncio +import binascii +import logging +import socket +import voluptuous as vol + +import homeassistant.loader as loader +from homeassistant.util.dt import utcnow +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_FRIENDLY_NAME, CONF_SWITCHES, + CONF_COMMAND_OFF, CONF_COMMAND_ON, + CONF_TIMEOUT, CONF_HOST, CONF_MAC) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['broadlink==0.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "broadlink" +DEFAULT_NAME = 'Broadlink switch' +DEFAULT_TIMEOUT = 10 +SERVICE_LEARN = "learn_command" + +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_COMMAND_OFF, default=None): cv.string, + vol.Optional(CONF_COMMAND_ON, default=None): cv.string, + vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Broadlink switches.""" + import broadlink + devices = config.get(CONF_SWITCHES, {}) + switches = [] + ip_addr = config.get(CONF_HOST) + mac_addr = binascii.unhexlify( + config.get(CONF_MAC).encode().replace(b':', b'')) + broadlink_device = broadlink.rm((ip_addr, 80), mac_addr) + broadlink_device.timeout = config.get(CONF_TIMEOUT) + try: + broadlink_device.auth() + except socket.timeout: + _LOGGER.error("Failed to connect to device.") + + persistent_notification = loader.get_component('persistent_notification') + + @asyncio.coroutine + def _learn_command(call): + try: + yield from hass.loop.run_in_executor(None, broadlink_device.auth) + except socket.timeout: + _LOGGER.error("Failed to connect to device.") + return + yield from hass.loop.run_in_executor(None, + broadlink_device.enter_learning) + + _LOGGER.info("Press the key you want HASS to learn") + start_time = utcnow() + while (utcnow() - start_time) < timedelta(seconds=20): + packet = yield from hass.loop.run_in_executor(None, + broadlink_device. + check_data) + if packet: + log_msg = 'Recieved packet is: {}'.\ + format(b64encode(packet).decode('utf8')) + _LOGGER.info(log_msg) + persistent_notification.async_create(hass, log_msg, + title='Broadlink switch') + return + yield from asyncio.sleep(1, loop=hass.loop) + _LOGGER.error('Did not received any signal.') + persistent_notification.async_create(hass, + "Did not received any signal", + title='Broadlink switch') + hass.services.register(DOMAIN, SERVICE_LEARN, _learn_command) + + for object_id, device_config in devices.items(): + switches.append( + BroadlinkRM2Switch( + device_config.get(CONF_FRIENDLY_NAME, object_id), + device_config.get(CONF_COMMAND_ON), + device_config.get(CONF_COMMAND_OFF), + broadlink_device + ) + ) + + add_devices(switches) + + +class BroadlinkRM2Switch(SwitchDevice): + """Representation of an Broadlink switch.""" + + def __init__(self, friendly_name, command_on, command_off, device): + """Initialize the switch.""" + self._name = friendly_name + self._state = False + self._command_on = b64decode(command_on) if command_on else None + self._command_off = b64decode(command_off) if command_off else None + self._device = device + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def assumed_state(self): + """Return true if unable to access real state of entity.""" + return True + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + if self._sendpacket(self._command_on): + self._state = True + + def turn_off(self, **kwargs): + """Turn the device off.""" + if self._sendpacket(self._command_off): + self._state = False + + def _sendpacket(self, packet, retry=2): + """Send packet to device.""" + if packet is None: + _LOGGER.debug("Empty packet.") + return True + try: + self._device.send_data(packet) + except socket.timeout as error: + if retry < 1: + _LOGGER.error(error) + return False + try: + self._device.auth() + except socket.timeout: + pass + return self._sendpacket(packet, max(0, retry-1)) + return True diff --git a/homeassistant/components/switch/digitalloggers.py b/homeassistant/components/switch/digitalloggers.py new file mode 100755 index 00000000000..a9d30e52ee6 --- /dev/null +++ b/homeassistant/components/switch/digitalloggers.py @@ -0,0 +1,148 @@ +""" +Support for Digital Loggers DIN III Relays. + +Support for Digital Loggers DIN III Relays and possibly other items +through Dwight Hubbard's, python-dlipower. + +For more details about python-dlipower, please see +https://github.com/dwighthubbard/python-dlipower + +Custom ports are NOT supported due to a limitation of the dlipower +library, not the digital loggers switch + +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + + +REQUIREMENTS = ['dlipower==0.7.165'] + +CONF_CYCLETIME = 'cycletime' + +DEFAULT_NAME = 'DINRelay' +DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = 'admin' +DEFAULT_TIMEOUT = 20 +DEFAULT_CYCLETIME = 2 + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=600)), + vol.Optional(CONF_CYCLETIME, default=DEFAULT_CYCLETIME): + vol.All(vol.Coerce(int), vol.Range(min=1, max=600)), + +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return DIN III Relay switch.""" + import dlipower + + host = config.get(CONF_HOST) + controllername = config.get(CONF_NAME) + user = config.get(CONF_USERNAME) + pswd = config.get(CONF_PASSWORD) + tout = config.get(CONF_TIMEOUT) + cycl = config.get(CONF_CYCLETIME) + + power_switch = dlipower.PowerSwitch( + hostname=host, userid=user, password=pswd, + timeout=tout, cycletime=cycl + ) + + if not power_switch.verify(): + _LOGGER.error('Could not connect to DIN III Relay') + return False + + devices = [] + parent_device = DINRelayDevice(power_switch) + + devices.extend( + DINRelay(controllername, device.outlet_number, parent_device) + for device in power_switch + ) + + add_devices(devices) + + +class DINRelay(SwitchDevice): + """Representation of a individual DIN III relay port.""" + + def __init__(self, name, outletnumber, parent_device): + """Initialize the DIN III Relay switch.""" + self._parent_device = parent_device + self.controllername = name + self.outletnumber = outletnumber + self.update() + + @property + def name(self): + """Return the display name of this relay.""" + return self._outletname + + @property + def is_on(self): + """Return true if relay is on.""" + return self._is_on + + @property + def should_poll(self): + """Polling is needed.""" + return True + + def turn_on(self, **kwargs): + """Instruct the relay to turn on.""" + self._parent_device.turn_on(outlet=self.outletnumber) + + def turn_off(self, **kwargs): + """Instruct the relay to turn off.""" + self._parent_device.turn_off(outlet=self.outletnumber) + + def update(self): + """Trigger update for all switches on the parent device.""" + self._parent_device.update() + self._is_on = ( + self._parent_device.statuslocal[self.outletnumber - 1][2] == 'ON' + ) + self._outletname = "{}_{}".format( + self.controllername, + self._parent_device.statuslocal[self.outletnumber - 1][1] + ) + + +class DINRelayDevice(object): + """Device representation for per device throttling.""" + + def __init__(self, device): + """Initialize the DINRelay device.""" + self._device = device + self.update() + + def turn_on(self, **kwargs): + """Instruct the relay to turn on.""" + self._device.on(**kwargs) + + def turn_off(self, **kwargs): + """Instruct the relay to turn off.""" + self._device.off(**kwargs) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Fetch new state data for this device.""" + self.statuslocal = self._device.statuslist() diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 6e16c9fa2e5..84cbbd9fb0e 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -11,7 +11,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.components.pilight as pilight from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE) +from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE, + CONF_PROTOCOL) _LOGGER = logging.getLogger(__name__) @@ -21,17 +22,20 @@ CONF_ON_CODE = 'on_code' CONF_ON_CODE_RECIEVE = 'on_code_receive' CONF_SYSTEMCODE = 'systemcode' CONF_UNIT = 'unit' +CONF_UNITCODE = 'unitcode' DEPENDENCIES = ['pilight'] -COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({ +COMMAND_SCHEMA = vol.Schema({ + vol.Optional(CONF_PROTOCOL): cv.string, vol.Optional('on'): cv.positive_int, vol.Optional('off'): cv.positive_int, vol.Optional(CONF_UNIT): cv.positive_int, + vol.Optional(CONF_UNITCODE): cv.positive_int, vol.Optional(CONF_ID): cv.positive_int, vol.Optional(CONF_STATE): cv.string, vol.Optional(CONF_SYSTEMCODE): cv.positive_int, -}) +}, extra=vol.ALLOW_EXTRA) SWITCHES_SCHEMA = vol.Schema({ vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 36674c16d16..cfa11897de9 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -4,23 +4,27 @@ Support for RESTful switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.rest/ """ +import asyncio import logging -import requests +import aiohttp +import async_timeout import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import Template CONF_BODY_OFF = 'body_off' CONF_BODY_ON = 'body_on' +CONF_IS_ON_TEMPLATE = 'is_on_template' + DEFAULT_BODY_OFF = Template('OFF') DEFAULT_BODY_ON = Template('ON') DEFAULT_NAME = 'REST Switch' DEFAULT_TIMEOUT = 10 -CONF_IS_ON_TEMPLATE = 'is_on_template' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, @@ -35,13 +39,15 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument, -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the RESTful switch.""" name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) body_on = config.get(CONF_BODY_ON) body_off = config.get(CONF_BODY_OFF) is_on_template = config.get(CONF_IS_ON_TEMPLATE) + websession = async_get_clientsession(hass) if is_on_template is not None: is_on_template.hass = hass @@ -51,19 +57,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): body_off.hass = hass timeout = config.get(CONF_TIMEOUT) + req = None try: - requests.get(resource, timeout=10) - except requests.exceptions.MissingSchema: + with async_timeout.timeout(timeout, loop=hass.loop): + req = yield from websession.get(resource) + except (TypeError, ValueError): _LOGGER.error("Missing resource or schema in configuration. " "Add http:// or https:// to your URL") return False - except requests.exceptions.ConnectionError: + except (asyncio.TimeoutError, aiohttp.errors.ClientError): _LOGGER.error("No route to resource/endpoint: %s", resource) return False + finally: + if req is not None: + yield from req.release() - add_devices( - [RestSwitch( - hass, name, resource, body_on, body_off, is_on_template, timeout)]) + yield from async_add_devices( + [RestSwitch(hass, name, resource, body_on, body_off, + is_on_template, timeout)]) class RestSwitch(SwitchDevice): @@ -73,7 +84,7 @@ class RestSwitch(SwitchDevice): is_on_template, timeout): """Initialize the REST switch.""" self._state = None - self._hass = hass + self.hass = hass self._name = name self._resource = resource self._body_on = body_on @@ -91,46 +102,85 @@ class RestSwitch(SwitchDevice): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): """Turn the device on.""" - body_on_t = self._body_on.render() - request = requests.post( - self._resource, data=body_on_t, timeout=self._timeout) - if request.status_code == 200: + body_on_t = self._body_on.async_render() + websession = async_get_clientsession(self.hass) + + request = None + try: + with async_timeout.timeout(self._timeout, loop=self.hass.loop): + request = yield from websession.post( + self._resource, data=bytes(body_on_t, 'utf-8')) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.error("Error while turn on %s", self._resource) + return + finally: + if request is not None: + yield from request.release() + + if request.status == 200: self._state = True else: _LOGGER.error("Can't turn on %s. Is resource/endpoint offline?", self._resource) - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Turn the device off.""" - body_off_t = self._body_off.render() - request = requests.post( - self._resource, data=body_off_t, timeout=self._timeout) - if request.status_code == 200: + body_off_t = self._body_off.async_render() + websession = async_get_clientsession(self.hass) + + request = None + try: + with async_timeout.timeout(self._timeout, loop=self.hass.loop): + request = yield from websession.post( + self._resource, data=bytes(body_off_t, 'utf-8')) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.error("Error while turn off %s", self._resource) + return + finally: + if request is not None: + yield from request.release() + + if request.status == 200: self._state = False else: _LOGGER.error("Can't turn off %s. Is resource/endpoint offline?", self._resource) - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data from REST API and update the state.""" - request = requests.get(self._resource, timeout=self._timeout) + websession = async_get_clientsession(self.hass) + + request = None + try: + with async_timeout.timeout(self._timeout, loop=self.hass.loop): + request = yield from websession.get(self._resource) + text = yield from request.text() + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error while fetch data.") + return + finally: + if request is not None: + yield from request.release() if self._is_on_template is not None: - response = self._is_on_template.render_with_possible_json_value( - request.text, 'None') - response = response.lower() - if response == 'true': + text = self._is_on_template.async_render_with_possible_json_value( + text, 'None') + text = text.lower() + if text == 'true': self._state = True - elif response == 'false': + elif text == 'false': self._state = False else: self._state = None else: - if request.text == self._body_on.template: + if text == self._body_on.template: self._state = True - elif request.text == self._body_off.template: + elif text == self._body_off.template: self._state = False else: self._state = None diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py index eaa78412c27..5f3901d79b8 100644 --- a/homeassistant/components/switch/tellduslive.py +++ b/homeassistant/components/switch/tellduslive.py @@ -9,7 +9,7 @@ https://home-assistant.io/components/switch.tellduslive/ """ import logging -from homeassistant.components import tellduslive +from homeassistant.components.tellduslive import TelldusLiveEntity from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -19,53 +19,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick switches.""" if discovery_info is None: return - add_devices(TelldusLiveSwitch(switch) for switch in discovery_info) + add_devices(TelldusLiveSwitch(hass, switch) for switch in discovery_info) -class TelldusLiveSwitch(ToggleEntity): +class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity): """Representation of a Tellstick switch.""" - def __init__(self, switch_id): - """Initialize the switch.""" - self._id = switch_id - self.update() - _LOGGER.debug("created switch %s", self) - - def update(self): - """Get the latest date and update the state.""" - tellduslive.NETWORK.update_switches() - self._switch = tellduslive.NETWORK.get_switch(self._id) - - @property - def should_poll(self): - """Polling is needed.""" - return True - - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return True - - @property - def name(self): - """Return the name of the switch if any.""" - return self._switch["name"] - - @property - def available(self): - """Return the state of the switch.""" - return not self._switch.get("offline", False) - @property def is_on(self): """Return true if switch is on.""" - from tellive.live import const - return self._switch["state"] == const.TELLSTICK_TURNON + return self.device.is_on def turn_on(self, **kwargs): """Turn the switch on.""" - tellduslive.NETWORK.turn_switch_on(self._id) + self.device.turn_on() + self.changed() def turn_off(self, **kwargs): """Turn the switch off.""" - tellduslive.NETWORK.turn_switch_off(self._id) + self.device.turn_off() + self.changed() diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index d3660ab36ca..46b1ad0aa49 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -26,7 +26,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS) - add_devices(TellstickSwitch(tellcore_id, signal_repetitions) + add_devices(TellstickSwitch(tellcore_id, hass.data['tellcore_registry'], + signal_repetitions) for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index bcc1b329fa8..44fecf37e56 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -1,5 +1,5 @@ """ -Support for TPLink HS100/HS110 smart switch. +Support for TPLink HS100/HS110/HS200 smart switch. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.tplink/ @@ -15,12 +15,10 @@ from homeassistant.const import (CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/GadgetReactor/pyHS100/archive/' - '1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0'] + '45fc3548882628bcde3e3d365db341849457bef2.zip#pyHS100==0.2.2'] _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'TPLink Switch HS100' - ATTR_CURRENT_CONSUMPTION = 'Current consumption' ATTR_TOTAL_CONSUMPTION = 'Total consumption' ATTR_DAILY_CONSUMPTION = 'Daily consumption' @@ -29,14 +27,14 @@ ATTR_CURRENT = 'Current' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, }) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the TPLink switch platform.""" - from pyHS100.pyHS100 import SmartPlug + from pyHS100 import SmartPlug host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -49,10 +47,15 @@ class SmartPlugSwitch(SwitchDevice): def __init__(self, smartplug, name): """Initialize the switch.""" self.smartplug = smartplug - self._name = name + + # Use the name set on the device if not set + if name is None: + self._name = self.smartplug.alias + else: + self._name = name + self._state = None - self._emeter_present = (smartplug.model == 110) - _LOGGER.debug("Setting up TP-Link Smart Plug HS%i", smartplug.model) + _LOGGER.debug("Setting up TP-Link Smart Plug") # Set up emeter cache self._emeter_params = {} @@ -64,15 +67,15 @@ class SmartPlugSwitch(SwitchDevice): @property def is_on(self): """Return true if switch is on.""" - return self._state == 'ON' + return self.smartplug.is_on def turn_on(self, **kwargs): """Turn the switch on.""" - self.smartplug.state = 'ON' + self.smartplug.turn_on() def turn_off(self): """Turn the switch off.""" - self.smartplug.state = 'OFF' + self.smartplug.turn_off() @property def device_state_attributes(self): @@ -84,7 +87,7 @@ class SmartPlugSwitch(SwitchDevice): try: self._state = self.smartplug.state - if self._emeter_present: + if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() self._emeter_params[ATTR_CURRENT_CONSUMPTION] \ diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 36e9b01d511..b470ae7daec 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -4,18 +4,20 @@ Support for Telldus Live. For more details about this component, please refer to the documentation at https://home-assistant.io/components/tellduslive/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta - -import voluptuous as vol +from homeassistant.const import ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME from homeassistant.helpers import discovery -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow +import voluptuous as vol DOMAIN = 'tellduslive' -REQUIREMENTS = ['tellive-py==0.5.2'] +REQUIREMENTS = ['tellduslive==0.1.13'] _LOGGER = logging.getLogger(__name__) @@ -23,11 +25,10 @@ CONF_PUBLIC_KEY = 'public_key' CONF_PRIVATE_KEY = 'private_key' CONF_TOKEN = 'token' CONF_TOKEN_SECRET = 'token_secret' +CONF_UPDATE_INTERVAL = 'update_interval' -MIN_TIME_BETWEEN_SWITCH_UPDATES = timedelta(minutes=1) -MIN_TIME_BETWEEN_SENSOR_UPDATES = timedelta(minutes=5) - -NETWORK = None +MIN_UPDATE_INTERVAL = timedelta(seconds=5) +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -35,183 +36,190 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PRIVATE_KEY): cv.string, vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_TOKEN_SECRET): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))) }), }, extra=vol.ALLOW_EXTRA) +ATTR_LAST_UPDATED = 'time_last_updated' + + def setup(hass, config): """Setup the Telldus Live component.""" - # fixme: aquire app key and provide authentication using username+password + client = TelldusLiveClient(hass, config) - global NETWORK - NETWORK = TelldusLiveData(hass, config) - - if not NETWORK.validate_session(): + if not client.validate_session(): _LOGGER.error( - "Authentication Error: " - "Please make sure you have configured your keys " - "that can be aquired from https://api.telldus.com/keys/index") + 'Authentication Error: ' + 'Please make sure you have configured your keys ' + 'that can be aquired from https://api.telldus.com/keys/index') return False - NETWORK.discover() + hass.data[DOMAIN] = client + client.update(utcnow()) return True -@Throttle(MIN_TIME_BETWEEN_SWITCH_UPDATES) -def request_switches(): - """Make request to online service.""" - _LOGGER.debug("Updating switches from Telldus Live") - switches = NETWORK.request('devices/list') - # Filter out any group of switches. - if switches and 'device' in switches: - return {switch["id"]: switch for switch in switches['device'] - if switch["type"] == "device"} - return None - - -@Throttle(MIN_TIME_BETWEEN_SENSOR_UPDATES) -def request_sensors(): - """Make request to online service.""" - _LOGGER.debug("Updating sensors from Telldus Live") - units = NETWORK.request('sensors/list') - # One unit can contain many sensors. - if units and 'sensor' in units: - return {(unit['id'], sensor['name'], sensor['scale']): - dict(unit, data=sensor) - for unit in units['sensor'] - for sensor in unit['data']} - return None - - -class TelldusLiveData(object): +class TelldusLiveClient(object): """Get the latest data and update the states.""" def __init__(self, hass, config): """Initialize the Tellus data object.""" + from tellduslive import Client + public_key = config[DOMAIN].get(CONF_PUBLIC_KEY) private_key = config[DOMAIN].get(CONF_PRIVATE_KEY) token = config[DOMAIN].get(CONF_TOKEN) token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET) - from tellive.client import LiveClient - - self._switches = {} - self._sensors = {} + self.entities = [] self._hass = hass self._config = config - self._client = LiveClient( - public_key=public_key, private_key=private_key, access_token=token, - access_secret=token_secret) + self._interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + _LOGGER.debug('Update interval %s', self._interval) + + self._client = Client(public_key, + private_key, + token, + token_secret) def validate_session(self): - """Make a dummy request to see if the session is valid.""" - response = self.request("user/profile") + """Make a request to see if the session is valid.""" + response = self._client.request_user() return response and 'email' in response - def discover(self): - """Update states, will trigger discover.""" - self.update_sensors() - self.update_switches() - - def _discover(self, found_devices, component_name): - """Send discovery event if component not yet discovered.""" - if not found_devices: - return - - _LOGGER.info("discovered %d new %s devices", - len(found_devices), component_name) - - discovery.load_platform(self._hass, component_name, DOMAIN, - found_devices, self._config) - - def request(self, what, **params): - """Send a request to the Tellstick Live API.""" - from tellive.live import const - - supported_methods = const.TELLSTICK_TURNON \ - | const.TELLSTICK_TURNOFF \ - | const.TELLSTICK_TOGGLE \ - - # Tellstick device methods not yet supported - # | const.TELLSTICK_BELL \ - # | const.TELLSTICK_DIM \ - # | const.TELLSTICK_LEARN \ - # | const.TELLSTICK_EXECUTE \ - # | const.TELLSTICK_UP \ - # | const.TELLSTICK_DOWN \ - # | const.TELLSTICK_STOP - - default_params = {'supportedMethods': supported_methods, - 'includeValues': 1, - 'includeScale': 1, - 'includeIgnored': 0} - params.update(default_params) - - # room for improvement: the telllive library doesn't seem to - # re-use sessions, instead it opens a new session for each request - # this needs to be fixed - + def update(self, now): + """Periodically poll the servers for current state.""" + _LOGGER.debug('Updating') try: - response = self._client.request(what, params) - _LOGGER.debug("got response %s", response) - return response - except OSError as error: - _LOGGER.error("failed to make request to Tellduslive servers: %s", - error) - return None + self._sync() + finally: + track_point_in_utc_time(self._hass, + self.update, + now + self._interval) - def update_devices(self, local_devices, remote_devices, component_name): - """Update local device list and discover new devices.""" - if remote_devices is None: - return local_devices + def _sync(self): + """Update local list of devices.""" + self._client.update() - remote_ids = remote_devices.keys() - local_ids = local_devices.keys() + def identify_device(device): + """Find out what type of HA component to create.""" + from tellduslive import (DIM, UP, TURNON) + if device.methods & DIM: + return 'light' + elif device.methods & UP: + return 'cover' + elif device.methods & TURNON: + return 'switch' + else: + _LOGGER.warning('Unidentified device type (methods: %d)', + device.methods) + return 'switch' - added_devices = list(remote_ids - local_ids) - self._discover(added_devices, - component_name) + def discover(device_id, component): + """Discover the component.""" + discovery.load_platform(self._hass, + component, + DOMAIN, + [device_id], + self._config) - removed_devices = list(local_ids - remote_ids) - remote_devices.update({id: dict(local_devices[id], offline=True) - for id in removed_devices}) + known_ids = set([entity.device_id for entity in self.entities]) + for device in self._client.devices: + if device.device_id in known_ids: + continue + if device.is_sensor: + for item_id in device.items: + discover((device.device_id,) + item_id, + 'sensor') + else: + discover(device.device_id, + identify_device(device)) - return remote_devices + for entity in self.entities: + entity.changed() - def update_sensors(self): - """Update local list of sensors.""" - self._sensors = self.update_devices( - self._sensors, request_sensors(), 'sensor') + def device(self, device_id): + """Return device representation.""" + import tellduslive + return tellduslive.Device(self._client, device_id) - def update_switches(self): - """Update local list of switches.""" - self._switches = self.update_devices( - self._switches, request_switches(), 'switch') + def is_available(self, device_id): + """Return device availability.""" + return device_id in self._client.device_ids - def _check_request(self, what, **params): - """Make request, check result if successful.""" - response = self.request(what, **params) - return response and response.get('status') == 'success' - def get_switch(self, switch_id): - """Return the switch representation.""" - return self._switches[switch_id] +class TelldusLiveEntity(Entity): + """Base class for all Telldus Live entities.""" - def get_sensor(self, sensor_id): - """Return the sensor representation.""" - return self._sensors[sensor_id] + def __init__(self, hass, device_id): + """Initialize the entity.""" + self._id = device_id + self._client = hass.data[DOMAIN] + self._client.entities.append(self) + _LOGGER.debug('Created device %s', self) - def turn_switch_on(self, switch_id): - """Turn switch off.""" - if self._check_request('device/turnOn', id=switch_id): - from tellive.live import const - self.get_switch(switch_id)['state'] = const.TELLSTICK_TURNON + def changed(self): + """A property of the device might have changed.""" + self.schedule_update_ha_state() - def turn_switch_off(self, switch_id): - """Turn switch on.""" - if self._check_request('device/turnOff', id=switch_id): - from tellive.live import const - self.get_switch(switch_id)['state'] = const.TELLSTICK_TURNOFF + @property + def device_id(self): + """Return the id of the device.""" + return self._id + + @property + def device(self): + """Return the representaion of the device.""" + return self._client.device(self.device_id) + + @property + def _state(self): + """Return the state of the device.""" + return self.device.state + + @property + def should_poll(self): + """Polling is not needed.""" + return False + + @property + def assumed_state(self): + """Return true if unable to access real state of entity.""" + return True + + @property + def name(self): + """Return name of device.""" + return self.device.name or DEVICE_DEFAULT_NAME + + @property + def available(self): + """Return true if device is not offline.""" + return self._client.is_available(self.device_id) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {} + if self._battery_level: + attrs[ATTR_BATTERY_LEVEL] = self._battery_level + if self._last_updated: + attrs[ATTR_LAST_UPDATED] = self._last_updated + return attrs + + @property + def _battery_level(self): + """Return the battery level of a device.""" + return round(self.device.battery * 100 / 255) \ + if self.device.battery else None + + @property + def _last_updated(self): + """Return the last update of a device.""" + return str(datetime.fromtimestamp(self.device.last_updated)) \ + if self.device.last_updated else None diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index cbd5ff20583..e957ef5e2a8 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -62,8 +62,6 @@ def setup(hass, config): from tellcore.library import DirectCallbackDispatcher from tellcore.telldus import TelldusCore - global TELLCORE_REGISTRY - try: tellcore_lib = TelldusCore( callback_dispatcher=DirectCallbackDispatcher()) @@ -75,8 +73,9 @@ def setup(hass, config): all_tellcore_devices = tellcore_lib.devices() # Register devices - TELLCORE_REGISTRY = TellstickRegistry(hass, tellcore_lib) - TELLCORE_REGISTRY.register_tellcore_devices(all_tellcore_devices) + tellcore_registry = TellstickRegistry(hass, tellcore_lib) + tellcore_registry.register_tellcore_devices(all_tellcore_devices) + hass.data['tellcore_registry'] = tellcore_registry # Discover the switches _discover(hass, config, 'switch', @@ -153,17 +152,17 @@ class TellstickDevice(Entity): Contains the common logic for all Tellstick devices. """ - def __init__(self, tellcore_id, signal_repetitions): + def __init__(self, tellcore_id, tellcore_registry, signal_repetitions): """Initalize the Tellstick device.""" self._signal_repetitions = signal_repetitions self._state = None # Look up our corresponding tellcore device - self._tellcore_device = TELLCORE_REGISTRY.get_tellcore_device( + self._tellcore_device = tellcore_registry.get_tellcore_device( tellcore_id) # Query tellcore for the current state self.update() # Add ourselves to the mapping - TELLCORE_REGISTRY.register_ha_device(tellcore_id, self) + tellcore_registry.register_ha_device(tellcore_id, self) @property def should_poll(self): diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py new file mode 100644 index 00000000000..d0faa60684f --- /dev/null +++ b/homeassistant/components/tts/__init__.py @@ -0,0 +1,419 @@ +""" +Provide functionality to TTS. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts/ +""" +import asyncio +import logging +import hashlib +import mimetypes +import os +import re + +from aiohttp import web +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.bootstrap import async_prepare_setup_platform +from homeassistant.core import callback +from homeassistant.config import load_yaml_config_file +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, MEDIA_TYPE_MUSIC, ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'tts' +DEPENDENCIES = ['http'] + +_LOGGER = logging.getLogger(__name__) + +MEM_CACHE_FILENAME = 'filename' +MEM_CACHE_VOICE = 'voice' + +CONF_LANG = 'language' +CONF_CACHE = 'cache' +CONF_CACHE_DIR = 'cache_dir' +CONF_TIME_MEMORY = 'time_memory' + +DEFAULT_CACHE = True +DEFAULT_CACHE_DIR = "tts" +DEFAULT_TIME_MEMORY = 300 + +SERVICE_SAY = 'say' +SERVICE_CLEAR_CACHE = 'clear_cache' + +ATTR_MESSAGE = 'message' +ATTR_CACHE = 'cache' + +_RE_VOICE_FILE = re.compile(r"([a-f0-9]{40})_([a-z]+)\.[a-z0-9]{3,4}") + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_CACHE, default=DEFAULT_CACHE): cv.boolean, + vol.Optional(CONF_CACHE_DIR, default=DEFAULT_CACHE_DIR): cv.string, + vol.Optional(CONF_TIME_MEMORY, default=DEFAULT_TIME_MEMORY): + vol.All(vol.Coerce(int), vol.Range(min=60, max=57600)), +}) + + +SCHEMA_SERVICE_SAY = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_CACHE): cv.boolean, +}) + +SCHEMA_SERVICE_CLEAR_CACHE = vol.Schema({}) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup TTS.""" + tts = SpeechManager(hass) + + try: + conf = config[DOMAIN][0] if len(config.get(DOMAIN, [])) > 0 else {} + use_cache = conf.get(CONF_CACHE, DEFAULT_CACHE) + cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR) + time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY) + + yield from tts.async_init_cache(use_cache, cache_dir, time_memory) + except (HomeAssistantError, KeyError) as err: + _LOGGER.error("Error on cache init %s", err) + return False + + hass.http.register_view(TextToSpeechView(tts)) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + @asyncio.coroutine + def async_setup_platform(p_type, p_config, disc_info=None): + """Setup a tts platform.""" + platform = yield from async_prepare_setup_platform( + hass, config, DOMAIN, p_type) + if platform is None: + return + + try: + if hasattr(platform, 'async_get_engine'): + provider = yield from platform.async_get_engine( + hass, p_config) + else: + provider = yield from hass.loop.run_in_executor( + None, platform.get_engine, hass, p_config) + + if provider is None: + _LOGGER.error('Error setting up platform %s', p_type) + return + + tts.async_register_engine(p_type, provider, p_config) + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error setting up platform %s', p_type) + return + + @asyncio.coroutine + def async_say_handle(service): + """Service handle for say.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + message = service.data.get(ATTR_MESSAGE) + cache = service.data.get(ATTR_CACHE) + + try: + url = yield from tts.async_get_url( + p_type, message, cache=cache) + except HomeAssistantError as err: + _LOGGER.error("Error on init tts: %s", err) + return + + data = { + ATTR_MEDIA_CONTENT_ID: url, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + } + + if entity_ids: + data[ATTR_ENTITY_ID] = entity_ids + + yield from hass.services.async_call( + DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True) + + hass.services.async_register( + DOMAIN, "{}_{}".format(p_type, SERVICE_SAY), async_say_handle, + descriptions.get(SERVICE_SAY), schema=SCHEMA_SERVICE_SAY) + + setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config + in config_per_platform(config, DOMAIN)] + + if setup_tasks: + yield from asyncio.wait(setup_tasks, loop=hass.loop) + + @asyncio.coroutine + def async_clear_cache_handle(service): + """Handle clear cache service call.""" + yield from tts.async_clear_cache() + + hass.services.async_register( + DOMAIN, SERVICE_CLEAR_CACHE, async_clear_cache_handle, + descriptions.get(SERVICE_CLEAR_CACHE), schema=SERVICE_CLEAR_CACHE) + + return True + + +class SpeechManager(object): + """Representation of a speech store.""" + + def __init__(self, hass): + """Initialize a speech store.""" + self.hass = hass + self.providers = {} + + self.use_cache = True + self.cache_dir = None + self.time_memory = None + self.file_cache = {} + self.mem_cache = {} + + @asyncio.coroutine + def async_init_cache(self, use_cache, cache_dir, time_memory): + """Init config folder and load file cache.""" + self.use_cache = use_cache + self.time_memory = time_memory + + def init_tts_cache_dir(cache_dir): + """Init cache folder.""" + if not os.path.isabs(cache_dir): + cache_dir = self.hass.config.path(cache_dir) + if not os.path.isdir(cache_dir): + _LOGGER.info("Create cache dir %s.", cache_dir) + os.mkdir(cache_dir) + return cache_dir + + try: + self.cache_dir = yield from self.hass.loop.run_in_executor( + None, init_tts_cache_dir, cache_dir) + except OSError as err: + raise HomeAssistantError( + "Can't init cache dir {}".format(err)) + + def get_cache_files(): + """Return a dict of given engine files.""" + cache = {} + + folder_data = os.listdir(self.cache_dir) + for file_data in folder_data: + record = _RE_VOICE_FILE.match(file_data) + if record: + key = "{}_{}".format(record.group(1), record.group(2)) + cache[key.lower()] = file_data.lower() + return cache + + try: + cache_files = yield from self.hass.loop.run_in_executor( + None, get_cache_files) + except OSError as err: + raise HomeAssistantError( + "Can't read cache dir {}".format(err)) + + if cache_files: + self.file_cache.update(cache_files) + + @asyncio.coroutine + def async_clear_cache(self): + """Read file cache and delete files.""" + self.mem_cache = {} + + def remove_files(): + """Remove files from filesystem.""" + for _, filename in self.file_cache.items(): + try: + os.remove(os.path.join(self.cache_dir), filename) + except OSError: + pass + + yield from self.hass.loop.run_in_executor(None, remove_files) + self.file_cache = {} + + @callback + def async_register_engine(self, engine, provider, config): + """Register a TTS provider.""" + provider.hass = self.hass + provider.language = config.get(CONF_LANG) + self.providers[engine] = provider + + @asyncio.coroutine + def async_get_url(self, engine, message, cache=None): + """Get URL for play message. + + This method is a coroutine. + """ + msg_hash = hashlib.sha1(bytes(message, 'utf-8')).hexdigest() + key = ("{}_{}".format(msg_hash, engine)).lower() + use_cache = cache if cache is not None else self.use_cache + + # is speech allready in memory + if key in self.mem_cache: + filename = self.mem_cache[key][MEM_CACHE_FILENAME] + # is file store in file cache + elif use_cache and key in self.file_cache: + filename = self.file_cache[key] + self.hass.async_add_job(self.async_file_to_mem(engine, key)) + # load speech from provider into memory + else: + filename = yield from self.async_get_tts_audio( + engine, key, message, use_cache) + + return "{}/api/tts_proxy/{}".format( + self.hass.config.api.base_url, filename) + + @asyncio.coroutine + def async_get_tts_audio(self, engine, key, message, cache): + """Receive TTS and store for view in cache. + + This method is a coroutine. + """ + provider = self.providers[engine] + extension, data = yield from provider.async_get_tts_audio(message) + + if data is None or extension is None: + raise HomeAssistantError( + "No TTS from {} for '{}'".format(engine, message)) + + # create file infos + filename = ("{}.{}".format(key, extension)).lower() + + # save to memory + self._async_store_to_memcache(key, filename, data) + + if cache: + self.hass.async_add_job( + self.async_save_tts_audio(key, filename, data)) + + return filename + + @asyncio.coroutine + def async_save_tts_audio(self, key, filename, data): + """Store voice data to file and file_cache. + + This method is a coroutine. + """ + voice_file = os.path.join(self.cache_dir, filename) + + def save_speech(): + """Store speech to filesystem.""" + with open(voice_file, 'wb') as speech: + speech.write(data) + + try: + yield from self.hass.loop.run_in_executor(None, save_speech) + self.file_cache[key] = filename + except OSError: + _LOGGER.error("Can't write %s", filename) + + @asyncio.coroutine + def async_file_to_mem(self, engine, key): + """Load voice from file cache into memory. + + This method is a coroutine. + """ + filename = self.file_cache.get(key) + if not filename: + raise HomeAssistantError("Key {} not in file cache!".format(key)) + + voice_file = os.path.join(self.cache_dir, filename) + + def load_speech(): + """Load a speech from filesystem.""" + with open(voice_file, 'rb') as speech: + return speech.read() + + try: + data = yield from self.hass.loop.run_in_executor(None, load_speech) + except OSError: + raise HomeAssistantError("Can't read {}".format(voice_file)) + + self._async_store_to_memcache(key, filename, data) + + @callback + def _async_store_to_memcache(self, key, filename, data): + """Store data to memcache and set timer to remove it.""" + self.mem_cache[key] = { + MEM_CACHE_FILENAME: filename, + MEM_CACHE_VOICE: data, + } + + @callback + def async_remove_from_mem(): + """Cleanup memcache.""" + self.mem_cache.pop(key) + + self.hass.loop.call_later(self.time_memory, async_remove_from_mem) + + @asyncio.coroutine + def async_read_tts(self, filename): + """Read a voice file and return binary. + + This method is a coroutine. + """ + record = _RE_VOICE_FILE.match(filename.lower()) + if not record: + raise HomeAssistantError("Wrong tts file format!") + + key = "{}_{}".format(record.group(1), record.group(2)) + + if key not in self.mem_cache: + if key not in self.file_cache: + raise HomeAssistantError("%s not in cache!", key) + engine = record.group(2) + yield from self.async_file_to_mem(engine, key) + + content, _ = mimetypes.guess_type(filename) + return (content, self.mem_cache[key][MEM_CACHE_VOICE]) + + +class Provider(object): + """Represent a single provider.""" + + hass = None + language = None + + def get_tts_audio(self, message): + """Load tts audio file from provider.""" + raise NotImplementedError() + + @asyncio.coroutine + def async_get_tts_audio(self, message): + """Load tts audio file from provider. + + Return a tuple of file extension and data as bytes. + + This method is a coroutine. + """ + extension, data = yield from self.hass.loop.run_in_executor( + None, self.get_tts_audio, message) + return (extension, data) + + +class TextToSpeechView(HomeAssistantView): + """TTS view to serve an speech audio.""" + + requires_auth = False + url = "/api/tts_proxy/{filename}" + name = "api:tts:speech" + + def __init__(self, tts): + """Initialize a tts view.""" + self.tts = tts + + @asyncio.coroutine + def get(self, request, filename): + """Start a get request.""" + try: + content, data = yield from self.tts.async_read_tts(filename) + except HomeAssistantError as err: + _LOGGER.error("Error on load tts: %s", err) + return web.Response(status=404) + + return web.Response(body=data, content_type=content) diff --git a/homeassistant/components/tts/demo.mp3 b/homeassistant/components/tts/demo.mp3 new file mode 100644 index 00000000000..f34241c7698 Binary files /dev/null and b/homeassistant/components/tts/demo.mp3 differ diff --git a/homeassistant/components/tts/demo.py b/homeassistant/components/tts/demo.py new file mode 100644 index 00000000000..a63bd6373ea --- /dev/null +++ b/homeassistant/components/tts/demo.py @@ -0,0 +1,29 @@ +""" +Support for the demo speech service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/demo/ +""" +import os + +from homeassistant.components.tts import Provider + + +def get_engine(hass, config): + """Setup Demo speech component.""" + return DemoProvider() + + +class DemoProvider(Provider): + """Demo speech api provider.""" + + def get_tts_audio(self, message): + """Load TTS from demo.""" + filename = os.path.join(os.path.dirname(__file__), "demo.mp3") + try: + with open(filename, 'rb') as voice: + data = voice.read() + except OSError: + return + + return ("mp3", data) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py new file mode 100644 index 00000000000..49d53961062 --- /dev/null +++ b/homeassistant/components/tts/google.py @@ -0,0 +1,133 @@ +""" +Support for the google speech service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts/google/ +""" +import asyncio +import logging +import re + +import aiohttp +import async_timeout +import voluptuous as vol +import yarl + +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +REQUIREMENTS = ["gTTS-token==1.1.1"] + +_LOGGER = logging.getLogger(__name__) + +GOOGLE_SPEECH_URL = "http://translate.google.com/translate_tts" +MESSAGE_SIZE = 148 + +SUPPORT_LANGUAGES = [ + 'af', 'sq', 'ar', 'hy', 'bn', 'ca', 'zh', 'zh-cn', 'zh-tw', 'zh-yue', + 'hr', 'cs', 'da', 'nl', 'en', 'en-au', 'en-uk', 'en-us', 'eo', 'fi', + 'fr', 'de', 'el', 'hi', 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', + 'mk', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru', 'sr', 'sk', 'es', 'es-es', + 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', +] + +DEFAULT_LANG = 'en' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), +}) + + +@asyncio.coroutine +def async_get_engine(hass, config): + """Setup Google speech component.""" + return GoogleProvider(hass) + + +class GoogleProvider(Provider): + """Google speech api provider.""" + + def __init__(self, hass): + """Init Google TTS service.""" + self.hass = hass + self.headers = { + 'Referer': "http://translate.google.com/", + 'User-Agent': ("Mozilla/5.0 (Windows NT 10.0; WOW64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/47.0.2526.106 Safari/537.36") + } + + @asyncio.coroutine + def async_get_tts_audio(self, message): + """Load TTS from google.""" + from gtts_token import gtts_token + + token = gtts_token.Token() + websession = async_get_clientsession(self.hass) + message_parts = self._split_message_to_parts(message) + + data = b'' + for idx, part in enumerate(message_parts): + part_token = yield from self.hass.loop.run_in_executor( + None, token.calculate_token, part) + + url_param = { + 'ie': 'UTF-8', + 'tl': self.language, + 'q': yarl.quote(part), + 'tk': part_token, + 'total': len(message_parts), + 'idx': idx, + 'client': 'tw-ob', + 'textlen': len(part), + } + + request = None + try: + with async_timeout.timeout(10, loop=self.hass.loop): + request = yield from websession.get( + GOOGLE_SPEECH_URL, params=url_param, + headers=self.headers + ) + + if request.status != 200: + _LOGGER.error("Error %d on load url %s", + request.status, request.url) + return (None, None) + data += yield from request.read() + + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.error("Timeout for google speech.") + return (None, None) + + finally: + if request is not None: + yield from request.release() + + return ("mp3", data) + + @staticmethod + def _split_message_to_parts(message): + """Split message into single parts.""" + if len(message) <= MESSAGE_SIZE: + return [message] + + punc = "!()[]?.,;:" + punc_list = [re.escape(c) for c in punc] + pattern = '|'.join(punc_list) + parts = re.split(pattern, message) + + def split_by_space(fullstring): + """Split a string by space.""" + if len(fullstring) > MESSAGE_SIZE: + idx = fullstring.rfind(' ', 0, MESSAGE_SIZE) + return [fullstring[:idx]] + split_by_space(fullstring[idx:]) + else: + return [fullstring] + + msg_parts = [] + for part in parts: + msg_parts += split_by_space(part) + + return [msg for msg in msg_parts if len(msg) > 0] diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml new file mode 100644 index 00000000000..aba1334da87 --- /dev/null +++ b/homeassistant/components/tts/services.yaml @@ -0,0 +1,14 @@ +say: + description: Say some things on a media player. + + fields: + entity_id: + description: Name(s) of media player entities + example: 'media_player.floor' + + message: + description: Text to speak on devices + example: 'My name is hanna' + +clear_cache: + description: Remove cache files and RAM cache. diff --git a/homeassistant/components/tts/voicerss.py b/homeassistant/components/tts/voicerss.py new file mode 100644 index 00000000000..728a1996a5d --- /dev/null +++ b/homeassistant/components/tts/voicerss.py @@ -0,0 +1,120 @@ +""" +Support for the voicerss speech service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts/voicerss/ +""" +import asyncio +import logging + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +VOICERSS_API_URL = "https://api.voicerss.org/" + +SUPPORT_LANGUAGES = [ + 'ca-es', 'zh-cn', 'zh-hk', 'zh-tw', 'da-dk', 'nl-nl', 'en-au', 'en-ca', + 'en-gb', 'en-in', 'en-us', 'fi-fi', 'fr-ca', 'fr-fr', 'de-de', 'it-it', + 'ja-jp', 'ko-kr', 'nb-no', 'pl-pl', 'pt-br', 'pt-pt', 'ru-ru', 'es-mx', + 'es-es', 'sv-se', +] + +SUPPORT_CODECS = [ + 'mp3', 'wav', 'aac', 'ogg', 'caf' +] + +SUPPORT_FORMATS = [ + '8khz_8bit_mono', '8khz_8bit_stereo', '8khz_16bit_mono', + '8khz_16bit_stereo', '11khz_8bit_mono', '11khz_8bit_stereo', + '11khz_16bit_mono', '11khz_16bit_stereo', '12khz_8bit_mono', + '12khz_8bit_stereo', '12khz_16bit_mono', '12khz_16bit_stereo', + '16khz_8bit_mono', '16khz_8bit_stereo', '16khz_16bit_mono', + '16khz_16bit_stereo', '22khz_8bit_mono', '22khz_8bit_stereo', + '22khz_16bit_mono', '22khz_16bit_stereo', '24khz_8bit_mono', + '24khz_8bit_stereo', '24khz_16bit_mono', '24khz_16bit_stereo', + '32khz_8bit_mono', '32khz_8bit_stereo', '32khz_16bit_mono', + '32khz_16bit_stereo', '44khz_8bit_mono', '44khz_8bit_stereo', + '44khz_16bit_mono', '44khz_16bit_stereo', '48khz_8bit_mono', + '48khz_8bit_stereo', '48khz_16bit_mono', '48khz_16bit_stereo', + 'alaw_8khz_mono', 'alaw_8khz_stereo', 'alaw_11khz_mono', + 'alaw_11khz_stereo', 'alaw_22khz_mono', 'alaw_22khz_stereo', + 'alaw_44khz_mono', 'alaw_44khz_stereo', 'ulaw_8khz_mono', + 'ulaw_8khz_stereo', 'ulaw_11khz_mono', 'ulaw_11khz_stereo', + 'ulaw_22khz_mono', 'ulaw_22khz_stereo', 'ulaw_44khz_mono', + 'ulaw_44khz_stereo', +] + +CONF_CODEC = 'codec' +CONF_FORMAT = 'format' + +DEFAULT_LANG = 'en-us' +DEFAULT_CODEC = 'mp3' +DEFAULT_FORMAT = '8khz_8bit_mono' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODECS), + vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(SUPPORT_FORMATS), +}) + + +@asyncio.coroutine +def async_get_engine(hass, config): + """Setup VoiceRSS speech component.""" + return VoiceRSSProvider(hass, config) + + +class VoiceRSSProvider(Provider): + """VoiceRSS speech api provider.""" + + def __init__(self, hass, conf): + """Init VoiceRSS TTS service.""" + self.hass = hass + self.extension = conf.get(CONF_CODEC) + + self.params = { + 'key': conf.get(CONF_API_KEY), + 'hl': conf.get(CONF_LANG), + 'c': (conf.get(CONF_CODEC)).upper(), + 'f': conf.get(CONF_FORMAT), + } + + @asyncio.coroutine + def async_get_tts_audio(self, message): + """Load TTS from voicerss.""" + websession = async_get_clientsession(self.hass) + + request = None + try: + with async_timeout.timeout(10, loop=self.hass.loop): + request = yield from websession.post( + VOICERSS_API_URL, params=self.params, + data=bytes(message, 'utf-8') + ) + + if request.status != 200: + _LOGGER.error("Error %d on load url %s", + request.status, request.url) + return (None, None) + data = yield from request.read() + + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.error("Timeout for voicerss api.") + return (None, None) + + finally: + if request is not None: + yield from request.release() + + return (self.extension, data) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index c8241d8fae5..f2b091aa0f1 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/verisure/ import logging import threading import time +import os.path from datetime import timedelta import voluptuous as vol @@ -14,12 +15,14 @@ import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery from homeassistant.util import Throttle +import homeassistant.config as conf_util import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['vsure==0.11.1'] _LOGGER = logging.getLogger(__name__) +ATTR_DEVICE_SERIAL = 'device_serial' CONF_ALARM = 'alarm' CONF_CODE_DIGITS = 'code_digits' CONF_HYDROMETERS = 'hygrometers' @@ -29,6 +32,7 @@ CONF_SMARTPLUGS = 'smartplugs' CONF_THERMOMETERS = 'thermometers' CONF_SMARTCAM = 'smartcam' DOMAIN = 'verisure' +SERVICE_CAPTURE_SMARTCAM = 'capture_smartcam' HUB = None @@ -47,6 +51,10 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +CAPTURE_IMAGE_SCHEMA = vol.Schema({ + vol.Required(ATTR_DEVICE_SERIAL): cv.string +}) + def setup(hass, config): """Setup the Verisure component.""" @@ -60,6 +68,20 @@ def setup(hass, config): 'camera'): discovery.load_platform(hass, component, DOMAIN, {}, config) + descriptions = conf_util.load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + def capture_smartcam(service): + """Capture a new picture from a smartcam.""" + device_id = service.data.get(ATTR_DEVICE_SERIAL) + HUB.smartcam_capture(device_id) + _LOGGER.debug('Capturing new image from %s', ATTR_DEVICE_SERIAL) + + hass.services.register(DOMAIN, SERVICE_CAPTURE_SMARTCAM, + capture_smartcam, + descriptions[DOMAIN][SERVICE_CAPTURE_SMARTCAM], + schema=CAPTURE_IMAGE_SCHEMA) + return True @@ -150,6 +172,11 @@ class VerisureHub(object): self.smartcam_dict = self.my_pages.smartcam.get_imagelist() _LOGGER.debug('New dict: %s', self.smartcam_dict) + @Throttle(timedelta(seconds=30)) + def smartcam_capture(self, device_id): + """Capture a new image from a smartcam.""" + self.my_pages.smartcam.capture(device_id) + @property def available(self): """Return True if hub is available.""" diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 26a5a41bf10..67dc7924aa3 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -4,6 +4,7 @@ Weather component that handles meteorological data for your location. For more details about this component, please refer to the documentation at https://home-assistant.io/components/weather/ """ +import asyncio import logging from numbers import Number @@ -30,11 +31,12 @@ ATTR_WEATHER_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_WIND_SPEED = 'wind_speed' -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup the weather component.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - component.setup(config) + yield from component.async_setup(config) return True diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index bb374afaf86..affeb376f5c 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-wink==0.10.1', 'pubnubsub-handler==0.0.5'] +REQUIREMENTS = ['python-wink==0.11.0', 'pubnubsub-handler==0.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/const.py b/homeassistant/const.py index bfca70a8092..0a93f080df7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 34 -PATCH_VERSION = '5' +MINOR_VERSION = 35 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) @@ -278,6 +278,8 @@ ATTR_GPS_ACCURACY = 'gps_accuracy' ATTR_ASSUMED_STATE = 'assumed_state' ATTR_STATE = 'state' +ATTR_OPTION = 'option' + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = 'stop' SERVICE_HOMEASSISTANT_RESTART = 'restart' @@ -318,6 +320,8 @@ SERVICE_SET_COVER_TILT_POSITION = 'set_cover_tilt_position' SERVICE_STOP_COVER = 'stop_cover' SERVICE_STOP_COVER_TILT = 'stop_cover_tilt' +SERVICE_SELECT_OPTION = 'select_option' + # #### API / REMOTE #### SERVER_PORT = 8123 diff --git a/homeassistant/core.py b/homeassistant/core.py index 86cfec7099c..7daab159f21 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -57,8 +57,8 @@ ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") # Size of a executor pool EXECUTOR_POOL_SIZE = 10 -# Time for cleanup internal pending tasks -TIME_INTERVAL_TASKS_CLEANUP = 10 +# AsyncHandler for logging +DATA_ASYNCHANDLER = 'log_asynchandler' _LOGGER = logging.getLogger(__name__) @@ -184,6 +184,8 @@ class HomeAssistant(object): target: target to call. args: parameters for method to call. """ + if target is None: + raise ValueError("Don't call add_job with None.") self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback @@ -292,6 +294,14 @@ class HomeAssistant(object): yield from self.async_block_till_done() self.executor.shutdown() self.state = CoreState.not_running + + # cleanup async layer from python logging + if self.data.get(DATA_ASYNCHANDLER): + handler = self.data.pop(DATA_ASYNCHANDLER) + logger = logging.getLogger('') + handler.close() + logger.removeHandler(handler) + self.loop.stop() # pylint: disable=no-self-use diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 87b05ded264..4137a31b8b6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -79,6 +79,9 @@ class Entity(object): # Owning hass instance. Will be set by EntityComponent hass = None # type: Optional[HomeAssistant] + # If we reported if this entity was slow + _slow_reported = False + @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -243,7 +246,8 @@ class Entity(object): end = timer() - if end - start > 0.4: + if not self._slow_reported and end - start > 0.4: + self._slow_reported = True _LOGGER.warning('Updating state for %s took %.3f seconds. ' 'Please report platform to the developers at ' 'https://goo.gl/Nvioub', self.entity_id, diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 9980ad11a8d..536975c100e 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -32,7 +32,7 @@ from homeassistant.const import ( SERVICE_CLOSE_COVER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN, STATE_UNLOCKED) + STATE_UNKNOWN, STATE_UNLOCKED, SERVICE_SELECT_OPTION, ATTR_OPTION) from homeassistant.core import State from homeassistant.util.async import run_coroutine_threadsafe @@ -58,7 +58,8 @@ SERVICE_ATTRIBUTES = { SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE], SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], - SERVICE_SEND_IR_CODE: [ATTR_IR_CODE] + SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], + SERVICE_SELECT_OPTION: [ATTR_OPTION] } # Update this dict when new services are added to HA. @@ -163,11 +164,28 @@ def async_reproduce_state(hass, states, blocking=False): json.dumps(dict(state.attributes), sort_keys=True)) to_call[key].append(state.entity_id) + domain_tasks = {} for (service_domain, service, service_data), entity_ids in to_call.items(): data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids - yield from hass.services.async_call( - service_domain, service, data, blocking) + + if service_domain not in domain_tasks: + domain_tasks[service_domain] = [] + + domain_tasks[service_domain].append( + hass.services.async_call(service_domain, service, data, blocking) + ) + + @asyncio.coroutine + def async_handle_service_calls(coro_list): + """Handle service calls by domain sequence.""" + for coro in coro_list: + yield from coro + + execute_tasks = [async_handle_service_calls(coro_list) + for coro_list in domain_tasks.values()] + if execute_tasks: + yield from asyncio.wait(execute_tasks, loop=hass.loop) def state_as_number(state): diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index ace1b4efe83..986630e4db0 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -231,7 +231,8 @@ def check(config_path): yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) try: - bootstrap.from_config_file(config_path, skip_pip=True) + with patch('homeassistant.util.logging.AsyncHandler._process'): + bootstrap.from_config_file(config_path, skip_pip=True) res['secret_cache'] = dict(yaml.__SECRET_CACHE) except Exception as err: # pylint: disable=broad-except print(color('red', 'Fatal error while loading config:'), str(err)) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index d324e7253b7..1ddec0bc6a1 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -1,5 +1,9 @@ """Logging utilities.""" +import asyncio import logging +import threading + +from .async import run_coroutine_threadsafe class HideSensitiveDataFilter(logging.Filter): @@ -15,3 +19,91 @@ class HideSensitiveDataFilter(logging.Filter): record.msg = record.msg.replace(self.text, '*******') return True + + +# pylint: disable=invalid-name +class AsyncHandler(object): + """Logging handler wrapper to add a async layer.""" + + def __init__(self, loop, handler): + """Initialize async logging handler wrapper.""" + self.handler = handler + self.loop = loop + self._queue = asyncio.Queue(loop=loop) + self._thread = threading.Thread(target=self._process) + + # Delegate from handler + self.setLevel = handler.setLevel + self.setFormatter = handler.setFormatter + self.addFilter = handler.addFilter + self.removeFilter = handler.removeFilter + self.filter = handler.filter + self.flush = handler.flush + self.handle = handler.handle + self.handleError = handler.handleError + self.format = handler.format + + self._thread.start() + + def close(self): + """Wrap close to handler.""" + self.emit(None) + + def emit(self, record): + """Process a record.""" + ident = self.loop.__dict__.get("_thread_ident") + + # inside eventloop + if ident is not None and ident == threading.get_ident(): + self._queue.put_nowait(record) + # from a thread/executor + else: + self.loop.call_soon_threadsafe(self._queue.put_nowait, record) + + def __repr__(self): + """String name of this.""" + return str(self.handler) + + def _process(self): + """Process log in a thread.""" + while True: + record = run_coroutine_threadsafe( + self._queue.get(), self.loop).result() + + if record is None: + self.handler.close() + return + + self.handler.emit(record) + + def createLock(self): + """Ignore lock stuff.""" + pass + + def acquire(self): + """Ignore lock stuff.""" + pass + + def release(self): + """Ignore lock stuff.""" + pass + + @property + def level(self): + """Wrap property level to handler.""" + return self.handler.level + + @property + def formatter(self): + """Wrap property formatter to handler.""" + return self.handler.formatter + + @property + def name(self): + """Wrap property set_name to handler.""" + return self.handler.get_name() + + @name.setter + def set_name(self, name): + """Wrap property get_name to handler.""" + self.handler.name = name diff --git a/requirements_all.txt b/requirements_all.txt index 93ba123ea10..f39a560f2f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -43,7 +43,7 @@ apcaccess==0.0.4 apns2==0.1.1 # homeassistant.components.sun -astral==1.3.2 +astral==1.3.3 # homeassistant.components.sensor.linux_battery batinfo==0.4.2 @@ -65,6 +65,10 @@ blockchain==1.3.3 # homeassistant.components.notify.aws_sqs boto3==1.3.1 +# homeassistant.components.sensor.broadlink +# homeassistant.components.switch.broadlink +broadlink==0.2 + # homeassistant.components.sensor.coinmarketcap coinmarketcap==2.0.1 @@ -76,7 +80,7 @@ colorlog>2.1,<3 concord232==0.14 # homeassistant.components.media_player.denonavr -denonavr==0.1.6 +denonavr==0.2.2 # homeassistant.components.media_player.directv directpy==0.1 @@ -84,6 +88,9 @@ directpy==0.1 # homeassistant.components.updater distro==1.0.1 +# homeassistant.components.switch.digitalloggers +dlipower==0.7.165 + # homeassistant.components.notify.xmpp dnspython3==1.15.0 @@ -98,7 +105,7 @@ dsmr_parser==0.4 dweepy==0.2.0 # homeassistant.components.sensor.eliqonline -eliqonline==1.0.12 +eliqonline==1.0.13 # homeassistant.components.enocean enocean==0.31 @@ -127,6 +134,9 @@ freesms==0.1.1 # homeassistant.components.conversation fuzzywuzzy==0.14.0 +# homeassistant.components.tts.google +gTTS-token==1.1.1 + # homeassistant.components.device_tracker.bluetooth_le_tracker # gattlib==0.20150805 @@ -167,10 +177,10 @@ hikvision==0.4 http://github.com/technicalpickles/python-nest/archive/b8391d2b3cb8682f8b0c2bdff477179983609f39.zip#python-nest==3.0.2 # homeassistant.components.light.flux_led -https://github.com/Danielhiversen/flux_led/archive/0.9.zip#flux_led==0.9 +https://github.com/Danielhiversen/flux_led/archive/0.10.zip#flux_led==0.10 # homeassistant.components.switch.tplink -https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0 +https://github.com/GadgetReactor/pyHS100/archive/45fc3548882628bcde3e3d365db341849457bef2.zip#pyHS100==0.2.2 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7 @@ -202,7 +212,7 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9. # https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6 # homeassistant.components.netatmo -https://github.com/jabesq/netatmo-api-python/archive/v0.7.0.zip#lnetatmo==0.7.0 +https://github.com/jabesq/netatmo-api-python/archive/v0.8.0.zip#lnetatmo==0.8.0 # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 @@ -238,6 +248,9 @@ https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e22462 # homeassistant.components.scene.hunterdouglas_powerview https://github.com/sander76/powerviewApi/archive/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15 +# homeassistant.components.binary_sensor.flic +https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4 + # homeassistant.components.light.osramlightify https://github.com/tfriedel/python-lightify/archive/d6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4 @@ -293,7 +306,7 @@ mficlient==0.3.0 miflora==0.1.13 # homeassistant.components.discovery -netdisco==0.7.7 +netdisco==0.8.1 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 @@ -381,10 +394,11 @@ pycmus==0.1.0 # homeassistant.components.envisalink # homeassistant.components.zwave +# homeassistant.components.binary_sensor.hikvision pydispatcher==2.0.5 # homeassistant.components.media_player.emby -pyemby==0.1 +pyemby==0.2 # homeassistant.components.envisalink pyenvisalink==1.9 @@ -395,6 +409,9 @@ pyfttt==0.3 # homeassistant.components.remote.harmony pyharmony==1.0.12 +# homeassistant.components.binary_sensor.hikvision +pyhik==0.0.6 + # homeassistant.components.homematic pyhomematic==0.1.18 @@ -465,13 +482,19 @@ python-pushover==0.2 python-synology==0.1.0 # homeassistant.components.notify.telegram -python-telegram-bot==5.2.0 +python-telegram-bot==5.3.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 +# homeassistant.components.media_player.vlc +python-vlc==1.1.2 + # homeassistant.components.wink -python-wink==0.10.1 +python-wink==0.11.0 + +# homeassistant.components.device_tracker.unifi +pyunifi==1.3 # homeassistant.components.keyboard # pyuserinput==0.1.11 @@ -509,6 +532,12 @@ scsgate==0.1.0 # homeassistant.components.notify.sendgrid sendgrid==3.6.3 +# homeassistant.components.sensor.sensehat +sense-hat==2.2.0 + +# homeassistant.components.media_player.aquostv +sharp-aquos-rc==0.2 + # homeassistant.components.notify.slack slacker==0.9.30 @@ -542,7 +571,7 @@ steamodd==4.21 tellcore-py==1.1.2 # homeassistant.components.tellduslive -tellive-py==0.5.2 +tellduslive==0.1.13 # homeassistant.components.sensor.temper temperusb==1.5.1 @@ -560,9 +589,6 @@ twilio==5.4.0 # homeassistant.components.sensor.uber uber_rides==0.2.7 -# homeassistant.components.device_tracker.unifi -unifi==1.2.5 - # homeassistant.components.device_tracker.unifi urllib3 @@ -578,12 +604,17 @@ vsure==0.11.1 # homeassistant.components.sensor.vasttrafik vtjp==0.1.11 +# homeassistant.components.media_player.panasonic_viera +# homeassistant.components.media_player.webostv # homeassistant.components.switch.wake_on_lan wakeonlan==0.2.2 # homeassistant.components.media_player.gpmdp websocket-client==0.37.0 +# homeassistant.components.media_player.webostv +websockets==3.2 + # homeassistant.components.zigbee xbee-helper==0.0.7 diff --git a/requirements_docs.txt b/requirements_docs.txt index b39476859f2..e5331727ec9 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.4.9 +Sphinx==1.5.1 sphinx-autodoc-typehints==1.1.0 sphinx-autodoc-annotation==1.0.post1 diff --git a/script/release b/script/release index 40d906b17bf..43af9b92cb1 100755 --- a/script/release +++ b/script/release @@ -2,7 +2,15 @@ cd "$(dirname "$0")/.." -head -n 3 homeassistant/const.py | tail -n 1 | grep dev +head -n 5 homeassistant/const.py | tail -n 1 | grep PATCH_VERSION > /dev/null + +if [ $? -eq 1 ] +then + echo "Patch version not found on const.py line 5" + exit 1 +fi + +head -n 5 homeassistant/const.py | tail -n 1 | grep dev > /dev/null if [ $? -eq 0 ] then diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index e704b9b2d64..7ee2aadeaf6 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -42,16 +42,17 @@ class TestAutomationMQTT(unittest.TestCase): 'service': 'test.automation', 'data_template': { 'some': '{{ trigger.platform }} - {{ trigger.topic }}' - ' - {{ trigger.payload }}' + ' - {{ trigger.payload }} - ' + '{{ trigger.payload_json.hello }}' }, } } }) - fire_mqtt_message(self.hass, 'test-topic', 'test_payload') + fire_mqtt_message(self.hass, 'test-topic', '{ "hello": "world" }') self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - self.assertEqual('mqtt - test-topic - test_payload', + self.assertEqual('mqtt - test-topic - { "hello": "world" } - world', self.calls[0].data['some']) automation.turn_off(self.hass) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 1730c3e003b..7c4ee8db58f 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -181,34 +181,10 @@ class TestClimateGenericThermostat(unittest.TestCase): self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) - def test_set_temp_change_heater_on(self): - """Test if temperature change turn heater on.""" - self._setup_switch(False) - climate.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_temp_change_heater_off(self): - """Test if temperature change turn heater off.""" - self._setup_switch(True) - climate.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - def test_temp_change_heater_on_within_tolerance(self): - """Test if temperature change turn heater on within tolerance.""" + """Test if temperature change doesn't turn heater on within + tolerance. + """ self._setup_switch(False) climate.set_temperature(self.hass, 30) self.hass.block_till_done() @@ -217,9 +193,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_temp_change_heater_on_outside_tolerance(self): - """Test if temperature change doesn't turn heater on outside - tolerance. - """ + """Test if temperature change turn heater on outside tolerance.""" self._setup_switch(False) climate.set_temperature(self.hass, 30) self.hass.block_till_done() @@ -231,6 +205,30 @@ class TestClimateGenericThermostat(unittest.TestCase): self.assertEqual(SERVICE_TURN_ON, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def test_temp_change_heater_off_within_tolerance(self): + """Test if temperature change doesn't turn heater off within + tolerance. + """ + self._setup_switch(True) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(31) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_heater_off_outside_tolerance(self): + """Test if temperature change turn heater off outside tolerance.""" + self._setup_switch(True) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(35) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): """Setup the test sensor.""" self.hass.states.set(ENT_SENSOR, temp, { @@ -297,7 +295,18 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self.assertEqual(SERVICE_TURN_ON, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) - def test_set_temp_change_ac_off(self): + def test_temp_change_ac_off_within_tolerance(self): + """Test if temperature change doesn't turn ac off within + tolerance. + """ + self._setup_switch(True) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(29.8) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_set_temp_change_ac_off_outside_tolerance(self): """Test if temperature change turn ac off.""" self._setup_switch(True) climate.set_temperature(self.hass, 30) @@ -310,7 +319,18 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) - def test_temp_change_ac_on(self): + def test_temp_change_ac_on_within_tolerance(self): + """Test if temperature change doesn't turn ac on within + tolerance. + """ + self._setup_switch(False) + climate.set_temperature(self.hass, 25) + self.hass.block_till_done() + self._setup_sensor(25.2) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_ac_on_outside_tolerance(self): """Test if temperature change turn ac on.""" self._setup_switch(False) climate.set_temperature(self.hass, 25) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index e2ee21ab90d..c3087b108e9 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -256,6 +256,24 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.vendor, 'unknown') + def test_mac_vendor_lookup_on_see(self): + """Test if macvendor is looked up when device is seen.""" + mac = 'B8:27:EB:00:00:00' + vendor_string = 'Raspberry Pi Foundation' + + tracker = device_tracker.DeviceTracker( + self.hass, timedelta(seconds=60), 0, []) + + with mock_aiohttp_client() as aioclient_mock: + aioclient_mock.get('http://api.macvendors.com/b8:27:eb', + text=vendor_string) + + run_coroutine_threadsafe( + tracker.async_see(mac=mac), self.hass.loop).result() + assert aioclient_mock.call_count == 1, \ + 'No http request for macvendor made!' + self.assertEqual(tracker.devices['b827eb000000'].vendor, vendor_string) + def test_discovery(self): """Test discovery.""" scanner = get_component('device_tracker.test').SCANNER @@ -440,6 +458,45 @@ class TestComponentsDeviceTracker(unittest.TestCase): timedelta(seconds=0)) assert len(config) == 0 + def test_see_state(self): + """Test device tracker see records state correctly.""" + self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, + TEST_PLATFORM)) + + params = { + 'mac': 'AA:BB:CC:DD:EE:FF', + 'dev_id': 'some_device', + 'host_name': 'example.com', + 'location_name': 'Work', + 'gps': [.3, .8], + 'gps_accuracy': 1, + 'battery': 100, + 'attributes': { + 'test': 'test', + 'number': 1, + }, + } + + device_tracker.see(self.hass, **params) + self.hass.block_till_done() + + config = device_tracker.load_config(self.yaml_devices, self.hass, + timedelta(seconds=0)) + assert len(config) == 1 + + state = self.hass.states.get('device_tracker.examplecom') + attrs = state.attributes + self.assertEqual(state.state, 'Work') + self.assertEqual(state.object_id, 'examplecom') + self.assertEqual(state.name, 'example.com') + self.assertEqual(attrs['friendly_name'], 'example.com') + self.assertEqual(attrs['battery'], 100) + self.assertEqual(attrs['latitude'], 0.3) + self.assertEqual(attrs['longitude'], 0.8) + self.assertEqual(attrs['test'], 'test') + self.assertEqual(attrs['gps_accuracy'], 1) + self.assertEqual(attrs['number'], 1) + @patch('homeassistant.components.device_tracker._LOGGER.warning') def test_see_failures(self, mock_warning): """Test that the device tracker see failures.""" diff --git a/tests/components/device_tracker/test_unifi.py b/tests/components/device_tracker/test_unifi.py index 32ef8976196..12d296959dc 100644 --- a/tests/components/device_tracker/test_unifi.py +++ b/tests/components/device_tracker/test_unifi.py @@ -3,9 +3,10 @@ import unittest from unittest import mock import urllib -from unifi import controller +from pyunifi import controller import voluptuous as vol +from tests.common import get_test_home_assistant from homeassistant.components.device_tracker import DOMAIN, unifi as unifi from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM) @@ -14,6 +15,14 @@ from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, class TestUnifiScanner(unittest.TestCase): """Test the Unifiy platform.""" + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') @mock.patch.object(controller, 'Controller') def test_config_minimal(self, mock_ctrl, mock_scanner): @@ -25,7 +34,7 @@ class TestUnifiScanner(unittest.TestCase): CONF_PASSWORD: 'password', }) } - result = unifi.get_scanner(None, config) + result = unifi.get_scanner(self.hass, config) self.assertEqual(mock_scanner.return_value, result) self.assertEqual(mock_ctrl.call_count, 1) self.assertEqual( @@ -52,7 +61,7 @@ class TestUnifiScanner(unittest.TestCase): 'site_id': 'abcdef01', }) } - result = unifi.get_scanner(None, config) + result = unifi.get_scanner(self.hass, config) self.assertEqual(mock_scanner.return_value, result) self.assertEqual(mock_ctrl.call_count, 1) self.assertEqual( @@ -96,7 +105,7 @@ class TestUnifiScanner(unittest.TestCase): } mock_ctrl.side_effect = urllib.error.HTTPError( '/', 500, 'foo', {}, None) - result = unifi.get_scanner(None, config) + result = unifi.get_scanner(self.hass, config) self.assertFalse(result) def test_scanner_update(self): # pylint: disable=no-self-use diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 9cee27f570f..4aab6401939 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -7,7 +7,9 @@ import requests from homeassistant import bootstrap, const, core import homeassistant.components as core_components -from homeassistant.components import emulated_hue, http, light, script +from homeassistant.components import ( + emulated_hue, http, light, script, media_player +) from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.emulated_hue.hue_api import ( HUE_API_STATE_ON, HUE_API_STATE_BRI) @@ -73,6 +75,14 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): } }) + bootstrap.setup_component(cls.hass, media_player.DOMAIN, { + 'media_player': [ + { + 'platform': 'demo', + } + ] + }) + cls.hass.start() # Kitchen light is explicitly excluded from being exposed @@ -111,6 +121,10 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): self.assertTrue('light.bed_light' in result_json) self.assertTrue('script.set_kitchen_light' in result_json) self.assertTrue('light.kitchen_lights' not in result_json) + self.assertTrue('media_player.living_room' in result_json) + self.assertTrue('media_player.bedroom' in result_json) + self.assertTrue('media_player.walkman' in result_json) + self.assertTrue('media_player.lounge_room' in result_json) def test_get_light_state(self): """Test the getting of light state.""" @@ -128,6 +142,21 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): self.assertEqual(office_json['state'][HUE_API_STATE_ON], True) self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127) + # Check all lights view + result = requests.get( + BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('application/json' in result.headers['content-type']) + + result_json = result.json() + + self.assertTrue('light.ceiling_lights' in result_json) + self.assertEqual( + result_json['light.ceiling_lights']['state'][HUE_API_STATE_BRI], + 127, + ) + # Turn bedroom light off self.hass.services.call( light.DOMAIN, const.SERVICE_TURN_OFF, @@ -204,15 +233,38 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): self.assertEqual(script_result.status_code, 200) self.assertEqual(len(script_result_json), 2) - # Wait until script is complete before continuing - self.hass.block_till_done() - kitchen_light = self.hass.states.get('light.kitchen_lights') self.assertEqual(kitchen_light.state, 'on') self.assertEqual( kitchen_light.attributes[light.ATTR_BRIGHTNESS], level) + def test_put_light_state_media_player(self): + """Test turning on media player and setting volume.""" + # Turn the music player off first + self.hass.services.call( + media_player.DOMAIN, const.SERVICE_TURN_OFF, + {const.ATTR_ENTITY_ID: 'media_player.walkman'}, + blocking=True) + + # Emulated hue converts 0.0-1.0 to 0-255. + level = 0.25 + brightness = round(level * 255) + + mp_result = self.perform_put_light_state( + 'media_player.walkman', True, brightness) + + mp_result_json = mp_result.json() + + self.assertEqual(mp_result.status_code, 200) + self.assertEqual(len(mp_result_json), 2) + + walkman = self.hass.states.get('media_player.walkman') + self.assertEqual(walkman.state, 'playing') + self.assertEqual( + walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL], + level) + # pylint: disable=invalid-name def test_put_with_form_urlencoded_content_type(self): """Test the form with urlencoded content.""" @@ -352,4 +404,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): result = requests.put( url, data=json.dumps(data), timeout=5, headers=req_headers) + # Wait until state change is complete before continuing + self.hass.block_till_done() + return result diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index b7018945551..ff70fe36a17 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -5,6 +5,8 @@ import unittest from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) import homeassistant.components.switch as switch +import homeassistant.components.input_slider as input_slider +import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.media_player.universal as universal @@ -26,6 +28,7 @@ class MockMediaPlayer(media_player.MediaPlayerDevice): self._supported_media_commands = 0 self._source = None self._tracks = 12 + self._media_image_url = None self.service_calls = { 'turn_on': mock_service( @@ -90,6 +93,11 @@ class MockMediaPlayer(media_player.MediaPlayerDevice): """Supported media commands flag.""" return self._supported_media_commands + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._media_image_url + def turn_on(self): """Mock turn_on function.""" self._state = STATE_UNKNOWN @@ -142,6 +150,17 @@ class TestMediaPlayer(unittest.TestCase): self.mock_state_switch_id = switch.ENTITY_ID_FORMAT.format('state') self.hass.states.set(self.mock_state_switch_id, STATE_OFF) + self.mock_volume_id = input_slider.ENTITY_ID_FORMAT.format( + 'volume_level') + self.hass.states.set(self.mock_volume_id, 0) + + self.mock_source_list_id = input_select.ENTITY_ID_FORMAT.format( + 'source_list') + self.hass.states.set(self.mock_source_list_id, ['dvd', 'htpc']) + + self.mock_source_id = input_select.ENTITY_ID_FORMAT.format('source') + self.hass.states.set(self.mock_source_id, 'dvd') + self.config_children_only = { 'name': 'test', 'platform': 'universal', 'children': [media_player.ENTITY_ID_FORMAT.format('mock1'), @@ -153,6 +172,9 @@ class TestMediaPlayer(unittest.TestCase): media_player.ENTITY_ID_FORMAT.format('mock2')], 'attributes': { 'is_volume_muted': self.mock_mute_switch_id, + 'volume_level': self.mock_volume_id, + 'source': self.mock_source_id, + 'source_list': self.mock_source_list_id, 'state': self.mock_state_switch_id } } @@ -384,6 +406,26 @@ class TestMediaPlayer(unittest.TestCase): ump.update() self.assertEqual(1, ump.volume_level) + def test_media_image_url(self): + """Test media_image_url property.""" + TEST_URL = "test_url" + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertEqual(None, ump.media_image_url) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1._media_image_url = TEST_URL + self.mock_mp_1.update_ha_state() + ump.update() + # mock_mp_1 will convert the url to the api proxy url. This test + # ensures ump passes through the same url without an additional proxy. + self.assertEqual(self.mock_mp_1.entity_picture, ump.entity_picture) + def test_is_volume_muted_children_only(self): """Test is volume muted property w/ children only.""" config = self.config_children_only @@ -405,6 +447,42 @@ class TestMediaPlayer(unittest.TestCase): ump.update() self.assertTrue(ump.is_volume_muted) + def test_source_list_children_and_attr(self): + """Test source list property w/ children and attrs.""" + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual("['dvd', 'htpc']", ump.source_list) + + self.hass.states.set(self.mock_source_list_id, ['dvd', 'htpc', 'game']) + self.assertEqual("['dvd', 'htpc', 'game']", ump.source_list) + + def test_source_children_and_attr(self): + """Test source property w/ children and attrs.""" + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual('dvd', ump.source) + + self.hass.states.set(self.mock_source_id, 'htpc') + self.assertEqual('htpc', ump.source) + + def test_volume_level_children_and_attr(self): + """Test volume level property w/ children and attrs.""" + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual('0', ump.volume_level) + + self.hass.states.set(self.mock_volume_id, 100) + self.assertEqual('100', ump.volume_level) + def test_is_volume_muted_children_and_attr(self): """Test is volume muted property w/ children and attrs.""" config = self.config_children_and_attr @@ -443,18 +521,20 @@ class TestMediaPlayer(unittest.TestCase): config['commands']['volume_up'] = 'test' config['commands']['volume_down'] = 'test' config['commands']['volume_mute'] = 'test' + config['commands']['volume_set'] = 'test' + config['commands']['select_source'] = 'test' ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) ump.update() - self.mock_mp_1._supported_media_commands = universal.SUPPORT_VOLUME_SET self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() ump.update() check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ - | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE + | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE \ + | universal.SUPPORT_SELECT_SOURCE self.assertEqual(check_flags, ump.supported_media_commands) diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index bbaca71ee13..6a2f8c7acbf 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -34,16 +34,18 @@ class TestNotifySmtp(unittest.TestCase): def test_text_email(self): """Test build of default text email behavior.""" msg = self.mailer.send_message('Test msg') - expected = ('Content-Type: text/plain; charset="us-ascii"\n' + expected = ('^Content-Type: text/plain; charset="us-ascii"\n' 'MIME-Version: 1.0\n' 'Content-Transfer-Encoding: 7bit\n' 'Subject: Home Assistant\n' 'To: testrecip@test.com\n' 'From: test@test.com\n' 'X-Mailer: HomeAssistant\n' + 'Date: [^\n]+\n' + 'Message-Id: <[^@]+@[^>]+>\n' '\n' - 'Test msg') - self.assertEqual(msg, expected) + 'Test msg$') + self.assertRegex(msg, expected) def test_mixed_email(self): """Test build of mixed text email behavior.""" diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 09ced049b58..976a9278452 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -17,6 +17,15 @@ from tests.common import load_fixture, get_test_home_assistant class TestDarkSkySetup(unittest.TestCase): """Test the Dark Sky platform.""" + def add_entities(self, new_entities, update_before_add=False): + """Mock add entities.""" + if update_before_add: + for entity in new_entities: + entity.update() + + for entity in new_entities: + self.entities.append(entity) + def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() @@ -30,6 +39,7 @@ class TestDarkSkySetup(unittest.TestCase): self.lon = -122.423 self.hass.config.latitude = self.lat self.hass.config.longitude = self.lon + self.entities = [] def test_setup_with_config(self): """Test the platform setup with configuration.""" @@ -63,6 +73,7 @@ class TestDarkSkySetup(unittest.TestCase): r'(-?\d+\.?\d*),(-?\d+\.?\d*)') mock_req.get(re.compile(uri), text=load_fixture('darksky.json')) - darksky.setup_platform(self.hass, self.config, MagicMock()) + darksky.setup_platform(self.hass, self.config, self.add_entities) self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) + self.assertEqual(len(self.entities), 2) diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 166a4af9657..e76b26a811b 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -9,6 +9,8 @@ from decimal import Decimal from unittest.mock import Mock from homeassistant.bootstrap import async_setup_component +from homeassistant.components.sensor.dsmr import DerivativeDSMREntity +from homeassistant.const import STATE_UNKNOWN from tests.common import assert_setup_component @@ -62,3 +64,37 @@ def test_default_setup(hass, monkeypatch): power_tariff = hass.states.get('sensor.power_tariff') assert power_tariff.state == 'low' assert power_tariff.attributes.get('unit_of_measurement') is None + + +def test_derivative(): + """Test calculation of derivative value.""" + from dsmr_parser.objects import MBusObject + + entity = DerivativeDSMREntity('test', '1.0.0') + yield from entity.async_update() + + assert entity.state == STATE_UNKNOWN, 'initial state not unknown' + + entity.telegram = { + '1.0.0': MBusObject([ + {'value': 1}, + {'value': 1, 'unit': 'm3'}, + ]) + } + yield from entity.async_update() + + assert entity.state == STATE_UNKNOWN, \ + 'state after first update shoudl still be unknown' + + entity.telegram = { + '1.0.0': MBusObject([ + {'value': 2}, + {'value': 2, 'unit': 'm3'}, + ]) + } + yield from entity.async_update() + + assert entity.state == 1, \ + 'state should be difference between first and second update' + + assert entity.unit_of_measurement == 'm3/h' diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 05c7fc93921..7c92ac20424 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -23,6 +23,16 @@ VALID_CONFIG = { ] } +INVALID_CONFIG = { + 'platform': 'wunderground', + 'api_key': 'BOB', + 'pws_id': 'bar', + 'lang': 'foo', + 'monitored_conditions': [ + 'weather', 'feelslike_c', 'alerts' + ] +} + FEELS_LIKE = '40' WEATHER = 'Clear' HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif' @@ -128,17 +138,9 @@ class TestWundergroundSetup(unittest.TestCase): self.assertTrue( wunderground.setup_platform(self.hass, VALID_CONFIG, self.add_devices, None)) - invalid_config = { - 'platform': 'wunderground', - 'api_key': 'BOB', - 'pws_id': 'bar', - 'monitored_conditions': [ - 'weather', 'feelslike_c', 'alerts' - ] - } self.assertTrue( - wunderground.setup_platform(self.hass, invalid_config, + wunderground.setup_platform(self.hass, INVALID_CONFIG, self.add_devices, None)) @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index dc6c58db928..38ddad5e9a2 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -1,76 +1,83 @@ """The tests for the REST switch platform.""" -import unittest -from unittest.mock import patch +import asyncio -import pytest -import requests -from requests.exceptions import Timeout -import requests_mock +import aiohttp import homeassistant.components.switch.rest as rest from homeassistant.bootstrap import setup_component +from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant, assert_setup_component -class TestRestSwitchSetup(unittest.TestCase): +class TestRestSwitchSetup: """Tests for setting up the REST switch platform.""" - def setUp(self): + def setup_method(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): + def teardown_method(self): """Stop everything that was started.""" self.hass.stop() def test_setup_missing_config(self): """Test setup with configuration missing required entries.""" - self.assertFalse(rest.setup_platform(self.hass, { - 'platform': 'rest' - }, None)) + assert not run_coroutine_threadsafe( + rest.async_setup_platform(self.hass, { + 'platform': 'rest' + }, None), + self.hass.loop + ).result() def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - self.assertFalse(rest.setup_platform(self.hass, { - 'platform': 'rest', - 'resource': 'localhost' - }, None)) + assert not run_coroutine_threadsafe( + rest.async_setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'localhost' + }, None), + self.hass.loop + ).result() - @patch('requests.get', side_effect=requests.exceptions.ConnectionError()) - def test_setup_failed_connect(self, mock_req): + def test_setup_failed_connect(self, aioclient_mock): """Test setup when connection error occurs.""" - self.assertFalse(rest.setup_platform(self.hass, { - 'platform': 'rest', - 'resource': 'http://localhost', - }, None)) - - @patch('requests.get', side_effect=Timeout()) - def test_setup_timeout(self, mock_req): - """Test setup when connection timeout occurs.""" - with self.assertRaises(Timeout): - rest.setup_platform(self.hass, { + aioclient_mock.get('http://localhost', exc=aiohttp.errors.ClientError) + assert not run_coroutine_threadsafe( + rest.async_setup_platform(self.hass, { 'platform': 'rest', 'resource': 'http://localhost', - }, None) + }, None), + self.hass.loop + ).result() - @requests_mock.Mocker() - def test_setup_minimum(self, mock_req): - """Test setup with minimum configuration.""" - mock_req.get('http://localhost', status_code=200) - self.assertTrue(setup_component(self.hass, 'switch', { - 'switch': { + def test_setup_timeout(self, aioclient_mock): + """Test setup when connection timeout occurs.""" + aioclient_mock.get('http://localhost', exc=asyncio.TimeoutError()) + assert not run_coroutine_threadsafe( + rest.async_setup_platform(self.hass, { 'platform': 'rest', - 'resource': 'http://localhost' - } - })) - self.assertEqual(1, mock_req.call_count) - assert_setup_component(1, 'switch') + 'resource': 'http://localhost', + }, None), + self.hass.loop + ).result() - @requests_mock.Mocker() - def test_setup(self, mock_req): + def test_setup_minimum(self, aioclient_mock): + """Test setup with minimum configuration.""" + aioclient_mock.get('http://localhost', status=200) + with assert_setup_component(1, 'switch'): + assert setup_component(self.hass, 'switch', { + 'switch': { + 'platform': 'rest', + 'resource': 'http://localhost' + } + }) + assert aioclient_mock.call_count == 1 + + def test_setup(self, aioclient_mock): """Test setup with valid configuration.""" - mock_req.get('localhost', status_code=200) - self.assertTrue(setup_component(self.hass, 'switch', { + aioclient_mock.get('http://localhost', status=200) + assert setup_component(self.hass, 'switch', { 'switch': { 'platform': 'rest', 'name': 'foo', @@ -78,111 +85,120 @@ class TestRestSwitchSetup(unittest.TestCase): 'body_on': 'custom on text', 'body_off': 'custom off text', } - })) - self.assertEqual(1, mock_req.call_count) + }) + assert aioclient_mock.call_count == 1 assert_setup_component(1, 'switch') -@pytest.mark.skip -class TestRestSwitch(unittest.TestCase): +class TestRestSwitch: """Tests for REST switch platform.""" - def setUp(self): + def setup_method(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.name = 'foo' self.resource = 'http://localhost/' - self.body_on = 'on' - self.body_off = 'off' + self.body_on = Template('on', self.hass) + self.body_off = Template('off', self.hass) self.switch = rest.RestSwitch(self.hass, self.name, self.resource, - self.body_on, self.body_off) + self.body_on, self.body_off, None, 10) - def tearDown(self): + def teardown_method(self): """Stop everything that was started.""" self.hass.stop() def test_name(self): """Test the name.""" - self.assertEqual(self.name, self.switch.name) + assert self.name == self.switch.name def test_is_on_before_update(self): """Test is_on in initial state.""" - self.assertEqual(None, self.switch.is_on) + assert self.switch.is_on is None - @requests_mock.Mocker() - def test_turn_on_success(self, mock_req): + def test_turn_on_success(self, aioclient_mock): """Test turn_on.""" - mock_req.post(self.resource, status_code=200) - self.switch.turn_on() + aioclient_mock.post(self.resource, status=200) + run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop).result() - self.assertEqual(self.body_on, mock_req.last_request.text) - self.assertEqual(True, self.switch.is_on) + assert self.body_on.template == \ + aioclient_mock.mock_calls[-1][2].decode() + assert self.switch.is_on - @requests_mock.Mocker() - def test_turn_on_status_not_ok(self, mock_req): + def test_turn_on_status_not_ok(self, aioclient_mock): """Test turn_on when error status returned.""" - mock_req.post(self.resource, status_code=500) - self.switch.turn_on() + aioclient_mock.post(self.resource, status=500) + run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop).result() - self.assertEqual(self.body_on, mock_req.last_request.text) - self.assertEqual(None, self.switch.is_on) + assert self.body_on.template == \ + aioclient_mock.mock_calls[-1][2].decode() + assert self.switch.is_on is None - @patch('requests.post', side_effect=Timeout()) - def test_turn_on_timeout(self, mock_req): + def test_turn_on_timeout(self, aioclient_mock): """Test turn_on when timeout occurs.""" - with self.assertRaises(Timeout): - self.switch.turn_on() + aioclient_mock.post(self.resource, status=500) + run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop).result() - @requests_mock.Mocker() - def test_turn_off_success(self, mock_req): + assert self.switch.is_on is None + + def test_turn_off_success(self, aioclient_mock): """Test turn_off.""" - mock_req.post(self.resource, status_code=200) - self.switch.turn_off() + aioclient_mock.post(self.resource, status=200) + run_coroutine_threadsafe( + self.switch.async_turn_off(), self.hass.loop).result() - self.assertEqual(self.body_off, mock_req.last_request.text) - self.assertEqual(False, self.switch.is_on) + assert self.body_off.template == \ + aioclient_mock.mock_calls[-1][2].decode() + assert not self.switch.is_on - @requests_mock.Mocker() - def test_turn_off_status_not_ok(self, mock_req): + def test_turn_off_status_not_ok(self, aioclient_mock): """Test turn_off when error status returned.""" - mock_req.post(self.resource, status_code=500) - self.switch.turn_off() + aioclient_mock.post(self.resource, status=500) + run_coroutine_threadsafe( + self.switch.async_turn_off(), self.hass.loop).result() - self.assertEqual(self.body_off, mock_req.last_request.text) - self.assertEqual(None, self.switch.is_on) + assert self.body_off.template == \ + aioclient_mock.mock_calls[-1][2].decode() + assert self.switch.is_on is None - @patch('requests.post', side_effect=Timeout()) - def test_turn_off_timeout(self, mock_req): + def test_turn_off_timeout(self, aioclient_mock): """Test turn_off when timeout occurs.""" - with self.assertRaises(Timeout): - self.switch.turn_on() + aioclient_mock.post(self.resource, exc=asyncio.TimeoutError()) + run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop).result() - @requests_mock.Mocker() - def test_update_when_on(self, mock_req): + assert self.switch.is_on is None + + def test_update_when_on(self, aioclient_mock): """Test update when switch is on.""" - mock_req.get(self.resource, text=self.body_on) - self.switch.update() + aioclient_mock.get(self.resource, text=self.body_on.template) + run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop).result() - self.assertEqual(True, self.switch.is_on) + assert self.switch.is_on - @requests_mock.Mocker() - def test_update_when_off(self, mock_req): + def test_update_when_off(self, aioclient_mock): """Test update when switch is off.""" - mock_req.get(self.resource, text=self.body_off) - self.switch.update() + aioclient_mock.get(self.resource, text=self.body_off.template) + run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop).result() - self.assertEqual(False, self.switch.is_on) + assert not self.switch.is_on - @requests_mock.Mocker() - def test_update_when_unknown(self, mock_req): + def test_update_when_unknown(self, aioclient_mock): """Test update when unknown status returned.""" - mock_req.get(self.resource, text='unknown status') - self.switch.update() + aioclient_mock.get(self.resource, text='unknown status') + run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop).result() - self.assertEqual(None, self.switch.is_on) + assert self.switch.is_on is None - @patch('requests.get', side_effect=Timeout()) - def test_update_timeout(self, mock_req): + def test_update_timeout(self, aioclient_mock): """Test update when timeout occurs.""" - with self.assertRaises(Timeout): - self.switch.update() + aioclient_mock.get(self.resource, exc=asyncio.TimeoutError()) + run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop).result() + + assert self.switch.is_on is None diff --git a/tests/components/test_group.py b/tests/components/test_group.py index c5b705cbc43..00b75c3a854 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -228,6 +228,7 @@ class TestComponentsGroup(unittest.TestCase): 'entities': 'light.Bowl, ' + test_group.entity_id, 'icon': 'mdi:work', 'view': True, + 'control': 'hidden', } group_conf['test_group'] = 'hello.world,sensor.happy' group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None} @@ -243,6 +244,8 @@ class TestComponentsGroup(unittest.TestCase): self.assertEqual('mdi:work', group_state.attributes.get(ATTR_ICON)) self.assertTrue(group_state.attributes.get(group.ATTR_VIEW)) + self.assertEqual('hidden', + group_state.attributes.get(group.ATTR_CONTROL)) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) self.assertEqual(1, group_state.attributes.get(group.ATTR_ORDER)) @@ -254,6 +257,7 @@ class TestComponentsGroup(unittest.TestCase): self.assertIsNone(group_state.attributes.get(group.ATTR_AUTO)) self.assertIsNone(group_state.attributes.get(ATTR_ICON)) self.assertIsNone(group_state.attributes.get(group.ATTR_VIEW)) + self.assertIsNone(group_state.attributes.get(group.ATTR_CONTROL)) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) self.assertEqual(2, group_state.attributes.get(group.ATTR_ORDER)) diff --git a/tests/components/tts/__init__.py b/tests/components/tts/__init__.py new file mode 100644 index 00000000000..f5eb0731409 --- /dev/null +++ b/tests/components/tts/__init__.py @@ -0,0 +1 @@ +"""The tests for tts platforms.""" diff --git a/tests/components/tts/test_google.py b/tests/components/tts/test_google.py new file mode 100644 index 00000000000..623a96f1dfb --- /dev/null +++ b/tests/components/tts/test_google.py @@ -0,0 +1,199 @@ +"""The tests for the Google speech platform.""" +import asyncio +import os +import shutil +from unittest.mock import patch + +import homeassistant.components.tts as tts +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP) +from homeassistant.bootstrap import setup_component + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_service) + + +class TestTTSGooglePlatform(object): + """Test the Google speech component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.url = "http://translate.google.com/translate_tts" + self.url_param = { + 'tl': 'en', + 'q': 'I%20person%20is%20on%20front%20of%20your%20door.', + 'tk': 5, + 'client': 'tw-ob', + 'textlen': 34, + 'total': 1, + 'idx': 0, + 'ie': 'UTF-8', + } + + def teardown_method(self): + """Stop everything that was started.""" + default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) + + self.hass.stop() + + def test_setup_component(self): + """Test setup component.""" + config = { + tts.DOMAIN: { + 'platform': 'google', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + @patch('gtts_token.gtts_token.Token.calculate_token', autospec=True, + return_value=5) + def test_service_say(self, mock_calculate, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'google', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'google_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert len(aioclient_mock.mock_calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 + + @patch('gtts_token.gtts_token.Token.calculate_token', autospec=True, + return_value=5) + def test_service_say_german(self, mock_calculate, aioclient_mock): + """Test service call say with german code.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + self.url_param['tl'] = 'de' + aioclient_mock.get( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'google', + 'language': 'de', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'google_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert len(aioclient_mock.mock_calls) == 1 + + @patch('gtts_token.gtts_token.Token.calculate_token', autospec=True, + return_value=5) + def test_service_say_error(self, mock_calculate, aioclient_mock): + """Test service call say with http response 400.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=400, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'google', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'google_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 + + @patch('gtts_token.gtts_token.Token.calculate_token', autospec=True, + return_value=5) + def test_service_say_timeout(self, mock_calculate, aioclient_mock): + """Test service call say with http timeout.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, exc=asyncio.TimeoutError()) + + config = { + tts.DOMAIN: { + 'platform': 'google', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'google_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 + + @patch('gtts_token.gtts_token.Token.calculate_token', autospec=True, + return_value=5) + def test_service_say_long_size(self, mock_calculate, aioclient_mock): + """Test service call say with a lot of text.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + self.url_param['total'] = 9 + self.url_param['q'] = "I%20person%20is%20on%20front%20of%20your%20door" + self.url_param['textlen'] = 33 + for idx in range(0, 9): + self.url_param['idx'] = idx + aioclient_mock.get( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'google', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'google_say', { + tts.ATTR_MESSAGE: ("I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door." + "I person is on front of your door."), + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert len(aioclient_mock.mock_calls) == 9 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py new file mode 100644 index 00000000000..fbdbddb8db5 --- /dev/null +++ b/tests/components/tts/test_init.py @@ -0,0 +1,320 @@ +"""The tests for the TTS component.""" +import os +import shutil +from unittest.mock import patch + +import requests + +import homeassistant.components.tts as tts +from homeassistant.components.tts.demo import DemoProvider +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, MEDIA_TYPE_MUSIC, ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP) +from homeassistant.bootstrap import setup_component + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_service) + + +class TestTTS(object): + """Test the Google speech component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.demo_provider = DemoProvider() + self.default_tts_cache = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + + def teardown_method(self): + """Stop everything that was started.""" + if os.path.isdir(self.default_tts_cache): + shutil.rmtree(self.default_tts_cache) + + self.hass.stop() + + def test_setup_component_demo(self): + """Setup the demo platform with defaults.""" + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + assert self.hass.services.has_service(tts.DOMAIN, 'demo_say') + assert self.hass.services.has_service(tts.DOMAIN, 'clear_cache') + + @patch('os.mkdir', side_effect=OSError(2, "No access")) + def test_setup_component_demo_no_access_cache_folder(self, mock_mkdir): + """Setup the demo platform with defaults.""" + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + assert not setup_component(self.hass, tts.DOMAIN, config) + + assert not self.hass.services.has_service(tts.DOMAIN, 'demo_say') + assert not self.hass.services.has_service(tts.DOMAIN, 'clear_cache') + + def test_setup_component_and_test_service(self): + """Setup the demo platform and call service.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find( + "/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" + "_demo.mp3") \ + != -1 + assert os.path.isfile(os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3")) + + def test_setup_component_and_test_service_clear_cache(self): + """Setup the demo platform and call service clear cache.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert os.path.isfile(os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3")) + + self.hass.services.call(tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}) + self.hass.block_till_done() + + assert not os.path.isfile(os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3")) + + def test_setup_component_and_test_service_with_receive_voice(self): + """Setup the demo platform and call service and receive voice.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.start() + + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) + _, demo_data = self.demo_provider.get_tts_audio("bla") + assert req.status_code == 200 + assert req.content == demo_data + + def test_setup_component_and_web_view_wrong_file(self): + """Setup the demo platform and receive wrong file from web.""" + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.start() + + url = ("{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" + "_demo.mp3").format(self.hass.config.api.base_url) + + req = requests.get(url) + assert req.status_code == 404 + + def test_setup_component_and_web_view_wrong_filename(self): + """Setup the demo platform and receive wrong filename from web.""" + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.start() + + url = ("{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd" + "_demo.mp3").format(self.hass.config.api.base_url) + + req = requests.get(url) + assert req.status_code == 404 + + def test_setup_component_test_without_cache(self): + """Setup demo platform without cache.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + 'cache': False, + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert not os.path.isfile(os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3")) + + def test_setup_component_test_with_cache_call_service_without_cache(self): + """Setup demo platform with cache and call service without cache.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + 'cache': True, + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + tts.ATTR_CACHE: False, + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert not os.path.isfile(os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3")) + + def test_setup_component_test_with_cache_dir(self): + """Setup demo platform with cache and call service without cache.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + _, demo_data = self.demo_provider.get_tts_audio("bla") + cache_file = os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3") + + os.mkdir(self.default_tts_cache) + with open(cache_file, "wb") as voice_file: + voice_file.write(demo_data) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + 'cache': True, + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + with patch('homeassistant.components.tts.demo.DemoProvider.' + 'get_tts_audio', return_value=None): + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find( + "/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" + "_demo.mp3") \ + != -1 + + @patch('homeassistant.components.tts.demo.DemoProvider.get_tts_audio', + return_value=None) + def test_setup_component_test_with_error_on_get_tts(self, tts_mock): + """Setup demo platform with wrong get_tts_audio.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = { + tts.DOMAIN: { + 'platform': 'demo' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'demo_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + + def test_setup_component_load_cache_retrieve_without_mem_cache(self): + """Setup component and load cache and get without mem cache.""" + _, demo_data = self.demo_provider.get_tts_audio("bla") + cache_file = os.path.join( + self.default_tts_cache, + "265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3") + + os.mkdir(self.default_tts_cache) + with open(cache_file, "wb") as voice_file: + voice_file.write(demo_data) + + config = { + tts.DOMAIN: { + 'platform': 'demo', + 'cache': True, + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.start() + + url = ("{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" + "_demo.mp3").format(self.hass.config.api.base_url) + + req = requests.get(url) + assert req.status_code == 200 + assert req.content == demo_data diff --git a/tests/components/tts/test_voicerss.py b/tests/components/tts/test_voicerss.py new file mode 100644 index 00000000000..44ce0d6739f --- /dev/null +++ b/tests/components/tts/test_voicerss.py @@ -0,0 +1,162 @@ +"""The tests for the VoiceRSS speech platform.""" +import asyncio +import os +import shutil + +import homeassistant.components.tts as tts +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP) +from homeassistant.bootstrap import setup_component + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_service) + + +class TestTTSVoiceRSSPlatform(object): + """Test the voicerss speech component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.url = "https://api.voicerss.org/" + self.url_param = { + 'key': '1234567xx', + 'hl': 'en-us', + 'c': 'MP3', + 'f': '8khz_8bit_mono', + } + + def teardown_method(self): + """Stop everything that was started.""" + default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) + + self.hass.stop() + + def test_setup_component(self): + """Test setup component.""" + config = { + tts.DOMAIN: { + 'platform': 'voicerss', + 'api_key': '1234567xx' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + def test_setup_component_without_api_key(self): + """Test setup component without api key.""" + config = { + tts.DOMAIN: { + 'platform': 'voicerss', + } + } + + with assert_setup_component(0, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + def test_service_say(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.post( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'voicerss', + 'api_key': '1234567xx', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'voicerss_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert len(aioclient_mock.mock_calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 + + def test_service_say_german(self, aioclient_mock): + """Test service call say with german code.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + self.url_param['hl'] = 'de-de' + aioclient_mock.post( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'voicerss', + 'api_key': '1234567xx', + 'language': 'de-de', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'voicerss_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 1 + assert len(aioclient_mock.mock_calls) == 1 + + def test_service_say_error(self, aioclient_mock): + """Test service call say with http response 400.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.post( + self.url, params=self.url_param, status=400, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'voicerss', + 'api_key': '1234567xx', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'voicerss_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 + + def test_service_say_timeout(self, aioclient_mock): + """Test service call say with http timeout.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.post( + self.url, params=self.url_param, exc=asyncio.TimeoutError()) + + config = { + tts.DOMAIN: { + 'platform': 'voicerss', + 'api_key': '1234567xx', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'voicerss_say', { + tts.ATTR_MESSAGE: "I person is on front of your door.", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index dd3b09d932f..887ef7c2c20 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -70,6 +70,8 @@ class TestBootstrap: with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ mock.patch('os.access', mock.Mock(return_value=True)), \ + mock.patch('homeassistant.bootstrap.enable_logging', + mock.Mock(return_value=True)), \ patch_yaml_files(files, True): self.hass = bootstrap.from_config_file('config.yaml') @@ -286,7 +288,8 @@ class TestBootstrap: assert not bootstrap.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components - def test_home_assistant_core_config_validation(self): + @mock.patch('homeassistant.bootstrap.enable_logging') + def test_home_assistant_core_config_validation(self, log_mock): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done in test_config.py assert None is bootstrap.from_config_dict({ diff --git a/tests/test_core.py b/tests/test_core.py index 9221ad68352..4049d10d32d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,6 +6,7 @@ from unittest.mock import patch, MagicMock from datetime import datetime, timedelta import pytz +import pytest import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError @@ -214,6 +215,11 @@ class TestHomeAssistant(unittest.TestCase): assert len(self.hass._pending_tasks) == 0 assert len(call_count) == 2 + def test_add_job_with_none(self): + """Try to add a job with None as function.""" + with pytest.raises(ValueError): + self.hass.add_job(None, 'test_arg') + class TestEvent(unittest.TestCase): """A Test Event class.""" diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d6f0c80b435..c0ed579f197 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -20,9 +20,11 @@ class AiohttpClientMocker: auth=None, status=200, text=None, + data=None, content=None, json=None, params=None, + headers=None, exc=None): """Mock a request.""" if json: @@ -65,19 +67,19 @@ class AiohttpClientMocker: return len(self.mock_calls) @asyncio.coroutine - def match_request(self, method, url, *, auth=None, params=None): \ - # pylint: disable=unused-variable + def match_request(self, method, url, *, data=None, auth=None, params=None, + headers=None): # pylint: disable=unused-variable """Match a request against pre-registered requests.""" for response in self._mocks: if response.match_request(method, url, params): - self.mock_calls.append((method, url)) + self.mock_calls.append((method, url, data)) if self.exc: raise self.exc return response - assert False, "No mock registered for {} {}".format(method.upper(), - url) + assert False, "No mock registered for {} {} {}".format(method.upper(), + url, params) class AiohttpClientMockResponse: