diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..58b3ff7bbf6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,50 +10,72 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/isy994.py + homeassistant/components/*/isy994.py + + homeassistant/components/modbus.py + homeassistant/components/*/modbus.py + + homeassistant/components/*/tellstick.py + homeassistant/components/*/vera.py + + homeassistant/components/verisure.py + homeassistant/components/*/verisure.py + homeassistant/components/wink.py homeassistant/components/*/wink.py homeassistant/components/zwave.py homeassistant/components/*/zwave.py - homeassistant/components/modbus.py - homeassistant/components/*/modbus.py - - homeassistant/components/isy994.py - homeassistant/components/*/isy994.py - - homeassistant/components/*/tellstick.py - homeassistant/components/*/vera.py - homeassistant/components/browser.py + homeassistant/components/camera/* + homeassistant/components/device_tracker/actiontec.py + homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py + homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py + homeassistant/components/device_tracker/tplink.py + homeassistant/components/discovery.py + homeassistant/components/downloader.py homeassistant/components/keyboard.py homeassistant/components/light/hue.py + homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py + homeassistant/components/media_player/squeezebox.py homeassistant/components/notify/file.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushover.py + homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py homeassistant/components/notify/xmpp.py homeassistant/components/sensor/bitcoin.py + homeassistant/components/sensor/dht.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/rfxtrx.py + homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py + homeassistant/components/sensor/temper.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py + homeassistant/components/switch/command_switch.py + homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py + homeassistant/components/switch/rpi_gpio.py + homeassistant/components/switch/transmission.py homeassistant/components/switch/wemo.py homeassistant/components/thermostat/nest.py diff --git a/.gitignore b/.gitignore index 8dab1d873da..658ad279292 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ homeassistant/components/frontend/www_static/polymer/bower_components/* config/custom_components/* !config/custom_components/example.py !config/custom_components/hello_world.py +!config/custom_components/mqtt_example.py # Hide sublime text stuff *.sublime-project diff --git a/.travis.yml b/.travis.yml index 7af8ce86dcd..65e417fffb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python python: - "3.4" install: - - pip install -r requirements.txt + - pip install -r requirements_all.txt - pip install flake8 pylint coveralls script: - flake8 homeassistant --exclude bower_components,external diff --git a/Dockerfile b/Dockerfile index ff34dc79297..8ce295ae6aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,12 @@ MAINTAINER Paulus Schoutsen VOLUME /config -RUN apt-get update && \ - apt-get install -y cython3 libudev-dev && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ - pip3 install cython && \ - scripts/build_python_openzwave +RUN pip3 install --no-cache-dir -r requirements_all.txt + +#RUN apt-get update && \ +# apt-get install -y cython3 libudev-dev && \ +# apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ +# pip3 install cython && \ +# scripts/build_python_openzwave CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/README.md b/README.md index 18a01345741..d6fed371b64 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,26 @@ # Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. [Open a demo.](https://home-assistant.io/demo/) +[demo]: https://home-assistant.io/demo/ -Check out [the website](https://home-assistant.io) for installation instructions, tutorials and documentation. +Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. -[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/) +To get started: +```bash +python3 -m pip install homeassistant +hass --open-ui +``` + +Check out [the website](https://home-assistant.io) for [a demo][demo], installation instructions, tutorials and documentation. + +[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)][demo] Examples of devices it can interface it: - * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/) - * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and [Kodi (XBMC)](http://kodi.tv/) - * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/) - * Integrate data from the [Bitcoin](https://bitcoin.org) network, local meteorological data from [OpenWeatherMap](http://openweathermap.org/), [Transmission](http://www.transmissionbt.com/) or [SABnzbd](http://sabnzbd.org). + * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) + * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Efergy](https://efergy.com) plugs, [Edimax](http://www.edimax.com/) switches, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors + * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), and [Kodi (XBMC)](http://kodi.tv/) + * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) + * Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org). * [See full list of supported devices](https://home-assistant.io/components/) Built home automation on top of your devices: @@ -21,26 +29,9 @@ Built home automation on top of your devices: * Turn on the lights when people get home after sun set * Turn on lights slowly during sun set to compensate for less light * Turn off all lights and devices when everybody leaves the house - * Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects - * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), and [Jabber (XMPP)](http://xmpp.org) + * Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects + * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org) The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html). -If you run into issues while using Home Assistant or during development of a component, reach out to the [Home Assistant help section](https://home-assistant.io/help/) how to reach us. - -## Quick-start guide - -Running Home Assistant requires [Python 3.4](https://www.python.org/). Run the following code to get up and running: - -``` -git clone --recursive https://github.com/balloob/home-assistant.git -python3 -m venv home-assistant -cd home-assistant -python3 -m homeassistant --open-ui -``` - -The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/configuration.yaml`. A standard configuration file will be written if none exists. - -If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command. - -Please see [the getting started guide](https://home-assistant.io/getting-started/) on how to further configure Home Assistant. +If you run into issues while using Home Assistant or during development of a component, check the [Home Assistant help section](https://home-assistant.io/help/) how to reach us. diff --git a/config/custom_components/mqtt_example.py b/config/custom_components/mqtt_example.py new file mode 100644 index 00000000000..5b54226cb7c --- /dev/null +++ b/config/custom_components/mqtt_example.py @@ -0,0 +1,60 @@ +""" +custom_components.mqtt_example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Shows how to communicate with MQTT. Follows a topic on MQTT and updates the +state of an entity to the last message received on that topic. + +Also offers a service 'set_state' that will publish a message on the topic that +will be passed via MQTT to our message received listener. Call the service with +example payload {"new_state": "some new state"}. + +Configuration: + +To use the mqtt_example component you will need to add the following to your +config/configuration.yaml + +mqtt_example: + topic: home-assistant/mqtt_example + +""" +import homeassistant.loader as loader + +# The domain of your component. Should be equal to the name of your component +DOMAIN = "mqtt_example" + +# List of component names (string) your component depends upon +DEPENDENCIES = ['mqtt'] + + +CONF_TOPIC = 'topic' +DEFAULT_TOPIC = 'home-assistant/mqtt_example' + + +def setup(hass, config): + """ Setup our mqtt_example component. """ + mqtt = loader.get_component('mqtt') + topic = config[DOMAIN].get('topic', DEFAULT_TOPIC) + entity_id = 'mqtt_example.last_message' + + # Listen to a message on MQTT + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + hass.states.set(entity_id, payload) + + mqtt.subscribe(hass, topic, message_received) + + hass.states.set(entity_id, 'No messages') + + # Service to publish a message on MQTT + + def set_state_service(call): + """ Service to send a message. """ + mqtt.publish(hass, topic, call.data.get('new_state')) + + # Register our service with Home Assistant + hass.services.register(DOMAIN, 'set_state', set_state_service) + + # return boolean to indicate that initialization was successful + return True diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 09069924e6b..e69de29bb2d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -1,988 +0,0 @@ -""" -homeassistant -~~~~~~~~~~~~~ - -Home Assistant is a Home Automation framework for observing the state -of entities and react to changes. -""" - -import os -import time -import logging -import threading -import enum -import re -import functools as ft - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, - EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, - EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, - TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) -import homeassistant.util as util -import homeassistant.util.dt as date_util - -DOMAIN = "homeassistant" - -# How often time_changed event should fire -TIMER_INTERVAL = 1 # seconds - -# How long we wait for the result of a service call -SERVICE_CALL_LIMIT = 10 # seconds - -# Define number of MINIMUM worker threads. -# During bootstrap of HA (see bootstrap.from_config_dict()) worker threads -# will be added for each component that polls devices. -MIN_WORKER_THREAD = 2 - -# Pattern for validating entity IDs (format: .) -ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") - -_LOGGER = logging.getLogger(__name__) - - -class HomeAssistant(object): - """ Core class to route all communication to right components. """ - - def __init__(self): - self.pool = pool = create_worker_pool() - self.bus = EventBus(pool) - self.services = ServiceRegistry(self.bus, pool) - self.states = StateMachine(self.bus) - self.config = Config() - - @property - def components(self): - """ DEPRECATED 3/21/2015. Use hass.config.components """ - _LOGGER.warning( - 'hass.components is deprecated. Use hass.config.components') - return self.config.components - - @property - def local_api(self): - """ DEPRECATED 3/21/2015. Use hass.config.api """ - _LOGGER.warning( - 'hass.local_api is deprecated. Use hass.config.api') - return self.config.api - - @property - def config_dir(self): - """ DEPRECATED 3/18/2015. Use hass.config.config_dir """ - _LOGGER.warning( - 'hass.config_dir is deprecated. Use hass.config.config_dir') - return self.config.config_dir - - def get_config_path(self, path): - """ DEPRECATED 3/18/2015. Use hass.config.path """ - _LOGGER.warning( - 'hass.get_config_path is deprecated. Use hass.config.path') - return self.config.path(path) - - def start(self): - """ Start home assistant. """ - _LOGGER.info( - "Starting Home Assistant (%d threads)", self.pool.worker_count) - - Timer(self) - - self.bus.fire(EVENT_HOMEASSISTANT_START) - - def block_till_stopped(self): - """ Will register service homeassistant/stop and - will block until called. """ - request_shutdown = threading.Event() - - self.services.register(DOMAIN, SERVICE_HOMEASSISTANT_STOP, - lambda service: request_shutdown.set()) - - while not request_shutdown.isSet(): - try: - time.sleep(1) - - except KeyboardInterrupt: - break - - self.stop() - - def track_point_in_time(self, action, point_in_time): - """ - Adds a listener that fires once after a spefic point in time. - """ - utc_point_in_time = date_util.as_utc(point_in_time) - - @ft.wraps(action) - def utc_converter(utc_now): - """ Converts passed in UTC now to local now. """ - action(date_util.as_local(utc_now)) - - self.track_point_in_utc_time(utc_converter, utc_point_in_time) - - def track_point_in_utc_time(self, action, point_in_time): - """ - Adds a listener that fires once after a specific point in UTC time. - """ - - @ft.wraps(action) - def point_in_time_listener(event): - """ Listens for matching time_changed events. """ - now = event.data[ATTR_NOW] - - if now >= point_in_time and \ - not hasattr(point_in_time_listener, 'run'): - - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. This will make - # sure the second time it does nothing. - point_in_time_listener.run = True - - self.bus.remove_listener(EVENT_TIME_CHANGED, - point_in_time_listener) - - action(now) - - self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) - return point_in_time_listener - - # pylint: disable=too-many-arguments - def track_utc_time_change(self, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None): - """ Adds a listener that will fire if time matches a pattern. """ - self.track_time_change( - action, year, month, day, hour, minute, second, utc=True) - - # pylint: disable=too-many-arguments - def track_time_change(self, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None, utc=False): - """ Adds a listener that will fire if UTC time matches a pattern. """ - - # We do not have to wrap the function with time pattern matching logic - # if no pattern given - if any((val is not None for val in - (year, month, day, hour, minute, second))): - - pmp = _process_match_param - year, month, day = pmp(year), pmp(month), pmp(day) - hour, minute, second = pmp(hour), pmp(minute), pmp(second) - - @ft.wraps(action) - def time_listener(event): - """ Listens for matching time_changed events. """ - now = event.data[ATTR_NOW] - - if not utc: - now = date_util.as_local(now) - - mat = _matcher - - if mat(now.year, year) and \ - mat(now.month, month) and \ - mat(now.day, day) and \ - mat(now.hour, hour) and \ - mat(now.minute, minute) and \ - mat(now.second, second): - - action(now) - - else: - @ft.wraps(action) - def time_listener(event): - """ Fires every time event that comes in. """ - action(event.data[ATTR_NOW]) - - self.bus.listen(EVENT_TIME_CHANGED, time_listener) - return time_listener - - def stop(self): - """ Stops Home Assistant and shuts down all threads. """ - _LOGGER.info("Stopping") - - self.bus.fire(EVENT_HOMEASSISTANT_STOP) - - # Wait till all responses to homeassistant_stop are done - self.pool.block_till_done() - - self.pool.stop() - - def get_entity_ids(self, domain_filter=None): - """ - Returns known entity ids. - - THIS METHOD IS DEPRECATED. Use hass.states.entity_ids - """ - _LOGGER.warning( - "hass.get_entiy_ids is deprecated. Use hass.states.entity_ids") - - return self.states.entity_ids(domain_filter) - - def listen_once_event(self, event_type, listener): - """ Listen once for event of a specific type. - - To listen to all events specify the constant ``MATCH_ALL`` - as event_type. - - Note: at the moment it is impossible to remove a one time listener. - - THIS METHOD IS DEPRECATED. Please use hass.events.listen_once. - """ - _LOGGER.warning( - "hass.listen_once_event is deprecated. Use hass.bus.listen_once") - - self.bus.listen_once(event_type, listener) - - def track_state_change(self, entity_ids, action, - from_state=None, to_state=None): - """ - Track specific state changes. - entity_ids, from_state and to_state can be string or list. - Use list to match multiple. - - THIS METHOD IS DEPRECATED. Use hass.states.track_change - """ - _LOGGER.warning(( - "hass.track_state_change is deprecated. " - "Use hass.states.track_change")) - - self.states.track_change(entity_ids, action, from_state, to_state) - - def call_service(self, domain, service, service_data=None): - """ - Fires event to call specified service. - - THIS METHOD IS DEPRECATED. Use hass.services.call - """ - _LOGGER.warning(( - "hass.services.call is deprecated. " - "Use hass.services.call")) - - self.services.call(domain, service, service_data) - - -def _process_match_param(parameter): - """ Wraps parameter in a list if it is not one and returns it. """ - if parameter is None or parameter == MATCH_ALL: - return MATCH_ALL - elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): - return (parameter,) - else: - return tuple(parameter) - - -def _matcher(subject, pattern): - """ Returns True if subject matches the pattern. - - Pattern is either a list of allowed subjects or a `MATCH_ALL`. - """ - return MATCH_ALL == pattern or subject in pattern - - -class JobPriority(util.OrderedEnum): - """ Provides priorities for bus events. """ - # pylint: disable=no-init,too-few-public-methods - - EVENT_CALLBACK = 0 - EVENT_SERVICE = 1 - EVENT_STATE = 2 - EVENT_TIME = 3 - EVENT_DEFAULT = 4 - - @staticmethod - def from_event_type(event_type): - """ Returns a priority based on event type. """ - if event_type == EVENT_TIME_CHANGED: - return JobPriority.EVENT_TIME - elif event_type == EVENT_STATE_CHANGED: - return JobPriority.EVENT_STATE - elif event_type == EVENT_CALL_SERVICE: - return JobPriority.EVENT_SERVICE - elif event_type == EVENT_SERVICE_EXECUTED: - return JobPriority.EVENT_CALLBACK - else: - return JobPriority.EVENT_DEFAULT - - -def create_worker_pool(): - """ Creates a worker pool to be used. """ - - def job_handler(job): - """ Called whenever a job is available to do. """ - try: - func, arg = job - func(arg) - except Exception: # pylint: disable=broad-except - # Catch any exception our service/event_listener might throw - # We do not want to crash our ThreadPool - _LOGGER.exception("BusHandler:Exception doing job") - - def busy_callback(worker_count, current_jobs, pending_jobs_count): - """ Callback to be called when the pool queue gets too big. """ - - _LOGGER.warning( - "WorkerPool:All %d threads are busy and %d jobs pending", - worker_count, pending_jobs_count) - - for start, job in current_jobs: - _LOGGER.warning("WorkerPool:Current job from %s: %s", - date_util.datetime_to_local_str(start), job) - - return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback) - - -class EventOrigin(enum.Enum): - """ Distinguish between origin of event. """ - # pylint: disable=no-init,too-few-public-methods - - local = "LOCAL" - remote = "REMOTE" - - def __str__(self): - return self.value - - -# pylint: disable=too-few-public-methods -class Event(object): - """ Represents an event within the Bus. """ - - __slots__ = ['event_type', 'data', 'origin', 'time_fired'] - - def __init__(self, event_type, data=None, origin=EventOrigin.local, - time_fired=None): - self.event_type = event_type - self.data = data or {} - self.origin = origin - self.time_fired = util.strip_microseconds( - time_fired or date_util.utcnow()) - - def as_dict(self): - """ Returns a dict representation of this Event. """ - return { - 'event_type': self.event_type, - 'data': dict(self.data), - 'origin': str(self.origin), - 'time_fired': date_util.datetime_to_str(self.time_fired), - } - - def __repr__(self): - # pylint: disable=maybe-no-member - if self.data: - return "".format( - self.event_type, str(self.origin)[0], - util.repr_helper(self.data)) - else: - return "".format(self.event_type, - str(self.origin)[0]) - - def __eq__(self, other): - return (self.__class__ == other.__class__ and - self.event_type == other.event_type and - self.data == other.data and - self.origin == other.origin and - self.time_fired == other.time_fired) - - -class EventBus(object): - """ Class that allows different components to communicate via services - and events. - """ - - def __init__(self, pool=None): - self._listeners = {} - self._lock = threading.Lock() - self._pool = pool or create_worker_pool() - - @property - def listeners(self): - """ Dict with events that is being listened for and the number - of listeners. - """ - with self._lock: - return {key: len(self._listeners[key]) - for key in self._listeners} - - def fire(self, event_type, event_data=None, origin=EventOrigin.local): - """ Fire an event. """ - if not self._pool.running: - raise HomeAssistantError('Home Assistant has shut down.') - - with self._lock: - # Copy the list of the current listeners because some listeners - # remove themselves as a listener while being executed which - # causes the iterator to be confused. - get = self._listeners.get - listeners = get(MATCH_ALL, []) + get(event_type, []) - - event = Event(event_type, event_data, origin) - - if event_type != EVENT_TIME_CHANGED: - _LOGGER.info("Bus:Handling %s", event) - - if not listeners: - return - - job_priority = JobPriority.from_event_type(event_type) - - for func in listeners: - self._pool.add_job(job_priority, (func, event)) - - def listen(self, event_type, listener): - """ Listen for all events or events of a specific type. - - To listen to all events specify the constant ``MATCH_ALL`` - as event_type. - """ - with self._lock: - if event_type in self._listeners: - self._listeners[event_type].append(listener) - else: - self._listeners[event_type] = [listener] - - def listen_once(self, event_type, listener): - """ Listen once for event of a specific type. - - To listen to all events specify the constant ``MATCH_ALL`` - as event_type. - - Note: at the moment it is impossible to remove a one time listener. - """ - @ft.wraps(listener) - def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ - if not hasattr(onetime_listener, 'run'): - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. - # This will make sure the second time it does nothing. - onetime_listener.run = True - - self.remove_listener(event_type, onetime_listener) - - listener(event) - - self.listen(event_type, onetime_listener) - - def remove_listener(self, event_type, listener): - """ Removes a listener of a specific event_type. """ - with self._lock: - try: - self._listeners[event_type].remove(listener) - - # delete event_type list if empty - if not self._listeners[event_type]: - self._listeners.pop(event_type) - - except (KeyError, ValueError): - # KeyError is key event_type listener did not exist - # ValueError if listener did not exist within event_type - pass - - -class State(object): - """ - Object to represent a state within the state machine. - - entity_id: the entity that is represented. - state: the state of the entity - attributes: extra information on entity and state - last_changed: last time the state was changed, not the attributes. - last_updated: last time this object was updated. - """ - - __slots__ = ['entity_id', 'state', 'attributes', - 'last_changed', 'last_updated'] - - # pylint: disable=too-many-arguments - def __init__(self, entity_id, state, attributes=None, last_changed=None, - last_updated=None): - if not ENTITY_ID_PATTERN.match(entity_id): - raise InvalidEntityFormatError(( - "Invalid entity id encountered: {}. " - "Format should be .").format(entity_id)) - - self.entity_id = entity_id.lower() - self.state = state - self.attributes = attributes or {} - self.last_updated = date_util.strip_microseconds( - last_updated or date_util.utcnow()) - - # Strip microsecond from last_changed else we cannot guarantee - # state == State.from_dict(state.as_dict()) - # This behavior occurs because to_dict uses datetime_to_str - # which does not preserve microseconds - self.last_changed = date_util.strip_microseconds( - last_changed or self.last_updated) - - @property - def domain(self): - """ Returns domain of this state. """ - return util.split_entity_id(self.entity_id)[0] - - @property - def object_id(self): - """ Returns object_id of this state. """ - return util.split_entity_id(self.entity_id)[1] - - @property - def name(self): - """ Name to represent this state. """ - return ( - self.attributes.get(ATTR_FRIENDLY_NAME) or - self.object_id.replace('_', ' ')) - - def copy(self): - """ Creates a copy of itself. """ - return State(self.entity_id, self.state, - dict(self.attributes), self.last_changed) - - def as_dict(self): - """ Converts State to a dict to be used within JSON. - Ensures: state == State.from_dict(state.as_dict()) """ - - return {'entity_id': self.entity_id, - 'state': self.state, - 'attributes': self.attributes, - 'last_changed': date_util.datetime_to_str(self.last_changed), - 'last_updated': date_util.datetime_to_str(self.last_updated)} - - @classmethod - def from_dict(cls, json_dict): - """ Static method to create a state from a dict. - Ensures: state == State.from_json_dict(state.to_json_dict()) """ - - if not (json_dict and - 'entity_id' in json_dict and - 'state' in json_dict): - return None - - last_changed = json_dict.get('last_changed') - - if last_changed: - last_changed = date_util.str_to_datetime(last_changed) - - last_updated = json_dict.get('last_updated') - - if last_updated: - last_updated = date_util.str_to_datetime(last_updated) - - return cls(json_dict['entity_id'], json_dict['state'], - json_dict.get('attributes'), last_changed, last_updated) - - def __eq__(self, other): - return (self.__class__ == other.__class__ and - self.entity_id == other.entity_id and - self.state == other.state and - self.attributes == other.attributes) - - def __repr__(self): - attr = "; {}".format(util.repr_helper(self.attributes)) \ - if self.attributes else "" - - return "".format( - self.entity_id, self.state, attr, - date_util.datetime_to_local_str(self.last_changed)) - - -class StateMachine(object): - """ Helper class that tracks the state of different entities. """ - - def __init__(self, bus): - self._states = {} - self._bus = bus - self._lock = threading.Lock() - - def entity_ids(self, domain_filter=None): - """ List of entity ids that are being tracked. """ - if domain_filter is not None: - domain_filter = domain_filter.lower() - - return [state.entity_id for key, state - in self._states.items() - if util.split_entity_id(key)[0] == domain_filter] - else: - return list(self._states.keys()) - - def all(self): - """ Returns a list of all states. """ - return [state.copy() for state in self._states.values()] - - def get(self, entity_id): - """ Returns the state of the specified entity. """ - state = self._states.get(entity_id.lower()) - - # Make a copy so people won't mutate the state - return state.copy() if state else None - - def get_since(self, point_in_time): - """ - Returns all states that have been changed since point_in_time. - """ - point_in_time = date_util.strip_microseconds(point_in_time) - - with self._lock: - return [state for state in self._states.values() - if state.last_updated >= point_in_time] - - def is_state(self, entity_id, state): - """ Returns True if entity exists and is specified state. """ - entity_id = entity_id.lower() - - return (entity_id in self._states and - self._states[entity_id].state == state) - - def remove(self, entity_id): - """ Removes an entity from the state machine. - - Returns boolean to indicate if an entity was removed. """ - entity_id = entity_id.lower() - - with self._lock: - return self._states.pop(entity_id, None) is not None - - def set(self, entity_id, new_state, attributes=None): - """ Set the state of an entity, add entity if it does not exist. - - Attributes is an optional dict to specify attributes of this state. - - If you just update the attributes and not the state, last changed will - not be affected. - """ - entity_id = entity_id.lower() - new_state = str(new_state) - attributes = attributes or {} - - with self._lock: - old_state = self._states.get(entity_id) - - is_existing = old_state is not None - same_state = is_existing and old_state.state == new_state - same_attr = is_existing and old_state.attributes == attributes - - # If state did not exist or is different, set it - if not (same_state and same_attr): - last_changed = old_state.last_changed if same_state else None - - state = State(entity_id, new_state, attributes, last_changed) - self._states[entity_id] = state - - event_data = {'entity_id': entity_id, 'new_state': state} - - if old_state: - event_data['old_state'] = old_state - - self._bus.fire(EVENT_STATE_CHANGED, event_data) - - def track_change(self, entity_ids, action, from_state=None, to_state=None): - """ - Track specific state changes. - entity_ids, from_state and to_state can be string or list. - Use list to match multiple. - - Returns the listener that listens on the bus for EVENT_STATE_CHANGED. - Pass the return value into hass.bus.remove_listener to remove it. - """ - from_state = _process_match_param(from_state) - to_state = _process_match_param(to_state) - - # Ensure it is a lowercase list with entity ids we want to match on - if isinstance(entity_ids, str): - entity_ids = (entity_ids.lower(),) - else: - entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) - - @ft.wraps(action) - def state_listener(event): - """ The listener that listens for specific state changes. """ - if event.data['entity_id'] not in entity_ids: - return - - if 'old_state' in event.data: - old_state = event.data['old_state'].state - else: - old_state = None - - if _matcher(old_state, from_state) and \ - _matcher(event.data['new_state'].state, to_state): - - action(event.data['entity_id'], - event.data.get('old_state'), - event.data['new_state']) - - self._bus.listen(EVENT_STATE_CHANGED, state_listener) - - return state_listener - - -# pylint: disable=too-few-public-methods -class ServiceCall(object): - """ Represents a call to a service. """ - - __slots__ = ['domain', 'service', 'data'] - - def __init__(self, domain, service, data=None): - self.domain = domain - self.service = service - self.data = data or {} - - def __repr__(self): - if self.data: - return "".format( - self.domain, self.service, util.repr_helper(self.data)) - else: - return "".format(self.domain, self.service) - - -class ServiceRegistry(object): - """ Offers services over the eventbus. """ - - def __init__(self, bus, pool=None): - self._services = {} - self._lock = threading.Lock() - self._pool = pool or create_worker_pool() - self._bus = bus - self._cur_id = 0 - bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) - - @property - def services(self): - """ Dict with per domain a list of available services. """ - with self._lock: - return {domain: list(self._services[domain].keys()) - for domain in self._services} - - def has_service(self, domain, service): - """ Returns True if specified service exists. """ - return service in self._services.get(domain, []) - - def register(self, domain, service, service_func): - """ Register a service. """ - with self._lock: - if domain in self._services: - self._services[domain][service] = service_func - else: - self._services[domain] = {service: service_func} - - self._bus.fire( - EVENT_SERVICE_REGISTERED, - {ATTR_DOMAIN: domain, ATTR_SERVICE: service}) - - def call(self, domain, service, service_data=None, blocking=False): - """ - Calls specified service. - Specify blocking=True to wait till service is executed. - Waits a maximum of SERVICE_CALL_LIMIT. - - If blocking = True, will return boolean if service executed - succesfully within SERVICE_CALL_LIMIT. - - This method will fire an event to call the service. - This event will be picked up by this ServiceRegistry and any - other ServiceRegistry that is listening on the EventBus. - - Because the service is sent as an event you are not allowed to use - the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. - """ - call_id = self._generate_unique_id() - event_data = service_data or {} - event_data[ATTR_DOMAIN] = domain - event_data[ATTR_SERVICE] = service - event_data[ATTR_SERVICE_CALL_ID] = call_id - - if blocking: - executed_event = threading.Event() - - def service_executed(call): - """ - Called when a service is executed. - Will set the event if matches our service call. - """ - if call.data[ATTR_SERVICE_CALL_ID] == call_id: - executed_event.set() - - self._bus.remove_listener( - EVENT_SERVICE_EXECUTED, service_executed) - - self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) - - self._bus.fire(EVENT_CALL_SERVICE, event_data) - - if blocking: - # wait will return False if event not set after our limit has - # passed. If not set, clean up the listener - if not executed_event.wait(SERVICE_CALL_LIMIT): - self._bus.remove_listener( - EVENT_SERVICE_EXECUTED, service_executed) - - return False - - return True - - def _event_to_service_call(self, event): - """ Calls a service from an event. """ - service_data = dict(event.data) - domain = service_data.pop(ATTR_DOMAIN, None) - service = service_data.pop(ATTR_SERVICE, None) - - with self._lock: - if domain in self._services and service in self._services[domain]: - service_call = ServiceCall(domain, service, service_data) - - # Add a job to the pool that calls _execute_service - self._pool.add_job(JobPriority.EVENT_SERVICE, - (self._execute_service, - (self._services[domain][service], - service_call))) - - def _execute_service(self, service_and_call): - """ Executes a service and fires a SERVICE_EXECUTED event. """ - service, call = service_and_call - - service(call) - - self._bus.fire( - EVENT_SERVICE_EXECUTED, { - ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID] - }) - - def _generate_unique_id(self): - """ Generates a unique service call id. """ - self._cur_id += 1 - return "{}-{}".format(id(self), self._cur_id) - - -class Timer(threading.Thread): - """ Timer will sent out an event every TIMER_INTERVAL seconds. """ - - def __init__(self, hass, interval=None): - threading.Thread.__init__(self) - - self.daemon = True - self.hass = hass - self.interval = interval or TIMER_INTERVAL - self._stop_event = threading.Event() - - # We want to be able to fire every time a minute starts (seconds=0). - # We want this so other modules can use that to make sure they fire - # every minute. - assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!" - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, - lambda event: self.start()) - - def run(self): - """ Start the timer. """ - - self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: self._stop_event.set()) - - _LOGGER.info("Timer:starting") - - last_fired_on_second = -1 - - calc_now = date_util.utcnow - interval = self.interval - - while not self._stop_event.isSet(): - now = calc_now() - - # First check checks if we are not on a second matching the - # timer interval. Second check checks if we did not already fire - # this interval. - if now.second % interval or \ - now.second == last_fired_on_second: - - # Sleep till it is the next time that we have to fire an event. - # Aim for halfway through the second that fits TIMER_INTERVAL. - # If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds. - # This will yield the best results because time.sleep() is not - # 100% accurate because of non-realtime OS's - slp_seconds = interval - now.second % interval + \ - .5 - now.microsecond/1000000.0 - - time.sleep(slp_seconds) - - now = calc_now() - - last_fired_on_second = now.second - - # Event might have been set while sleeping - if not self._stop_event.isSet(): - try: - self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) - except HomeAssistantError: - # HA raises error if firing event after it has shut down - break - - -class Config(object): - """ Configuration settings for Home Assistant. """ - - # pylint: disable=too-many-instance-attributes - def __init__(self): - self.latitude = None - self.longitude = None - self.temperature_unit = None - self.location_name = None - self.time_zone = None - - # List of loaded components - self.components = [] - - # Remote.API object pointing at local API - self.api = None - - # Directory that holds the configuration - self.config_dir = os.path.join(os.getcwd(), 'config') - - def path(self, *path): - """ Returns path to the file within the config dir. """ - return os.path.join(self.config_dir, *path) - - def temperature(self, value, unit): - """ Converts temperature to user preferred unit if set. """ - if not (unit and self.temperature_unit and - unit != self.temperature_unit): - return value, unit - - try: - if unit == TEMP_CELCIUS: - # Convert C to F - return round(float(value) * 1.8 + 32.0, 1), TEMP_FAHRENHEIT - - # Convert F to C - return round((float(value)-32.0)/1.8, 1), TEMP_CELCIUS - - except ValueError: - # Could not convert value to float - return value, unit - - def as_dict(self): - """ Converts config to a dictionary. """ - time_zone = self.time_zone or date_util.UTC - - return { - 'latitude': self.latitude, - 'longitude': self.longitude, - 'temperature_unit': self.temperature_unit, - 'location_name': self.location_name, - 'time_zone': time_zone.zone, - 'components': self.components, - } - - -class HomeAssistantError(Exception): - """ General Home Assistant exception occured. """ - pass - - -class InvalidEntityFormatError(HomeAssistantError): - """ When an invalid formatted entity is encountered. """ - pass - - -class NoEntitySpecifiedError(HomeAssistantError): - """ When no entity is specified. """ - pass diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 2514b35587f..19e2cc2993b 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -4,120 +4,61 @@ from __future__ import print_function import sys import os import argparse -import subprocess -import importlib -DEPENDENCIES = ['requests>=2.0', 'pyyaml>=3.11', 'pytz>=2015.2'] -IS_VIRTUAL = (getattr(sys, 'base_prefix', sys.prefix) != sys.prefix or - hasattr(sys, 'real_prefix')) - - -def validate_python(): - """ Validate we're running the right Python version. """ - major, minor = sys.version_info[:2] - - if major < 3 or (major == 3 and minor < 4): - print("Home Assistant requires atleast Python 3.4") - sys.exit() - - -def ensure_pip(): - """ Validate pip is installed so we can install packages on demand. """ - if importlib.find_loader('pip') is None: - print("Your Python installation did not bundle 'pip'") - print("Home Assistant requires 'pip' to be installed.") - print("Please install pip: " - "https://pip.pypa.io/en/latest/installing.html") - sys.exit() - - -# Copy of homeassistant.util.package because we can't import yet -def install_package(package): - """Install a package on PyPi. Accepts pip compatible package strings. - Return boolean if install successfull.""" - args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] - if not IS_VIRTUAL: - args.append('--user') - try: - return 0 == subprocess.call(args) - except subprocess.SubprocessError: - return False - - -def validate_dependencies(): - """ Validate all dependencies that HA uses. """ - ensure_pip() - - print("Validating dependencies...") - import_fail = False - - for requirement in DEPENDENCIES: - if not install_package(requirement): - import_fail = True - print('Fatal Error: Unable to install dependency', requirement) - - if import_fail: - print(("Install dependencies by running: " - "python3 -m pip install -r requirements.txt")) - sys.exit() - - -def ensure_path_and_load_bootstrap(): - """ Ensure sys load path is correct and load Home Assistant bootstrap. """ - try: - from homeassistant import bootstrap - - except ImportError: - # This is to add support to load Home Assistant using - # `python3 homeassistant` instead of `python3 -m homeassistant` - - # Insert the parent directory of this file into the module search path - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - - from homeassistant import bootstrap - - return bootstrap - - -def validate_git_submodules(): - """ Validate the git submodules are cloned. """ - try: - # pylint: disable=no-name-in-module, unused-variable - from homeassistant.external.noop import WORKING # noqa - except ImportError: - print("Repository submodules have not been initialized") - print("Please run: git submodule update --init --recursive") - sys.exit() +from homeassistant import bootstrap +import homeassistant.config as config_util +from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START def ensure_config_path(config_dir): - """ Gets the path to the configuration file. - Creates one if it not exists. """ + """ Validates configuration directory. """ + + lib_dir = os.path.join(config_dir, 'lib') # Test if configuration directory exists if not os.path.isdir(config_dir): - print(('Fatal Error: Unable to find specified configuration ' - 'directory {} ').format(config_dir)) - sys.exit() + if config_dir != config_util.get_default_config_dir(): + print(('Fatal Error: Specified configuration directory does ' + 'not exist {} ').format(config_dir)) + sys.exit(1) - import homeassistant.config as config_util + try: + os.mkdir(config_dir) + except OSError: + print(('Fatal Error: Unable to create default configuration ' + 'directory {} ').format(config_dir)) + sys.exit(1) + # Test if library directory exists + if not os.path.isdir(lib_dir): + try: + os.mkdir(lib_dir) + except OSError: + print(('Fatal Error: Unable to create library ' + 'directory {} ').format(lib_dir)) + sys.exit(1) + + +def ensure_config_file(config_dir): + """ Ensure configuration file exists. """ config_path = config_util.ensure_config_exists(config_dir) if config_path is None: print('Error getting configuration path') - sys.exit() + sys.exit(1) return config_path def get_arguments(): """ Get parsed passed in arguments. """ - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + description="Home Assistant: Observe, Control, Automate.") + parser.add_argument('--version', action='version', version=__version__) parser.add_argument( '-c', '--config', metavar='path_to_config_dir', - default="config", + default=config_util.get_default_config_dir(), help="Directory that contains the Home Assistant configuration") parser.add_argument( '--demo-mode', @@ -133,34 +74,21 @@ def get_arguments(): def main(): """ Starts Home Assistant. """ - validate_python() - validate_dependencies() - - # Windows needs this to pick up new modules - importlib.invalidate_caches() - - bootstrap = ensure_path_and_load_bootstrap() - - validate_git_submodules() - args = get_arguments() config_dir = os.path.join(os.getcwd(), args.config) - config_path = ensure_config_path(config_dir) + ensure_config_path(config_dir) if args.demo_mode: - from homeassistant.components import frontend, demo - hass = bootstrap.from_config_dict({ - frontend.DOMAIN: {}, - demo.DOMAIN: {} - }) + 'frontend': {}, + 'demo': {} + }, config_dir=config_dir) else: - hass = bootstrap.from_config_file(config_path) + config_file = ensure_config_file(config_dir) + hass = bootstrap.from_config_file(config_file) if args.open_ui: - from homeassistant.const import EVENT_HOMEASSISTANT_START - def open_browser(event): """ Open the webinterface in a browser. """ if hass.config.api is not None: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 2da2f4fb7b5..03cb762d56e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -10,10 +10,11 @@ start by calling homeassistant.start_home_assistant(bus) """ import os +import sys import logging from collections import defaultdict -import homeassistant +import homeassistant.core as core import homeassistant.util.dt as date_util import homeassistant.util.package as pkg_util import homeassistant.util.location as loc_util @@ -61,14 +62,17 @@ def setup_component(hass, domain, config=None): return True -def _handle_requirements(component, name): +def _handle_requirements(hass, component, name): """ Installs requirements for component. """ - if hasattr(component, 'REQUIREMENTS'): - for req in component.REQUIREMENTS: - if not pkg_util.install_package(req): - _LOGGER.error('Not initializing %s because could not install ' - 'dependency %s', name, req) - return False + if not hasattr(component, 'REQUIREMENTS'): + return True + + for req in component.REQUIREMENTS: + if not pkg_util.install_package(req, target=hass.config.path('lib')): + _LOGGER.error('Not initializing %s because could not install ' + 'dependency %s', name, req) + return False + return True @@ -83,33 +87,30 @@ def _setup_component(hass, domain, config): _LOGGER.error( 'Not initializing %s because not all dependencies loaded: %s', domain, ", ".join(missing_deps)) - return False - if not _handle_requirements(component, domain): + if not _handle_requirements(hass, component, domain): return False try: - if component.setup(hass, config): - hass.config.components.append(component.DOMAIN) - - # Assumption: if a component does not depend on groups - # it communicates with devices - if group.DOMAIN not in component.DEPENDENCIES: - hass.pool.add_worker() - - hass.bus.fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) - - return True - - else: + if not component.setup(hass, config): _LOGGER.error('component %s failed to initialize', domain) - + return False except Exception: # pylint: disable=broad-except _LOGGER.exception('Error during setup of component %s', domain) + return False - return False + hass.config.components.append(component.DOMAIN) + + # Assumption: if a component does not depend on groups + # it communicates with devices + if group.DOMAIN not in component.DEPENDENCIES: + hass.pool.add_worker() + + hass.bus.fire( + EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) + + return True def prepare_setup_platform(hass, config, domain, platform_name): @@ -138,25 +139,35 @@ def prepare_setup_platform(hass, config, domain, platform_name): component) return None - if not _handle_requirements(platform, platform_path): + if not _handle_requirements(hass, platform, platform_path): return None return platform +def mount_local_lib_path(config_dir): + """ Add local library to Python Path """ + sys.path.insert(0, os.path.join(config_dir, 'lib')) + + # pylint: disable=too-many-branches, too-many-statements -def from_config_dict(config, hass=None): +def from_config_dict(config, hass=None, config_dir=None, enable_log=True): """ Tries to configure Home Assistant from a config dict. Dynamically loads required components and its dependencies. """ if hass is None: - hass = homeassistant.HomeAssistant() + hass = core.HomeAssistant() + if config_dir is not None: + config_dir = os.path.abspath(config_dir) + hass.config.config_dir = config_dir + mount_local_lib_path(config_dir) - process_ha_core_config(hass, config.get(homeassistant.DOMAIN, {})) + process_ha_core_config(hass, config.get(core.DOMAIN, {})) - enable_logging(hass) + if enable_log: + enable_logging(hass) _ensure_loader_prepared(hass) @@ -168,7 +179,7 @@ def from_config_dict(config, hass=None): # Filter out the repeating and common config section [homeassistant] components = (key for key in config.keys() - if ' ' not in key and key != homeassistant.DOMAIN) + if ' ' not in key and key != core.DOMAIN) if not core_components.setup(hass, config): _LOGGER.error('Home Assistant core failed to initialize. ' @@ -192,14 +203,18 @@ def from_config_file(config_path, hass=None): instantiates a new Home Assistant object if 'hass' is not given. """ if hass is None: - hass = homeassistant.HomeAssistant() + hass = core.HomeAssistant() # Set config dir to directory holding config file - hass.config.config_dir = os.path.abspath(os.path.dirname(config_path)) + config_dir = os.path.abspath(os.path.dirname(config_path)) + hass.config.config_dir = config_dir + mount_local_lib_path(config_dir) + + enable_logging(hass) config_dict = config_util.load_config_file(config_path) - return from_config_dict(config_dict, hass) + return from_config_dict(config_dict, hass, enable_log=False) def enable_logging(hass): @@ -222,7 +237,8 @@ def enable_logging(hass): } )) except ImportError: - _LOGGER.warn("Colorlog package not found, console coloring disabled") + _LOGGER.warning( + "Colorlog package not found, console coloring disabled") # Log errors to a file if we have write access to file or config dir err_log_path = hass.config.path('home-assistant.log') diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 0b757766bc0..e5e917c5250 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -17,7 +17,7 @@ Each component should publish services only under its own domain. import itertools as it import logging -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util as util from homeassistant.helpers import extract_entity_ids from homeassistant.loader import get_component diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b0d6faa2f49..108cc88741b 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -9,7 +9,7 @@ import logging import threading import json -import homeassistant as ha +import homeassistant.core as ha from homeassistant.helpers.state import TrackStates import homeassistant.remote as rem from homeassistant.const import ( diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 21bea96201b..8dcb158dea4 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,7 +6,7 @@ Allows to setup simple automation rules via the config file. """ import logging -from homeassistant.loader import get_component +from homeassistant.bootstrap import prepare_setup_platform from homeassistant.helpers import config_per_platform from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID @@ -25,9 +25,10 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Sets up automation. """ + success = False for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER): - platform = get_component('automation.{}'.format(p_type)) + platform = prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: _LOGGER.error("Unknown automation platform specified: %s", p_type) @@ -36,11 +37,12 @@ def setup(hass, config): if platform.register(hass, p_config, _get_action(hass, p_config)): _LOGGER.info( "Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, "")) + success = True else: _LOGGER.error( "Error setting up rule %s", p_config.get(CONF_ALIAS, "")) - return True + return success def _get_action(hass, config): @@ -56,13 +58,16 @@ def _get_action(hass, config): service_data = config.get(CONF_SERVICE_DATA, {}) if not isinstance(service_data, dict): - _LOGGER.error( - "%s should be a serialized JSON object", CONF_SERVICE_DATA) + _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) service_data = {} if CONF_SERVICE_ENTITY_ID in config: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID].split(",") + try: + service_data[ATTR_ENTITY_ID] = \ + config[CONF_SERVICE_ENTITY_ID].split(",") + except AttributeError: + service_data[ATTR_ENTITY_ID] = \ + config[CONF_SERVICE_ENTITY_ID] hass.services.call(domain, service, service_data) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py new file mode 100644 index 00000000000..6b4e6b1e039 --- /dev/null +++ b/homeassistant/components/automation/mqtt.py @@ -0,0 +1,34 @@ +""" +homeassistant.components.automation.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Offers MQTT listening automation rules. +""" +import logging + +import homeassistant.components.mqtt as mqtt + +DEPENDENCIES = ['mqtt'] + +CONF_TOPIC = 'mqtt_topic' +CONF_PAYLOAD = 'mqtt_payload' + + +def register(hass, config, action): + """ Listen for state changes based on `config`. """ + topic = config.get(CONF_TOPIC) + payload = config.get(CONF_PAYLOAD) + + if topic is None: + logging.getLogger(__name__).error( + "Missing configuration key %s", CONF_TOPIC) + return False + + def mqtt_automation_listener(msg_topic, msg_payload, qos): + """ Listens for MQTT messages. """ + if payload is None or payload == msg_payload: + action() + + mqtt.subscribe(hass, topic, mqtt_automation_listener) + + return True diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index c8adfe95bbe..ba96debf9ac 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,6 +6,7 @@ Offers state listening automation rules. """ import logging +from homeassistant.helpers.event import track_state_change from homeassistant.const import MATCH_ALL @@ -30,7 +31,7 @@ def register(hass, config, action): """ Listens for state changes and calls action. """ action() - hass.states.track_change( - entity_id, state_automation_listener, from_state, to_state) + track_state_change( + hass, entity_id, state_automation_listener, from_state, to_state) return True diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 7e38960534d..77bd40a7a41 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -5,6 +5,7 @@ homeassistant.components.automation.time Offers time listening automation rules. """ from homeassistant.util import convert +from homeassistant.helpers.event import track_time_change CONF_HOURS = "time_hours" CONF_MINUTES = "time_minutes" @@ -21,8 +22,7 @@ def register(hass, config, action): """ Listens for time changes and calls action. """ action() - hass.track_time_change( - time_automation_listener, - hour=hours, minute=minutes, second=seconds) + track_time_change(hass, time_automation_listener, + hour=hours, minute=minutes, second=seconds) return True diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index bf78e13a094..2d439a7ac4a 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -8,7 +8,7 @@ This is more a proof of concept. import logging import re -import homeassistant +from homeassistant import core from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) @@ -52,16 +52,14 @@ def setup(hass, config): return if command == 'on': - hass.services.call( - homeassistant.DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity_ids, - }, blocking=True) + hass.services.call(core.DOMAIN, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity_ids, + }, blocking=True) elif command == 'off': - hass.services.call( - homeassistant.DOMAIN, SERVICE_TURN_OFF, { - ATTR_ENTITY_ID: entity_ids, - }, blocking=True) + hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, { + ATTR_ENTITY_ID: entity_ids, + }, blocking=True) else: logger.error( diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index d0b8b155b4a..71621502878 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -6,15 +6,15 @@ Sets up a demo environment that mimics interaction with devices. """ import time -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.loader as loader from homeassistant.const import ( - CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID) + CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) DOMAIN = "demo" -DEPENDENCIES = [] +DEPENDENCIES = ['introduction', 'conversation'] COMPONENTS_WITH_DEMO_PLATFORM = [ 'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify'] @@ -48,8 +48,11 @@ def setup(hass, config): # Setup room groups lights = hass.states.entity_ids('light') switches = hass.states.entity_ids('switch') - group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0]]) - group.setup_group(hass, 'bedroom', [lights[2], switches[1]]) + media_players = sorted(hass.states.entity_ids('media_player')) + group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0], + media_players[1]]) + group.setup_group(hass, 'bedroom', [lights[2], switches[1], + media_players[0]]) # Setup IP Camera bootstrap.setup_component( @@ -102,10 +105,10 @@ def setup(hass, config): # Setup fake device tracker hass.states.set("device_tracker.paulus", "home", {ATTR_ENTITY_PICTURE: - "http://graph.facebook.com/297400035/picture"}) + "http://graph.facebook.com/297400035/picture", + ATTR_FRIENDLY_NAME: 'Paulus'}) hass.states.set("device_tracker.anne_therese", "not_home", - {ATTR_ENTITY_PICTURE: - "http://graph.facebook.com/621994601/picture"}) + {ATTR_FRIENDLY_NAME: 'Anne Therese'}) hass.states.set("group.all_devices", "home", { diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index c53fff0e4f3..67da9e26a82 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -8,6 +8,7 @@ the state of the sun and devices. import logging from datetime import timedelta +from homeassistant.helpers.event import track_point_in_time, track_state_change import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME from . import light, sun, device_tracker, group @@ -91,14 +92,14 @@ def setup(hass, config): if start_point: for index, light_id in enumerate(light_ids): - hass.track_point_in_time(turn_on(light_id), - (start_point + - index * LIGHT_TRANSITION_TIME)) + track_point_in_time( + hass, turn_on(light_id), + (start_point + index * LIGHT_TRANSITION_TIME)) # Track every time sun rises so we can schedule a time-based # pre-sun set event - hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise, - sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + track_state_change(hass, sun.ENTITY_ID, schedule_light_on_sun_rise, + sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) # If the sun is already above horizon # schedule the time-based pre-sun set event @@ -157,13 +158,13 @@ def setup(hass, config): light.turn_off(hass, light_ids) # Track home coming of each device - hass.states.track_change( - device_entity_ids, check_light_on_dev_state_change, + track_state_change( + hass, device_entity_ids, check_light_on_dev_state_change, STATE_NOT_HOME, STATE_HOME) # Track when all devices are gone to shut down lights - hass.states.track_change( - device_group, check_light_on_dev_state_change, + track_state_change( + hass, device_group, check_light_on_dev_state_change, STATE_HOME, STATE_NOT_HOME) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 611136aac5b..099c23973f0 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -12,9 +12,11 @@ from datetime import timedelta from homeassistant.loader import get_component from homeassistant.helpers import validate_config +from homeassistant.helpers.entity import _OVERWRITE import homeassistant.util as util import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, CONF_PLATFORM, DEVICE_DEFAULT_NAME) @@ -65,14 +67,15 @@ def setup(hass, config): 'device_tracker.{}'.format(tracker_type)) if tracker_implementation is None: - _LOGGER.error("Unknown device_tracker type specified.") + _LOGGER.error("Unknown device_tracker type specified: %s.", + tracker_type) return False device_scanner = tracker_implementation.get_scanner(hass, config) if device_scanner is None: - _LOGGER.error("Failed to initialize device scanner for %s", + _LOGGER.error("Failed to initialize device scanner: %s", tracker_type) return False @@ -134,7 +137,7 @@ class DeviceTracker(object): seconds = range(0, 60, seconds) _LOGGER.info("Device tracker interval second=%s", seconds) - hass.track_utc_time_change(update_device_state, second=seconds) + track_utc_time_change(hass, update_device_state, second=seconds) hass.services.register(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, @@ -160,9 +163,12 @@ class DeviceTracker(object): state = STATE_HOME if is_home else STATE_NOT_HOME + # overwrite properties that have been set in the config file + attr = dict(dev_info['state_attr']) + attr.update(_OVERWRITE.get(dev_info['entity_id'], {})) + self.hass.states.set( - dev_info['entity_id'], state, - dev_info['state_attr']) + dev_info['entity_id'], state, attr) def update_devices(self, now): """ Update device states based on the found devices. """ diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py new file mode 100644 index 00000000000..06956475ba0 --- /dev/null +++ b/homeassistant/components/device_tracker/actiontec.py @@ -0,0 +1,149 @@ +""" +homeassistant.components.device_tracker.actiontec +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning an Actiontec MI424WR +(Verizon FIOS) router for device presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the Actiontec tracker you will need to add something like the +following to your config/configuration.yaml + +device_tracker: + platform: actiontec + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +_LEASES_REGEX = re.compile( + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + + r'\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a DD-WRT scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ActiontecDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ActiontecDeviceScanner(object): + """ This class queries a an actiontec router + for connected devices. Adapted from DD-WRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_actiontec_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['ip'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the Actiontec MI424WR router is up + to date. Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + # _LOGGER.info("Checking ARP") + data = self.get_actiontec_data() + if not data: + return False + active_clients = [client for client in data.values()] + self.last_results = active_clients + return True + + def get_actiontec_data(self): + """ Retrieve data from Actiontec MI424WR and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'Username: ') + telnet.write((self.username + '\n').encode('ascii')) + telnet.read_until(b'Password: ') + telnet.write((self.password + '\n').encode('ascii')) + prompt = telnet.read_until( + b'Wireless Broadband Router> ').split(b'\n')[-1] + telnet.write('firewall mac_cache_dump\n'.encode('ascii')) + telnet.write('\n'.encode('ascii')) + telnet.read_until(prompt) + leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] + telnet.write('exit\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return None + + devices = {} + for lease in leases_result: + match = _LEASES_REGEX.search(lease.decode('utf-8')) + if match is not None: + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper() + } + return devices diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py new file mode 100644 index 00000000000..fdf2ca70eaa --- /dev/null +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -0,0 +1,167 @@ +""" +homeassistant.components.device_tracker.asuswrt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a ASUSWRT router for device +presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the ASUSWRT tracker you will need to add something like the following +to your config/configuration.yaml + +device_tracker: + platform: asuswrt + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +_LEASES_REGEX = re.compile( + r'\w+\s' + + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' + + r'(?P([^\s]+))') + +_IP_NEIGH_REGEX = re.compile( + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' + + r'\w+\s' + + r'\w+\s' + + r'(\w+\s(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' + + r'(?P(\w+))') + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a DD-WRT scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = AsusWrtDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class AsusWrtDeviceScanner(object): + """ This class queries a router running ASUSWRT firmware + for connected devices. Adapted from DD-WRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_asuswrt_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['host'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the ASUSWRT router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + data = self.get_asuswrt_data() + if not data: + return False + + active_clients = [client for client in data.values() if + client['status'] == 'REACHABLE' or + client['status'] == 'DELAY' or + client['status'] == 'STALE'] + self.last_results = active_clients + return True + + def get_asuswrt_data(self): + """ Retrieve data from ASUSWRT and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'login: ') + telnet.write((self.username + '\n').encode('ascii')) + telnet.read_until(b'Password: ') + telnet.write((self.password + '\n').encode('ascii')) + prompt_string = telnet.read_until(b'#').split(b'\n')[-1] + telnet.write('ip neigh\n'.encode('ascii')) + neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1] + telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii')) + leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1] + telnet.write('exit\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return + + devices = {} + for lease in leases_result: + match = _LEASES_REGEX.search(lease.decode('utf-8')) + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper(), + 'host': match.group('host'), + 'status': '' + } + + for neighbor in neighbors: + match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) + if match.group('ip') in devices: + devices[match.group('ip')]['status'] = match.group('status') + return devices diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 3fe11f99fe6..346fbb37d37 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -35,7 +35,6 @@ from datetime import timedelta import threading from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import validate_config from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN @@ -43,20 +42,21 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear>=0.1'] +REQUIREMENTS = ['pynetgear==0.3'] def get_scanner(hass, config): """ Validates config and returns a Netgear scanner. """ - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): + info = config[DOMAIN] + host = info.get(CONF_HOST) + username = info.get(CONF_USERNAME) + password = info.get(CONF_PASSWORD) + + if password is not None and host is None: + _LOGGER.warning('Found username or password but no host') return None - info = config[DOMAIN] - - scanner = NetgearDeviceScanner( - info[CONF_HOST], info[CONF_USERNAME], info[CONF_PASSWORD]) + scanner = NetgearDeviceScanner(host, username, password) return scanner if scanner.success_init else None @@ -68,16 +68,24 @@ class NetgearDeviceScanner(object): import pynetgear self.last_results = [] - - self._api = pynetgear.Netgear(host, username, password) self.lock = threading.Lock() + if host is None: + print("BIER") + self._api = pynetgear.Netgear() + elif username is None: + self._api = pynetgear.Netgear(password, host) + else: + self._api = pynetgear.Netgear(password, host, username) + _LOGGER.info("Logging in") - self.success_init = self._api.login() + results = self._api.get_attached_devices() + + self.success_init = results is not None if self.success_init: - self._update_info() + self.last_results = results else: _LOGGER.error("Failed to Login") diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index e3af9ad5c44..ee1650594ee 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -26,8 +26,12 @@ from collections import namedtuple import subprocess import re -from libnmap.process import NmapProcess -from libnmap.parser import NmapParser, NmapParserException +try: + from libnmap.process import NmapProcess + from libnmap.parser import NmapParser, NmapParserException + LIB_LOADED = True +except ImportError: + LIB_LOADED = False import homeassistant.util.dt as dt_util from homeassistant.const import CONF_HOSTS @@ -43,6 +47,8 @@ _LOGGER = logging.getLogger(__name__) # interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" +REQUIREMENTS = ['python-libnmap==0.6.1'] + def get_scanner(hass, config): """ Validates config and returns a Nmap scanner. """ @@ -50,6 +56,10 @@ def get_scanner(hass, config): _LOGGER): return None + if not LIB_LOADED: + _LOGGER.error("Error while importing dependency python-libnmap.") + return False + scanner = NmapDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py new file mode 100644 index 00000000000..ffe1a7f64c2 --- /dev/null +++ b/homeassistant/components/device_tracker/thomson.py @@ -0,0 +1,157 @@ +""" +homeassistant.components.device_tracker.thomson +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a THOMSON router for device +presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the THOMSON tracker you will need to add something like the following +to your config/configuration.yaml + +device_tracker: + platform: thomson + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + +_DEVICES_REGEX = re.compile( + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))') + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a THOMSON scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ThomsonDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ThomsonDeviceScanner(object): + """ This class queries a router running THOMSON firmware + for connected devices. Adapted from ASUSWRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_thomson_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device + or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['host'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the THOMSON router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + data = self.get_thomson_data() + if not data: + return False + + # flag C stands for CONNECTED + active_clients = [client for client in data.values() if + client['status'].find('C') != -1] + self.last_results = active_clients + return True + + def get_thomson_data(self): + """ Retrieve data from THOMSON and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'Username : ') + telnet.write((self.username + '\r\n').encode('ascii')) + telnet.read_until(b'Password : ') + telnet.write((self.password + '\r\n').encode('ascii')) + telnet.read_until(b'=>') + telnet.write(('hostmgr list\r\n').encode('ascii')) + devices_result = telnet.read_until(b'=>').split(b'\r\n') + telnet.write('exit\r\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return + + devices = {} + for device in devices_result: + match = _DEVICES_REGEX.search(device.decode('utf-8')) + if match: + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper(), + 'host': match.group('host'), + 'status': match.group('status') + } + return devices diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 24d170a5de7..8e556e47e8a 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -31,6 +31,7 @@ password The password for your given admin account. """ +import base64 import logging from datetime import timedelta import re @@ -55,7 +56,10 @@ def get_scanner(hass, config): _LOGGER): return None - scanner = TplinkDeviceScanner(config[DOMAIN]) + scanner = Tplink2DeviceScanner(config[DOMAIN]) + + if not scanner.success_init: + scanner = TplinkDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -115,3 +119,63 @@ class TplinkDeviceScanner(object): return True return False + + +class Tplink2DeviceScanner(TplinkDeviceScanner): + """ This class queries a wireless router running newer version of TP-Link + firmware for connected devices. + """ + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return self.last_results.keys() + + # pylint: disable=no-self-use + def get_device_name(self, device): + """ The TP-Link firmware doesn't save the name of the wireless + device. """ + + return self.last_results.get(device) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the TP-Link router is up to date. + Returns boolean if scanning successful. """ + + with self.lock: + _LOGGER.info("Loading wireless clients...") + + url = 'http://{}/data/map_access_wireless_client_grid.json'\ + .format(self.host) + referer = 'http://{}'.format(self.host) + + # Router uses Authorization cookie instead of header + # Let's create the cookie + username_password = '{}:{}'.format(self.username, self.password) + b64_encoded_username_password = base64.b64encode( + username_password.encode('ascii') + ).decode('ascii') + cookie = 'Authorization=Basic {}'\ + .format(b64_encoded_username_password) + + response = requests.post(url, headers={'referer': referer, + 'cookie': cookie}) + + try: + result = response.json().get('data') + except ValueError: + _LOGGER.error("Router didn't respond with JSON. " + "Check if credentials are correct.") + return False + + if result: + self.last_results = { + device['mac_addr'].replace('-', ':'): device['name'] + for device in result + } + return True + + return False diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 0aa7312bfd7..c21249fbc60 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -19,7 +19,7 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['netdisco>=0.1'] +REQUIREMENTS = ['netdisco==0.3'] SCAN_INTERVAL = 300 # seconds @@ -28,11 +28,13 @@ SCAN_INTERVAL = 300 # seconds SERVICE_WEMO = 'belkin_wemo' SERVICE_HUE = 'philips_hue' SERVICE_CAST = 'google_cast' +SERVICE_NETGEAR = 'netgear_router' SERVICE_HANDLERS = { SERVICE_WEMO: "switch", SERVICE_CAST: "media_player", SERVICE_HUE: "light", + SERVICE_NETGEAR: 'device_tracker', } @@ -77,6 +79,13 @@ def setup(hass, config): if not component: return + # Hack - fix when device_tracker supports discovery + if service == SERVICE_NETGEAR: + bootstrap.setup_component(hass, component, { + 'device_tracker': {'platform': 'netgear'} + }) + return + # This component cannot be setup. if not bootstrap.setup_component(hass, component, config): return diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index f84c8653b31..556fe4e67c8 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -5,24 +5,46 @@ Home Assistant - - - - - - + + + - - -

Initializing Home Assistant

- - - + + + +
+ +

Initializing

+
+ + + diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 6f30746f137..f4a1ad184e7 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "ccfe7497d635ab4df3e6943b05adbd9b" +VERSION = "e9060d58fc9034468cfefa9794026d0c" diff --git a/homeassistant/components/frontend/www_static/__init__.py b/homeassistant/components/frontend/www_static/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index c8cabd1b0a3..e1b3b84dfc3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,6 @@ - \ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 5a3fcc970b3..a97750b5dd8 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 5a3fcc970b30d640e6a370b6f20904a745f69659 +Subproject commit a97750b5dd887af42030e01bfe50bc3c60183514 diff --git a/homeassistant/components/frontend/www_static/images/__init__.py b/homeassistant/components/frontend/www_static/images/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/frontend/www_static/splash.png b/homeassistant/components/frontend/www_static/splash.png new file mode 100644 index 00000000000..582140a2bc3 Binary files /dev/null and b/homeassistant/components/frontend/www_static/splash.png differ diff --git a/homeassistant/components/frontend/www_static/version.py b/homeassistant/components/frontend/www_static/version.py deleted file mode 100644 index 0f9641f054b..00000000000 --- a/homeassistant/components/frontend/www_static/version.py +++ /dev/null @@ -1,2 +0,0 @@ -""" DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "" diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js index 84acb55dd45..ec6063b7a58 100644 --- a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js +++ b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js @@ -7,7 +7,6 @@ * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ -// @version 0.7.6 -window.WebComponents=window.WebComponents||{},function(e){var t=e.flags||{},n="webcomponents-lite.js",r=document.querySelector('script[src*="'+n+'"]');if(!t.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var n,r=e.split("=");r[0]&&(n=r[0].match(/wc-(.+)/))&&(t[n[1]]=r[1]||!0)}),r)for(var o,i=0;o=r.attributes[i];i++)"src"!==o.name&&(t[o.name]=o.value||!0);if(t.log){var a=t.log.split(",");t.log={},a.forEach(function(e){t.log[e]=!0})}else t.log={}}t.shadow=t.shadow||t.shadowdom||t.polyfill,t.shadow="native"===t.shadow?!1:t.shadow||!HTMLElement.prototype.createShadowRoot,t.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=t.register),e.flags=t}(window.WebComponents),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&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",u=0,l="",_=!1,w=!1,g=[];e:for(;(e[u-1]!=f||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(f==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",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"):f!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(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[u+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),f==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[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||f!=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){_&&(c("@ already seen."),l+="%40"),_=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[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(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){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();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(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 r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=p(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=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;np&&(h=s[p]);p++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>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?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),p=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},f=p(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return p(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(f,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),_=v?"complete":"interactive",w="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>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,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=f.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),f.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=f,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),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,r,o){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=i.getResponseHeader("Location"),a=null;if(n)var a="/"===n.substr(0,1)?location.origin+n:n;r.call(o,!t.ok(i)&&i,i.response||i.responseText,a)}}),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,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>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,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),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,r=e.length;r>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,r=e.length;r>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===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".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,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"link[rel=stylesheet]","style","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(window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.dispatchEvent(e.__resource&&!e.__error?new CustomEvent("load",{bubbles:!1}):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,r=function(r){t&&t(r),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!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&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},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 r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r["import"],r):r: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["import"]?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),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 r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(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(o),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,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){p.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);p.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,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n["import"]=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},p=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var f={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",f),Object.defineProperty(c,"baseURI",f)}e.importer=h,e.importLoader=p}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=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,r=e.isIE;if(!e.useNative){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.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),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=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),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 r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function o(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,r=t.length;r>n&&(e=t[n]);n++)e();L=[]}function a(e){y?o(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?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(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){w.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function p(e,n){if(w.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(M(e.addedNodes,function(e){e.localName&&t(e,a)}),M(e.removedNodes,function(e){e.localName&&c(e)}))}),w.dom&&console.groupEnd()}function f(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(p(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(p.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),w.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),w.dom&&console.groupEnd()}function _(e){b(e,v)}var w=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=!window.MutationObserver||window.MutationObserver===window.JsMutationObserver;e.hasPolyfillMutations=y;var E=!1,L=[],M=Array.prototype.forEach.call.bind(Array.prototype.forEach),T=Element.prototype.createShadowRoot;T&&(Element.prototype.createShadowRoot=function(){var e=T.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=_,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=f}),window.CustomElements.addModule(function(e){function t(t,r){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(o||t.localName);if(i){if(o&&i.tag==t.localName)return n(t,i,r);if(!o&&!i["extends"])return n(t,i,r)}}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};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(o(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.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&_(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0; - -}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function f(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=e.isIE11OrOlder,_=e.upgradeDocumentTree,w=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},M="http://www.w3.org/1999/xhtml",T=document.createElement.bind(document),O=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},f(Node.prototype,"cloneNode"),f(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=p,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){a(window.wrap(document)),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){a(window.wrap(e["import"]))}),window.CustomElements.ready=!0,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,r=e.initializeModules,o=/Trident/.test(navigator.userAgent);if(n){var i=function(){};e.watchShadow=i,e.upgrade=i,e.upgradeAll=i,e.upgradeDocumentTree=i,e.upgradeSubtree=i,e.takeRecords=i,e["instanceof"]=function(e,t){return e instanceof t}}else r();var a=e.upgradeDocumentTree;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),o&&"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.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),"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();e.isIE11OrOlder=o}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){var e="template";HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=e.ownerDocument.createDocumentFragment());for(var t;t=e.firstChild;)e.content.appendChild(t)},HTMLTemplateElement.bootstrap=function(t){for(var n,r=t.querySelectorAll(e),o=0,i=r.length;i>o&&(n=r[o]);o++)HTMLTemplateElement.decorate(n)},window.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var t=document.createElement;document.createElement=function(){"use strict";var e=t.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e}}(),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.12 +window.WebComponents=window.WebComponents||{},function(e){var t=e.flags||{},n="webcomponents-lite.js",r=document.querySelector('script[src*="'+n+'"]');if(!t.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var n,r=e.split("=");r[0]&&(n=r[0].match(/wc-(.+)/))&&(t[n[1]]=r[1]||!0)}),r)for(var o,i=0;o=r.attributes[i];i++)"src"!==o.name&&(t[o.name]=o.value||!0);if(t.log){var a=t.log.split(",");t.log={},a.forEach(function(e){t.log[e]=!0})}else t.log={}}t.shadow=t.shadow||t.shadowdom||t.polyfill,t.shadow="native"===t.shadow?!1:t.shadow||!HTMLElement.prototype.createShadowRoot,t.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=t.register),e.flags=t}(window.WebComponents),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&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",u=0,l="",_=!1,w=!1,g=[];e:for(;(e[u-1]!=f||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(f==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",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"):f!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(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[u+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),f==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[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||f!=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){_&&(c("@ already seen."),l+="%40"),_=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[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(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){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();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(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 r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=p(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=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;np&&(h=s[p]);p++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>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?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),p=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},f=p(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return p(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(f,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),_=v?"complete":"interactive",w="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>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,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=f.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),f.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=f,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),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,r,o){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=i.getResponseHeader("Location"),a=null;if(n)var a="/"===n.substr(0,1)?location.origin+n:n;r.call(o,!t.ok(i)&&i,i.response||i.responseText,a)}}),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,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>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,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),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,r=e.length;r>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,r=e.length;r>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===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".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,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"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.dispatchEvent(e.__resource&&!e.__error?new CustomEvent("load",{bubbles:!1}):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,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!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&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},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 r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r: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?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),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 r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(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(o),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,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){p.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);p.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,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},p=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var f={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",f),Object.defineProperty(c,"baseURI",f)}e.importer=h,e.importLoader=p}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=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,r=e.isIE;if(!e.useNative){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.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),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=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,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 r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function o(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,r=t.length;r>n&&(e=t[n]);n++)e();L=[]}function a(e){y?o(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?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(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){w.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function p(e,n){if(w.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(M(e.addedNodes,function(e){e.localName&&t(e,a)}),M(e.removedNodes,function(e){e.localName&&c(e)}))}),w.dom&&console.groupEnd()}function f(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(p(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(p.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),w.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),w.dom&&console.groupEnd()}function _(e){b(e,v)}var w=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=!window.MutationObserver||window.MutationObserver===window.JsMutationObserver;e.hasPolyfillMutations=y;var E=!1,L=[],M=Array.prototype.forEach.call.bind(Array.prototype.forEach),T=Element.prototype.createShadowRoot;T&&(Element.prototype.createShadowRoot=function(){var e=T.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=_,e.upgradeDocument=v,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=f}),window.CustomElements.addModule(function(e){function t(t,r){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};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(o(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.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&_(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){ +var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function f(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,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},M="http://www.w3.org/1999/xhtml",T=document.createElement.bind(document),N=document.createElementNS.bind(document);m=Object.__proto__||y?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},f(Node.prototype,"cloneNode"),f(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=p,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){a(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,r=e.initializeModules,o=e.isIE;if(n){var i=function(){};e.watchShadow=i,e.upgrade=i,e.upgradeAll=i,e.upgradeDocumentTree=i,e.upgradeSubtree=i,e.takeRecords=i,e["instanceof"]=function(e,t){return e instanceof t}}else r();var a=e.upgradeDocumentTree,s=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"]&&s(wrap(e["import"]))}),o&&"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.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var c=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(c,t)}else t()}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){function e(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case" ":return" "}}function t(t){return t.replace(a,e)}var n="template",r=document.implementation.createHTMLDocument("template"),o=!0;HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=r.createDocumentFragment());for(var n;n=e.firstChild;)e.content.appendChild(n);if(o)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(r.body.innerHTML=e,HTMLTemplateElement.bootstrap(r);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;r.body.firstChild;)this.content.appendChild(r.body.firstChild)},configurable:!0})}catch(i){o=!1}},HTMLTemplateElement.bootstrap=function(e){for(var t,r=e.querySelectorAll(n),o=0,i=r.length;i>o&&(t=r[o]);o++)HTMLTemplateElement.decorate(t)},window.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var i=document.createElement;document.createElement=function(){"use strict";var e=i.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e};var a=/[&\u00A0<>]/g}(),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/group.py b/homeassistant/components/group.py index 20931f2b363..1d307baaca9 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -5,8 +5,9 @@ homeassistant.components.group Provides functionality to group devices that can be turned on or off. """ -import homeassistant as ha +import homeassistant.core as ha from homeassistant.helpers import generate_entity_id +from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import Entity import homeassistant.util as util from homeassistant.const import ( @@ -102,10 +103,8 @@ def get_entity_ids(hass, entity_id, domain_filter=None): def setup(hass, config): """ Sets up all groups found definded in the configuration. """ for name, entity_ids in config.get(DOMAIN, {}).items(): - # Support old deprecated method - 2/28/2015 if isinstance(entity_ids, str): entity_ids = entity_ids.split(",") - setup_group(hass, name, entity_ids) return True @@ -162,8 +161,8 @@ class Group(Entity): def start(self): """ Starts the tracking. """ - self.hass.states.track_change( - self.tracking, self._state_changed_listener) + track_state_change( + self.hass, self.tracking, self._state_changed_listener) def stop(self): """ Unregisters the group from Home Assistant. """ diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index c1c0899c9ef..a28def8e7ba 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -86,7 +86,7 @@ from http import cookies from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import ( SERVER_PORT, CONTENT_TYPE_JSON, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, @@ -119,7 +119,6 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config=None): """ Sets up the HTTP API and debug interface. """ - if config is None or DOMAIN not in config: config = {DOMAIN: {}} @@ -139,9 +138,14 @@ def setup(hass, config=None): sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True) - server = HomeAssistantHTTPServer( - (server_host, server_port), RequestHandler, hass, api_password, - development, no_password_set, sessions_enabled) + try: + server = HomeAssistantHTTPServer( + (server_host, server_port), RequestHandler, hass, api_password, + development, no_password_set, sessions_enabled) + except OSError: + # Happens if address already in use + _LOGGER.exception("Error setting up HTTP server") + return False hass.bus.listen_once( ha.EVENT_HOMEASSISTANT_START, @@ -183,10 +187,12 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): _LOGGER.info("running http in development mode") def start(self): - """ Starts the server. """ - self.hass.bus.listen_once( - ha.EVENT_HOMEASSISTANT_STOP, - lambda event: self.shutdown()) + """ Starts the HTTP server. """ + def stop_http(event): + """ Stops the HTTP server. """ + self.shutdown() + + self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http) _LOGGER.info( "Starting web interface at http://%s:%d", *self.server_address) @@ -199,7 +205,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): self.serve_forever() def register_path(self, method, url, callback, require_auth=True): - """ Regitsters a path wit the server. """ + """ Registers a path wit the server. """ self.paths.append((method, url, callback, require_auth)) diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py new file mode 100644 index 00000000000..b84a02d5fa5 --- /dev/null +++ b/homeassistant/components/introduction.py @@ -0,0 +1,41 @@ +""" +homeassistant.components.introduction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Component that will help guide the user taking its first steps. +""" +import logging + +DOMAIN = 'introduction' +DEPENDENCIES = [] + + +def setup(hass, config=None): + """ Setup the introduction component. """ + log = logging.getLogger(__name__) + log.info(""" + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Hello, and welcome to Home Assistant! + + We'll hope that we can make all your dreams come true. + + Here are some resources to get started: + + - Configuring Home Assistant: + https://home-assistant.io/getting-started/configuration.html + + - Available components: + https://home-assistant.io/components/ + + - Chat room: + https://gitter.im/balloob/home-assistant + + This message is generated by the introduction component. You can + disable it in configuration.yaml. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + """) + + return True diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index dad3cd70534..63c7b6c4af6 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -1,15 +1,15 @@ """ homeassistant.components.isy994 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Connects to an ISY-994 controller and loads relevant components to control its devices. Also contains the base classes for ISY Sensors, Lights, and Switches. + +For configuration details please visit the documentation for this component at +https://home-assistant.io/components/isy994.html """ -# system imports import logging from urllib.parse import urlparse -# homeassistant imports from homeassistant import bootstrap from homeassistant.loader import get_component from homeassistant.helpers import validate_config @@ -19,10 +19,9 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) -# homeassistant constants DOMAIN = "isy994" DEPENDENCIES = [] -REQUIREMENTS = ['PyISY>=1.0.5'] +REQUIREMENTS = ['PyISY==1.0.5'] DISCOVER_LIGHTS = "isy994.lights" DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" @@ -31,7 +30,6 @@ SENSOR_STRING = 'Sensor' HIDDEN_STRING = '{HIDE ME}' CONF_TLS_VER = 'tls' -# setup logger _LOGGER = logging.getLogger(__name__) @@ -158,6 +156,12 @@ class ISYDeviceABC(ToggleEntity): attr = {ATTR_FRIENDLY_NAME: self.name} for name, prop in self._attrs.items(): attr[name] = getattr(self, prop) + attr = self._attr_filter(attr) + return attr + + def _attr_filter(self, attr): + """ Placeholder for attribute filters. """ + # pylint: disable=no-self-use return attr @property diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 5359791087e..3629fce31bf 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -14,7 +14,7 @@ from homeassistant.const import ( DOMAIN = "keyboard" DEPENDENCIES = [] -REQUIREMENTS = ['pyuserinput>=0.1.9'] +REQUIREMENTS = ['pyuserinput==0.1.9'] def volume_up(hass): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 35e80cc143d..d2f8033add7 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,6 +1,6 @@ """ homeassistant.components.light -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to interact with lights. @@ -166,26 +166,25 @@ def setup(hass, config): profiles = {} for profile_path in profile_paths: + if not os.path.isfile(profile_path): + continue + with open(profile_path) as inp: + reader = csv.reader(inp) - if os.path.isfile(profile_path): - with open(profile_path) as inp: - reader = csv.reader(inp) + # Skip the header + next(reader, None) - # Skip the header - next(reader, None) + try: + for profile_id, color_x, color_y, brightness in reader: + profiles[profile_id] = (float(color_x), float(color_y), + int(brightness)) + except ValueError: + # ValueError if not 4 values per row + # ValueError if convert to float/int failed + _LOGGER.error( + "Error parsing light profiles from %s", profile_path) - try: - for profile_id, color_x, color_y, brightness in reader: - profiles[profile_id] = (float(color_x), float(color_y), - int(brightness)) - - except ValueError: - # ValueError if not 4 values per row - # ValueError if convert to float/int failed - _LOGGER.error( - "Error parsing light profiles from %s", profile_path) - - return False + return False def handle_light_service(service): """ Hande a turn light on or off service call. """ @@ -206,66 +205,70 @@ def setup(hass, config): for light in target_lights: light.turn_off(**params) - else: - # Processing extra data for turn light on request - - # We process the profile first so that we get the desired - # behavior that extra service data attributes overwrite - # profile values - profile = profiles.get(dat.get(ATTR_PROFILE)) - - if profile: - *params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile - - if ATTR_BRIGHTNESS in dat: - # We pass in the old value as the default parameter if parsing - # of the new one goes wrong. - params[ATTR_BRIGHTNESS] = util.convert( - dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS)) - - if ATTR_XY_COLOR in dat: - try: - # xy_color should be a list containing 2 floats - xycolor = dat.get(ATTR_XY_COLOR) - - # Without this check, a xycolor with value '99' would work - if not isinstance(xycolor, str): - params[ATTR_XY_COLOR] = [float(val) for val in xycolor] - - except (TypeError, ValueError): - # TypeError if xy_color is not iterable - # ValueError if value could not be converted to float - pass - - if ATTR_RGB_COLOR in dat: - try: - # rgb_color should be a list containing 3 ints - rgb_color = dat.get(ATTR_RGB_COLOR) - - if len(rgb_color) == 3: - params[ATTR_XY_COLOR] = \ - color_util.color_RGB_to_xy(int(rgb_color[0]), - int(rgb_color[1]), - int(rgb_color[2])) - - except (TypeError, ValueError): - # TypeError if rgb_color is not iterable - # ValueError if not all values can be converted to int - pass - - if ATTR_FLASH in dat: - if dat[ATTR_FLASH] == FLASH_SHORT: - params[ATTR_FLASH] = FLASH_SHORT - - elif dat[ATTR_FLASH] == FLASH_LONG: - params[ATTR_FLASH] = FLASH_LONG - - if ATTR_EFFECT in dat: - if dat[ATTR_EFFECT] == EFFECT_COLORLOOP: - params[ATTR_EFFECT] = EFFECT_COLORLOOP - for light in target_lights: - light.turn_on(**params) + if light.should_poll: + light.update_ha_state(True) + return + + # Processing extra data for turn light on request + + # We process the profile first so that we get the desired + # behavior that extra service data attributes overwrite + # profile values + profile = profiles.get(dat.get(ATTR_PROFILE)) + + if profile: + *params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile + + if ATTR_BRIGHTNESS in dat: + # We pass in the old value as the default parameter if parsing + # of the new one goes wrong. + params[ATTR_BRIGHTNESS] = util.convert( + dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS)) + + if ATTR_XY_COLOR in dat: + try: + # xy_color should be a list containing 2 floats + xycolor = dat.get(ATTR_XY_COLOR) + + # Without this check, a xycolor with value '99' would work + if not isinstance(xycolor, str): + params[ATTR_XY_COLOR] = [float(val) for val in xycolor] + + except (TypeError, ValueError): + # TypeError if xy_color is not iterable + # ValueError if value could not be converted to float + pass + + if ATTR_RGB_COLOR in dat: + try: + # rgb_color should be a list containing 3 ints + rgb_color = dat.get(ATTR_RGB_COLOR) + + if len(rgb_color) == 3: + params[ATTR_XY_COLOR] = \ + color_util.color_RGB_to_xy(int(rgb_color[0]), + int(rgb_color[1]), + int(rgb_color[2])) + + except (TypeError, ValueError): + # TypeError if rgb_color is not iterable + # ValueError if not all values can be converted to int + pass + + if ATTR_FLASH in dat: + if dat[ATTR_FLASH] == FLASH_SHORT: + params[ATTR_FLASH] = FLASH_SHORT + + elif dat[ATTR_FLASH] == FLASH_LONG: + params[ATTR_FLASH] = FLASH_LONG + + if ATTR_EFFECT in dat: + if dat[ATTR_EFFECT] == EFFECT_COLORLOOP: + params[ATTR_EFFECT] = EFFECT_COLORLOOP + + for light in target_lights: + light.turn_on(**params) for light in target_lights: if light.should_poll: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index c908992eb82..b438d7b92b1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -1,4 +1,8 @@ -""" Support for Hue lights. """ +""" +homeassistant.components.light.hue +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Hue lights. +""" import logging import socket from datetime import timedelta @@ -12,7 +16,7 @@ from homeassistant.components.light import ( ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT, EFFECT_COLORLOOP) -REQUIREMENTS = ['phue>=0.8'] +REQUIREMENTS = ['phue==0.8'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -35,7 +39,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return if discovery_info is not None: - host = urlparse(discovery_info).hostname + host = urlparse(discovery_info[1]).hostname else: host = config.get(CONF_HOST, None) diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index ae0225a1e3c..5b62120ee98 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -1,8 +1,10 @@ -""" Support for ISY994 lights. """ -# system imports +""" +homeassistant.components.light.isy994 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for ISY994 lights. +""" import logging -# homeassistant imports from homeassistant.components.isy994 import (ISYDeviceABC, ISY, SENSOR_STRING, HIDDEN_STRING) from homeassistant.components.light import ATTR_BRIGHTNESS @@ -10,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the isy994 platform. """ + """ Sets up the ISY994 platform. """ logger = logging.getLogger(__name__) devs = [] # verify connection @@ -29,10 +31,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ISYLightDevice(ISYDeviceABC): - """ represents as isy light within home assistant. """ + """ Represents as ISY light. """ _domain = 'light' _dtype = 'analog' _attrs = {ATTR_BRIGHTNESS: 'value'} _onattrs = [ATTR_BRIGHTNESS] _states = [STATE_ON, STATE_OFF] + + def _attr_filter(self, attr): + """ Filter brightness out of entity while off. """ + if ATTR_BRIGHTNESS in attr and not self.is_on: + del attr[ATTR_BRIGHTNESS] + return attr diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index b515bb1cbac..8fdb525d4e0 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -1,16 +1,21 @@ """ homeassistant.components.light.limitlessled -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for LimitlessLED bulbs, also known as... -EasyBulb -AppLight -AppLamp -MiLight -LEDme -dekolight -iLight +- EasyBulb +- AppLight +- AppLamp +- MiLight +- LEDme +- dekolight +- iLight + +Configuration: + +To use limitlessled you will need to add the following to your +config/configuration.yaml. light: platform: limitlessled @@ -24,10 +29,12 @@ light: import logging from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.components.light import Light, ATTR_BRIGHTNESS +from homeassistant.components.light import (Light, ATTR_BRIGHTNESS, + ATTR_XY_COLOR) +from homeassistant.util.color import color_RGB_to_xy _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['ledcontroller>=1.0.7'] +REQUIREMENTS = ['ledcontroller==1.0.7'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -57,6 +64,29 @@ class LimitlessLED(Light): self._name = name or DEVICE_DEFAULT_NAME self._state = False self._brightness = 100 + self._xy_color = color_RGB_to_xy(255, 255, 255) + + # Build a color table that maps an RGB color to a color string + # recognized by LedController's set_color method + self._color_table = [(color_RGB_to_xy(*x[0]), x[1]) for x in [ + ((0xFF, 0xFF, 0xFF), 'white'), + ((0xEE, 0x82, 0xEE), 'violet'), + ((0x41, 0x69, 0xE1), 'royal_blue'), + ((0x87, 0xCE, 0xFA), 'baby_blue'), + ((0x00, 0xFF, 0xFF), 'aqua'), + ((0x7F, 0xFF, 0xD4), 'royal_mint'), + ((0x2E, 0x8B, 0x57), 'seafoam_green'), + ((0x00, 0x80, 0x00), 'green'), + ((0x32, 0xCD, 0x32), 'lime_green'), + ((0xFF, 0xFF, 0x00), 'yellow'), + ((0xDA, 0xA5, 0x20), 'yellow_orange'), + ((0xFF, 0xA5, 0x00), 'orange'), + ((0xFF, 0x00, 0x00), 'red'), + ((0xFF, 0xC0, 0xCB), 'pink'), + ((0xFF, 0x00, 0xFF), 'fusia'), + ((0xDA, 0x70, 0xD6), 'lilac'), + ((0xE6, 0xE6, 0xFA), 'lavendar'), + ]] @property def should_poll(self): @@ -72,6 +102,22 @@ class LimitlessLED(Light): def brightness(self): return self._brightness + @property + def color_xy(self): + return self._xy_color + + def _xy_to_led_color(self, xy_color): + """ Convert an XY color to the closest LedController color string """ + def abs_dist_squared(p_0, p_1): + """ Returns the absolute value of the squared distance """ + return abs((p_0[0] - p_1[0])**2 + (p_0[1] - p_1[1])**2) + + candidates = [(abs_dist_squared(xy_color, x[0]), x[1]) for x in + self._color_table] + + # First candidate in the sorted list is closest to desired color: + return sorted(candidates)[0][1] + @property def is_on(self): """ True if device is on. """ @@ -84,7 +130,11 @@ class LimitlessLED(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - self.led.set_brightness(self._brightness, self.group) + if ATTR_XY_COLOR in kwargs: + self._xy_color = kwargs[ATTR_XY_COLOR] + + self.led.set_color(self._xy_to_led_color(self._xy_color), self.group) + self.led.set_brightness(self._brightness / 255.0, self.group) self.update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index d3c35ae0640..8068d20bb74 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -1,13 +1,19 @@ -""" Support for Tellstick lights. """ +""" +homeassistant.components.light.tellstick +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Tellstick lights. +""" import logging # pylint: disable=no-name-in-module, import-error from homeassistant.components.light import Light, ATTR_BRIGHTNESS from homeassistant.const import ATTR_FRIENDLY_NAME import tellcore.constants as tellcore_constants +REQUIREMENTS = ['tellcore-py==1.0.4'] + def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """ Find and return tellstick lights. """ + """ Find and return Tellstick lights. """ try: import tellcore.telldus as telldus @@ -27,7 +33,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TellstickLight(Light): - """ Represents a tellstick light """ + """ Represents a Tellstick light. """ last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | tellcore_constants.TELLSTICK_TURNOFF | tellcore_constants.TELLSTICK_DIM | diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index fe363923c76..f25c110cc46 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -1,13 +1,15 @@ """ -Support for Vera lights. +homeassistant.components.light.vera +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support for Vera lights. This component is useful if you wish for switches +connected to your Vera controller to appear as lights in Home Assistant. +All switches will be added as a light unless you exclude them in the config. Configuration: -This component is useful if you wish for switches connected to your Vera -controller to appear as lights in homeassistant. All switches will be added -as a light unless you exclude them in the config. To use the Vera lights you will need to add something like the following to -your config/configuration.yaml +your config/configuration.yaml. light: platform: vera @@ -19,22 +21,19 @@ light: 13: name: Another switch -VARIABLES: +Variables: vera_controller_url *Required This is the base URL of your vera controller including the port number if not -running on 80 -Example: http://192.168.1.21:3480/ - +running on 80. Example: http://192.168.1.21:3480/ device_data *Optional -This contains an array additional device info for your Vera devices. It is not +This contains an array additional device info for your Vera devices. It is not required and if not specified all lights configured in your Vera controller -will be added with default values. You should use the id of your vera device -as the key for the device within device_data - +will be added with default values. You should use the id of your vera device +as the key for the device within device_data. These are the variables for the device_data array: @@ -42,13 +41,12 @@ name *Optional This parameter allows you to override the name of your Vera device in the HA interface, if not specified the value configured for the device in your Vera -will be used - +will be used. exclude *Optional -This parameter allows you to exclude the specified device from homeassistant, -it should be set to "true" if you want this device excluded +This parameter allows you to exclude the specified device from Home Assistant, +it should be set to "true" if you want this device excluded. """ import logging diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index c4a47ca7da1..4b5af0c3250 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -1,10 +1,17 @@ -""" Support for Wink lights. """ +""" +homeassistant.components.light.wink +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Wink lights. +""" import logging from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] + def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Wink lights. """ @@ -26,7 +33,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class WinkLight(WinkToggleDevice): - """ Represents a Wink light """ + """ Represents a Wink light. """ # pylint: disable=too-few-public-methods def turn_on(self, **kwargs): diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index acc9a36e494..c7a403f12ec 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -8,7 +8,7 @@ from datetime import timedelta from itertools import groupby import re -from homeassistant import State, DOMAIN as HA_DOMAIN +from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8cd3353bea8..29bcb731062 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -98,9 +98,7 @@ ATTR_TO_PROPERTY = [ def is_on(hass, entity_id=None): """ Returns true if specified media player entity_id is on. Will check all media player if no entity_id specified. """ - entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) - return any(not hass.states.is_state(entity_id, STATE_OFF) for entity_id in entity_ids) @@ -108,28 +106,24 @@ def is_on(hass, entity_id=None): def turn_on(hass, entity_id=None): """ Will turn on specified media player or all. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None): """ Will turn off specified media player or all. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) def volume_up(hass, entity_id=None): """ Send the media player the command for volume up. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) def volume_down(hass, entity_id=None): """ Send the media player the command for volume down. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) @@ -156,35 +150,30 @@ def set_volume_level(hass, volume, entity_id=None): def media_play_pause(hass, entity_id=None): """ Send the media player the command for play/pause. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) def media_play(hass, entity_id=None): """ Send the media player the command for play/pause. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) def media_pause(hass, entity_id=None): """ Send the media player the command for play/pause. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) def media_next_track(hass, entity_id=None): """ Send the media player the command for next track. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) def media_previous_track(hass, entity_id=None): """ Send the media player the command for prev track. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) @@ -262,29 +251,30 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service) - def play_youtube_video_service(service, media_id): + def play_youtube_video_service(service, media_id=None): """ Plays specified media_id on the media player. """ - target_players = component.extract_from_service(service) + if media_id is None: + service.data.get('video') - if media_id: - for player in target_players: - player.play_youtube(media_id) + if media_id is None: + return - if player.should_poll: - player.update_ha_state(True) + for player in component.extract_from_service(service): + player.play_youtube(media_id) - hass.services.register(DOMAIN, "start_fireplace", - lambda service: - play_youtube_video_service(service, "eyU3bRy2x44")) + if player.should_poll: + player.update_ha_state(True) - hass.services.register(DOMAIN, "start_epic_sax", - lambda service: - play_youtube_video_service(service, "kxopViU98Xo")) + hass.services.register( + DOMAIN, "start_fireplace", + lambda service: play_youtube_video_service(service, "eyU3bRy2x44")) - hass.services.register(DOMAIN, SERVICE_YOUTUBE_VIDEO, - lambda service: - play_youtube_video_service( - service, service.data.get('video'))) + hass.services.register( + DOMAIN, "start_epic_sax", + lambda service: play_youtube_video_service(service, "kxopViU98Xo")) + + hass.services.register( + DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service) return True diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 17ae9e6cd33..a576898de59 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -8,14 +8,9 @@ WARNING: This platform is currently not working due to a changed Cast API """ import logging -try: - import pychromecast -except ImportError: - pychromecast = None - from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF, - STATE_UNKNOWN) + STATE_UNKNOWN, CONF_HOST) from homeassistant.components.media_player import ( MediaPlayerDevice, @@ -24,7 +19,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) -REQUIREMENTS = ['pychromecast>=0.6.9'] +REQUIREMENTS = ['pychromecast==0.6.12'] CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -32,21 +27,23 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE KNOWN_HOSTS = [] +# pylint: disable=invalid-name +cast = None + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the cast platform. """ - global pychromecast # pylint: disable=invalid-name - if pychromecast is None: - import pychromecast as pychromecast_ - pychromecast = pychromecast_ + global cast + import pychromecast + cast = pychromecast logger = logging.getLogger(__name__) # import CEC IGNORE attributes ignore_cec = config.get(CONF_IGNORE_CEC, []) if isinstance(ignore_cec, list): - pychromecast.IGNORE_CEC += ignore_cec + cast.IGNORE_CEC += ignore_cec else: logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC) @@ -55,9 +52,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info and discovery_info[0] not in KNOWN_HOSTS: hosts = [discovery_info[0]] + elif CONF_HOST in config: + hosts = [config[CONF_HOST]] + else: hosts = (host_port[0] for host_port - in pychromecast.discover_chromecasts() + in cast.discover_chromecasts() if host_port[0] not in KNOWN_HOSTS) casts = [] @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for host in hosts: try: casts.append(CastDevice(host)) - except pychromecast.ChromecastConnectionError: + except cast.ChromecastConnectionError: pass else: KNOWN_HOSTS.append(host) @@ -80,7 +80,7 @@ class CastDevice(MediaPlayerDevice): def __init__(self, host): import pychromecast.controllers.youtube as youtube - self.cast = pychromecast.Chromecast(host) + self.cast = cast.Chromecast(host) self.youtube = youtube.YouTubeController() self.cast.register_handler(self.youtube) @@ -226,7 +226,7 @@ class CastDevice(MediaPlayerDevice): self.cast.quit_app() self.cast.play_media( - CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + CAST_SPLASH, cast.STREAM_TYPE_BUFFERED) def turn_off(self): """ Turns Chromecast off. """ diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index e134c1c2f7e..dfc2f64a2a8 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -1,12 +1,12 @@ """ homeassistant.components.media_player.kodi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides an interface to the XBMC/Kodi JSON-RPC API Configuration: -To use Kodi add something like this to your configuration: +To use the Kodi you will need to add something like the following to +your config/configuration.yaml. media_player: platform: kodi @@ -19,7 +19,7 @@ Variables: name *Optional -The name of the device +The name of the device. url *Required @@ -27,13 +27,12 @@ The URL of the XBMC/Kodi JSON-RPC API. Example: http://192.168.0.123/jsonrpc user *Optional -The XBMC/Kodi HTTP username +The XBMC/Kodi HTTP username. password *Optional -The XBMC/Kodi HTTP password +The XBMC/Kodi HTTP password. """ - import urllib import logging @@ -49,7 +48,7 @@ except ImportError: jsonrpc_requests = None _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['jsonrpc-requests>=0.1'] +REQUIREMENTS = ['jsonrpc-requests==0.1'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 0239173f7cc..aca2413d3e4 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -48,7 +48,7 @@ from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-mpd2>=0.5.4'] +REQUIREMENTS = ['python-mpd2==0.5.4'] SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py new file mode 100644 index 00000000000..8cbc087c50c --- /dev/null +++ b/homeassistant/components/media_player/squeezebox.py @@ -0,0 +1,319 @@ +""" +homeassistant.components.media_player.squeezebox +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides an interface to the Logitech SqueezeBox API + +Configuration: + +To use SqueezeBox add something like this to your configuration: + +media_player: + platform: squeezebox + host: 192.168.1.21 + port: 9090 + username: user + password: password + +Variables: + +host +*Required +The host name or address of the Logitech Media Server + +port +*Optional +Telnet port to Logitech Media Server, default 9090 + +usermame +*Optional +Username, if password protection is enabled + +password +*Optional +Password, if password protection is enabled +""" + +import logging +import telnetlib +import urllib.parse + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + MEDIA_TYPE_MUSIC, DOMAIN) + +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the squeezebox platform. """ + if not config.get(CONF_HOST): + _LOGGER.error( + "Missing required configuration items in %s: %s", + DOMAIN, + CONF_HOST) + return False + + lms = LogitechMediaServer( + config.get(CONF_HOST), + config.get('port', '9090'), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) + + if not lms.init_success: + return False + + add_devices(lms.create_players()) + + return True + + +class LogitechMediaServer(object): + """ Represents a Logitech media server. """ + + def __init__(self, host, port, username, password): + self.host = host + self.port = port + self._username = username + self._password = password + self.http_port = self._get_http_port() + self.init_success = True if self.http_port else False + + def _get_http_port(self): + """ Get http port from media server, it is used to get cover art """ + http_port = None + try: + http_port = self.query('pref', 'httpport', '?') + if not http_port: + _LOGGER.error( + "Unable to read data from server %s:%s", + self.host, + self.port) + return + return http_port + except ConnectionError as ex: + _LOGGER.error( + "Failed to connect to server %s:%s - %s", + self.host, + self.port, + ex) + return + + def create_players(self): + """ Create a list of SqueezeBoxDevices connected to the LMS """ + players = [] + count = self.query('player', 'count', '?') + for index in range(0, int(count)): + player_id = self.query('player', 'id', str(index), '?') + player = SqueezeBoxDevice(self, player_id) + players.append(player) + return players + + def query(self, *parameters): + """ Send request and await response from server """ + telnet = telnetlib.Telnet(self.host, self.port) + if self._username and self._password: + telnet.write('login {username} {password}\n'.format( + username=self._username, + password=self._password).encode('UTF-8')) + telnet.read_until(b'\n', timeout=3) + message = '{}\n'.format(' '.join(parameters)) + telnet.write(message.encode('UTF-8')) + response = telnet.read_until(b'\n', timeout=3)\ + .decode('UTF-8')\ + .split(' ')[-1]\ + .strip() + telnet.write(b'exit\n') + return urllib.parse.unquote(response) + + def get_player_status(self, player): + """ Get ithe status of a player """ + # (title) : Song title + # Requested Information + # a (artist): Artist name 'artist' + # d (duration): Song duration in seconds 'duration' + # K (artwork_url): URL to remote artwork + tags = 'adK' + new_status = {} + telnet = telnetlib.Telnet(self.host, self.port) + telnet.write('{player} status - 1 tags:{tags}\n'.format( + player=player, + tags=tags + ).encode('UTF-8')) + response = telnet.read_until(b'\n', timeout=3)\ + .decode('UTF-8')\ + .split(' ') + telnet.write(b'exit\n') + for item in response: + parts = urllib.parse.unquote(item).partition(':') + new_status[parts[0]] = parts[2] + return new_status + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods +class SqueezeBoxDevice(MediaPlayerDevice): + """ Represents a SqueezeBox device. """ + + # pylint: disable=too-many-arguments + def __init__(self, lms, player_id): + super(SqueezeBoxDevice, self).__init__() + self._lms = lms + self._id = player_id + self._name = self._lms.query(self._id, 'name', '?') + self._status = self._lms.get_player_status(self._id) + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if 'power' in self._status and self._status['power'] == '0': + return STATE_OFF + if 'mode' in self._status: + if self._status['mode'] == 'pause': + return STATE_PAUSED + if self._status['mode'] == 'play': + return STATE_PLAYING + if self._status['mode'] == 'stop': + return STATE_IDLE + return STATE_UNKNOWN + + def update(self): + """ Retrieve latest state. """ + self._status = self._lms.get_player_status(self._name) + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + if 'mixer volume' in self._status: + return int(self._status['mixer volume']) / 100.0 + + @property + def is_volume_muted(self): + if 'mixer volume' in self._status: + return self._status['mixer volume'].startswith('-') + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + if 'current_title' in self._status: + return self._status['current_title'] + + @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. """ + if 'duration' in self._status: + return int(float(self._status['duration'])) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + if 'artwork_url' in self._status: + return self._status['artwork_url'] + return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ + .format( + server=self._lms.host, + port=self._lms.http_port, + player=self._id) + + @property + def media_title(self): + """ Title of current playing media. """ + if 'artist' in self._status and 'title' in self._status: + return '{artist} - {title}'.format( + artist=self._status['artist'], + title=self._status['title'] + ) + if 'current_title' in self._status: + return self._status['current_title'] + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_SQUEEZEBOX + + def turn_off(self): + """ turn_off media player. """ + self._lms.query(self._id, 'power', '0') + self.update_ha_state() + + def volume_up(self): + """ volume_up media player. """ + self._lms.query(self._id, 'mixer', 'volume', '+5') + self.update_ha_state() + + def volume_down(self): + """ volume_down media player. """ + self._lms.query(self._id, 'mixer', 'volume', '-5') + self.update_ha_state() + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + volume_percent = str(int(volume*100)) + self._lms.query(self._id, 'mixer', 'volume', volume_percent) + self.update_ha_state() + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + mute_numeric = '1' if mute else '0' + self._lms.query(self._id, 'mixer', 'muting', mute_numeric) + self.update_ha_state() + + def media_play_pause(self): + """ media_play_pause media player. """ + self._lms.query(self._id, 'pause') + self.update_ha_state() + + def media_play(self): + """ media_play media player. """ + self._lms.query(self._id, 'play') + self.update_ha_state() + + def media_pause(self): + """ media_pause media player. """ + self._lms.query(self._id, 'pause', '0') + self.update_ha_state() + + def media_next_track(self): + """ Send next track command. """ + self._lms.query(self._id, 'playlist', 'index', '+1') + self.update_ha_state() + + def media_previous_track(self): + """ Send next track command. """ + self._lms.query(self._id, 'playlist', 'index', '-1') + self.update_ha_state() + + def media_seek(self, position): + """ Send seek command. """ + self._lms.query(self._id, 'time', position) + self.update_ha_state() + + def turn_on(self): + """ turn the media player on. """ + self._lms.query(self._id, 'power', '1') + self.update_ha_state() + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + raise NotImplementedError() diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 4f46915d870..e6c3f1cfcee 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -5,6 +5,11 @@ Modbus component, using pymodbus (python3 branch) Configuration: +To use the Forecast sensor you will need to add something like the +following to your config/configuration.yaml + +Configuration: + To use the Modbus component you will need to add something like the following to your config/configuration.yaml @@ -33,6 +38,8 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, DOMAIN = "modbus" DEPENDENCIES = [] +REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/' + + 'd7fc4f1cc975631e0a9011390e8017f64b612661.zip'] # Type of network MEDIUM = "type" diff --git a/homeassistant/components/mqtt.py b/homeassistant/components/mqtt.py new file mode 100644 index 00000000000..474b5ebb53e --- /dev/null +++ b/homeassistant/components/mqtt.py @@ -0,0 +1,256 @@ +""" +homeassistant.components.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +MQTT component, using paho-mqtt. This component needs a MQTT broker like +Mosquitto or Mosca. The Eclipse Foundation is running a public MQTT server +at iot.eclipse.org. If you prefer to use that one, keep in mind to adjust +the topic/client ID and that your messages are public. + +Configuration: + +To use MQTT you will need to add something like the following to your +config/configuration.yaml. + +mqtt: + broker: 127.0.0.1 + +Or, if you want more options: + +mqtt: + broker: 127.0.0.1 + port: 1883 + client_id: home-assistant-1 + keepalive: 60 + username: your_username + password: your_secret_password + +Variables: + +broker +*Required +This is the IP address of your MQTT broker, e.g. 192.168.1.32. + +port +*Optional +The network port to connect to. Default is 1883. + +client_id +*Optional +Client ID that Home Assistant will use. Has to be unique on the server. +Default is a random generated one. + +keepalive +*Optional +The keep alive in seconds for this client. Default is 60. +""" +import logging +import socket + +from homeassistant.exceptions import HomeAssistantError +import homeassistant.util as util +from homeassistant.helpers import validate_config +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "mqtt" + +MQTT_CLIENT = None + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_QOS = 0 + +SERVICE_PUBLISH = 'publish' +EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' + +DEPENDENCIES = [] +REQUIREMENTS = ['paho-mqtt==1.1'] + +CONF_BROKER = 'broker' +CONF_PORT = 'port' +CONF_CLIENT_ID = 'client_id' +CONF_KEEPALIVE = 'keepalive' +CONF_USERNAME = 'username' +CONF_PASSWORD = 'password' + +ATTR_QOS = 'qos' +ATTR_TOPIC = 'topic' +ATTR_PAYLOAD = 'payload' + + +def publish(hass, topic, payload): + """ Send an MQTT message. """ + data = { + ATTR_TOPIC: topic, + ATTR_PAYLOAD: payload, + } + hass.services.call(DOMAIN, SERVICE_PUBLISH, data) + + +def subscribe(hass, topic, callback, qos=0): + """ Subscribe to a topic. """ + def mqtt_topic_subscriber(event): + """ Match subscribed MQTT topic. """ + if _match_topic(topic, event.data[ATTR_TOPIC]): + callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD], + event.data[ATTR_QOS]) + + hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, mqtt_topic_subscriber) + + if topic not in MQTT_CLIENT.topics: + MQTT_CLIENT.subscribe(topic, qos) + + +def setup(hass, config): + """ Get the MQTT protocol service. """ + + if not validate_config(config, {DOMAIN: ['broker']}, _LOGGER): + return False + + conf = config[DOMAIN] + + broker = conf[CONF_BROKER] + port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) + client_id = util.convert(conf.get(CONF_CLIENT_ID), str) + keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE) + username = util.convert(conf.get(CONF_USERNAME), str) + password = util.convert(conf.get(CONF_PASSWORD), str) + + global MQTT_CLIENT + try: + MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username, + password) + except socket.error: + _LOGGER.exception("Can't connect to the broker. " + "Please check your settings and the broker " + "itself.") + return False + + def stop_mqtt(event): + """ Stop MQTT component. """ + MQTT_CLIENT.stop() + + def start_mqtt(event): + """ Launch MQTT component when Home Assistant starts up. """ + MQTT_CLIENT.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mqtt) + + def publish_service(call): + """ Handle MQTT publish service calls. """ + msg_topic = call.data.get(ATTR_TOPIC) + payload = call.data.get(ATTR_PAYLOAD) + if msg_topic is None or payload is None: + return + MQTT_CLIENT.publish(msg_topic, payload) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt) + + hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service) + + return True + + +# This is based on one of the paho-mqtt examples: +# http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.python.git/tree/examples/sub-class.py +# pylint: disable=too-many-arguments +class MQTT(object): # pragma: no cover + """ Implements messaging service for MQTT. """ + def __init__(self, hass, broker, port, client_id, keepalive, username, + password): + import paho.mqtt.client as mqtt + + self.hass = hass + self._progress = {} + self.topics = {} + + if client_id is None: + self._mqttc = mqtt.Client() + else: + self._mqttc = mqtt.Client(client_id) + if username is not None: + self._mqttc.username_pw_set(username, password) + self._mqttc.on_subscribe = self._mqtt_on_subscribe + self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe + self._mqttc.on_connect = self._mqtt_on_connect + self._mqttc.on_message = self._mqtt_on_message + self._mqttc.connect(broker, port, keepalive) + + def publish(self, topic, payload): + """ Publish a MQTT message. """ + self._mqttc.publish(topic, payload) + + def unsubscribe(self, topic): + """ Unsubscribe from topic. """ + result, mid = self._mqttc.unsubscribe(topic) + _raise_on_error(result) + self._progress[mid] = topic + + def start(self): + """ Run the MQTT client. """ + self._mqttc.loop_start() + + def stop(self): + """ Stop the MQTT client. """ + self._mqttc.loop_stop() + + def subscribe(self, topic, qos): + """ Subscribe to a topic. """ + if topic in self.topics: + return + result, mid = self._mqttc.subscribe(topic, qos) + _raise_on_error(result) + self._progress[mid] = topic + self.topics[topic] = None + + def _mqtt_on_connect(self, mqttc, obj, flags, result_code): + """ On connect, resubscribe to all topics we were subscribed to. """ + old_topics = self.topics + self._progress = {} + self.topics = {} + for topic, qos in old_topics.items(): + # qos is None if we were in process of subscribing + if qos is not None: + self._mqttc.subscribe(topic, qos) + + def _mqtt_on_subscribe(self, mqttc, obj, mid, granted_qos): + """ Called when subscribe succesfull. """ + topic = self._progress.pop(mid, None) + if topic is None: + return + self.topics[topic] = granted_qos + + def _mqtt_on_unsubscribe(self, mqttc, obj, mid, granted_qos): + """ Called when subscribe succesfull. """ + topic = self._progress.pop(mid, None) + if topic is None: + return + self.topics.pop(topic, None) + + def _mqtt_on_message(self, mqttc, obj, msg): + """ Message callback """ + self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { + ATTR_TOPIC: msg.topic, + ATTR_QOS: msg.qos, + ATTR_PAYLOAD: msg.payload.decode('utf-8'), + }) + + +def _raise_on_error(result): # pragma: no cover + """ Raise error if error result. """ + if result != 0: + raise HomeAssistantError('Error talking to MQTT: {}'.format(result)) + + +def _match_topic(subscription, topic): + """ Returns if topic matches subscription. """ + if subscription.endswith('#'): + return (subscription[:-2] == topic or + topic.startswith(subscription[:-1])) + + sub_parts = subscription.split('/') + topic_parts = topic.split('/') + + return (len(sub_parts) == len(topic_parts) and + all(a == b for a, b in zip(sub_parts, topic_parts) if a != '+')) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index ecd15aeb8e2..ee53159d5e6 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -4,12 +4,13 @@ homeassistant.components.notify Provides functionality to notify people. """ +from functools import partial import logging from homeassistant.loader import get_component -from homeassistant.helpers import validate_config +from homeassistant.helpers import config_per_platform -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_NAME DOMAIN = "notify" DEPENDENCIES = [] @@ -33,42 +34,45 @@ def send_message(hass, message): def setup(hass, config): """ Sets up notify services. """ + success = False - if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER): - return False + for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER): + # get platform + notify_implementation = get_component( + 'notify.{}'.format(platform)) - platform = config[DOMAIN].get(CONF_PLATFORM) + if notify_implementation is None: + _LOGGER.error("Unknown notification service specified.") + continue - notify_implementation = get_component( - 'notify.{}'.format(platform)) + # create platform service + notify_service = notify_implementation.get_service( + hass, {DOMAIN: p_config}) - if notify_implementation is None: - _LOGGER.error("Unknown notification service specified.") + if notify_service is None: + _LOGGER.error("Failed to initialize notification service %s", + platform) + continue - return False + # create service handler + def notify_message(notify_service, call): + """ Handle sending notification message service calls. """ + message = call.data.get(ATTR_MESSAGE) - notify_service = notify_implementation.get_service(hass, config) + if message is None: + return - if notify_service is None: - _LOGGER.error("Failed to initialize notification service %s", - platform) + title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - return False + notify_service.send_message(message, title=title) - def notify_message(call): - """ Handle sending notification message service calls. """ - message = call.data.get(ATTR_MESSAGE) + # register service + service_call_handler = partial(notify_message, notify_service) + service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY) + hass.services.register(DOMAIN, service_notify, service_call_handler) + success = True - if message is None: - return - - title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - notify_service.send_message(message, title=title) - - hass.services.register(DOMAIN, SERVICE_NOTIFY, notify_message) - - return True + return success # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 5e322cfc3b5..58462954d2e 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -28,7 +28,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_API_KEY _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pushbullet.py>=0.7.1'] +REQUIREMENTS = ['pushbullet.py==0.7.1'] def get_service(hass, config): diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 1bc5e9ac9a3..0df035a4a6e 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -42,7 +42,7 @@ from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) from homeassistant.const import CONF_API_KEY -REQUIREMENTS = ['python-pushover>=0.2'] +REQUIREMENTS = ['python-pushover==0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py new file mode 100644 index 00000000000..d604cffb754 --- /dev/null +++ b/homeassistant/components/notify/slack.py @@ -0,0 +1,92 @@ +""" +homeassistant.components.notify.slack +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Slack platform for notify component. + +Configuration: + +To use the Slack notifier you will need to add something like the following +to your config/configuration.yaml + +notify: + platform: slack + api_key: ABCDEFGHJKLMNOPQRSTUVXYZ + default_channel: '#general' + +Variables: + +api_key +*Required +The slack API token to use for sending slack messages. +You can get your slack API token here https://api.slack.com/web?sudo=1 + +default_channel +*Required +The default channel to post to if no channel is explicitly specified when +sending the notification message. +""" +import logging + +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, BaseNotificationService) +from homeassistant.const import CONF_API_KEY + +REQUIREMENTS = ['slacker==0.6.8'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-variable +def get_service(hass, config): + """ Get the slack notification service. """ + + if not validate_config(config, + {DOMAIN: ['default_channel', CONF_API_KEY]}, + _LOGGER): + return None + + try: + # pylint: disable=no-name-in-module, unused-variable + from slacker import Error as SlackError + + except ImportError: + _LOGGER.exception( + "Unable to import slacker. " + "Did you maybe not install the 'slacker.py' package?") + + return None + + try: + api_token = config[DOMAIN].get(CONF_API_KEY) + + return SlackNotificationService( + config[DOMAIN]['default_channel'], + api_token) + + except SlackError as ex: + _LOGGER.error( + "Slack authentication failed") + _LOGGER.exception(ex) + + +# pylint: disable=too-few-public-methods +class SlackNotificationService(BaseNotificationService): + """ Implements notification service for Slack. """ + + def __init__(self, default_channel, api_token): + from slacker import Slacker + self._default_channel = default_channel + self._api_token = api_token + self.slack = Slacker(self._api_token) + self.slack.auth.test() + + def send_message(self, message="", **kwargs): + """ Send a message to a user. """ + + from slacker import Error as SlackError + channel = kwargs.get('channel', self._default_channel) + try: + self.slack.chat.post_message(channel, message) + except SlackError as ex: + _LOGGER.exception("Could not send slack notification") + _LOGGER.exception(ex) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 25099921f45..81268c734b7 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -1,7 +1,6 @@ """ homeassistant.components.notify.xmpp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Jabber (XMPP) notification service. Configuration: @@ -29,7 +28,6 @@ The password for your given Jabber account. recipient *Required The Jabber ID (JID) that will receive the messages. - """ import logging @@ -47,7 +45,7 @@ from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) -REQUIREMENTS = ['sleekxmpp>=1.3.1'] +REQUIREMENTS = ['sleekxmpp==1.3.1', 'dnspython3==1.12.0'] def get_service(hass, config): diff --git a/homeassistant/components/process.py b/homeassistant/components/process.py deleted file mode 100644 index 21343aa977b..00000000000 --- a/homeassistant/components/process.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -homeassistant.components.process -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Provides functionality to watch for specific processes running -on the host machine. - -Author: Markus Stenberg -""" -import logging -import os - -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.util as util - -DOMAIN = 'process' -DEPENDENCIES = [] -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -PS_STRING = 'ps awx' - - -def setup(hass, config): - """ Sets up a check if specified processes are running. - - processes: dict mapping entity id to substring to search for - in process list. - """ - - # Deprecated as of 3/7/2015 - logging.getLogger(__name__).warning( - "This component has been deprecated and will be removed in the future." - " Please use sensor.systemmonitor with the process type") - - entities = {ENTITY_ID_FORMAT.format(util.slugify(pname)): pstring - for pname, pstring in config[DOMAIN].items()} - - def update_process_states(time): - """ Check ps for currently running processes and update states. """ - with os.popen(PS_STRING, 'r') as psfile: - lines = list(psfile) - - for entity_id, pstring in entities.items(): - state = STATE_ON if any(pstring in l for l in lines) else STATE_OFF - - hass.states.set(entity_id, state) - - update_process_states(None) - - hass.track_time_change(update_process_states, second=[0, 30]) - - return True diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 5dca3bddde4..73487163425 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -13,7 +13,7 @@ from datetime import datetime, date import json import atexit -from homeassistant import Event, EventOrigin, State +from homeassistant.core import Event, EventOrigin, State import homeassistant.util.dt as date_util from homeassistant.remote import JSONEncoder from homeassistant.const import ( diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 979b3281300..579ce1f20fb 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -18,7 +18,8 @@ old state will not be restored when it is being deactivated. import logging from collections import namedtuple -from homeassistant import State +from homeassistant.core import State +from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import reproduce_state @@ -104,8 +105,8 @@ class Scene(ToggleEntity): self.prev_states = None self.ignore_updates = False - self.hass.states.track_change( - self.entity_ids, self.entity_state_changed) + track_state_change( + self.hass, self.entity_ids, self.entity_state_changed) self.update() diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py index e69ba6627a7..1a67636da3d 100644 --- a/homeassistant/components/scheduler/__init__.py +++ b/homeassistant/components/scheduler/__init__.py @@ -131,7 +131,7 @@ class ServiceEventListener(EventListener): def execute(self, hass): """ Call the service. """ data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids} - hass.call_service(self.domain, self.service, data) + hass.services.call(self.domain, self.service, data) # Reschedule for next day self.schedule(hass) diff --git a/homeassistant/components/scheduler/time.py b/homeassistant/components/scheduler/time.py index 793f33d0502..9fec19fbe57 100644 --- a/homeassistant/components/scheduler/time.py +++ b/homeassistant/components/scheduler/time.py @@ -17,6 +17,7 @@ from datetime import timedelta import logging import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_point_in_time from homeassistant.components.scheduler import ServiceEventListener _LOGGER = logging.getLogger(__name__) @@ -62,7 +63,7 @@ class TimeEventListener(ServiceEventListener): """ Call the execute method """ self.execute(hass) - hass.track_point_in_time(execute, next_time) + track_point_in_time(hass, execute, next_time) _LOGGER.info( 'TimeEventListener scheduled for %s, will call service %s.%s', diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 97e12c47a46..788eb8af96d 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -10,6 +10,7 @@ from datetime import timedelta import homeassistant.util.dt as date_util import threading +from homeassistant.helpers.event import track_point_in_time from homeassistant.util import split_entity_id from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, EVENT_TIME_CHANGED) @@ -111,8 +112,8 @@ class Script(object): elif CONF_DELAY in action: delay = timedelta(**action[CONF_DELAY]) point_in_time = date_util.now() + delay - self.listener = self.hass.track_point_in_time( - self, point_in_time) + self.listener = track_point_in_time( + self.hass, self, point_in_time) return False return True diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 5cbd07d0e59..90317cdf90a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -6,7 +6,7 @@ Component to interface with various sensors that can be monitored. import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave, isy994 +from homeassistant.components import wink, zwave, isy994, verisure DOMAIN = 'sensor' DEPENDENCIES = [] @@ -18,7 +18,8 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DISCOVERY_PLATFORMS = { wink.DISCOVER_SENSORS: 'wink', zwave.DISCOVER_SENSORS: 'zwave', - isy994.DISCOVER_SENSORS: 'isy994' + isy994.DISCOVER_SENSORS: 'isy994', + verisure.DISCOVER_SENSORS: 'verisure' } diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index e0ecbab6db5..b30886448ad 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -71,7 +71,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['blockchain>=1.1.2'] +REQUIREMENTS = ['blockchain==1.1.2'] _LOGGER = logging.getLogger(__name__) OPTION_TYPES = { 'wallet': ['Wallet balance', 'BTC'], diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 37e2555136a..218860290b0 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -15,6 +15,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ DemoSensor('Outside Temperature', 15.6, TEMP_CELCIUS, 12), DemoSensor('Outside Humidity', 54, '%', None), + DemoSensor('Alarm back', 'Armed', None, None), ]) diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py new file mode 100644 index 00000000000..7949a7a44fa --- /dev/null +++ b/homeassistant/components/sensor/dht.py @@ -0,0 +1,164 @@ +""" +homeassistant.components.sensor.dht +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adafruit DHT temperature and humidity sensor. +You need a Python3 compatible version of the Adafruit_Python_DHT library +(e.g. https://github.com/mala-zaba/Adafruit_Python_DHT, +also see requirements.txt). +As this requires access to the GPIO, you will need to run home-assistant +as root. + +Configuration: + +To use the Adafruit DHT sensor you will need to +add something like the following to your config/configuration.yaml: + +sensor: + platform: dht + sensor: DHT22 + pin: 23 + monitored_conditions: + - temperature + - humidity + +Variables: + +sensor +*Required +The sensor type, DHT11, DHT22 or AM2302 + +pin +*Required +The pin the sensor is connected to, something like +'P8_11' for Beaglebone, '23' for Raspberry Pi + +monitored_conditions +*Optional +Conditions to monitor. Available conditions are temperature and humidity. +""" +import logging +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.helpers.entity import Entity + +# update this requirement to upstream as soon as it supports python3 +REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/' + + '4101340de8d2457dd194bca1e8d11cbfc237e919.zip'] +_LOGGER = logging.getLogger(__name__) +SENSOR_TYPES = { + 'temperature': ['Temperature', ''], + 'humidity': ['Humidity', '%'] +} +# Return cached results if last scan was less then this time ago +# DHT11 is able to deliver data once per second, DHT22 once every two +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the DHT sensor. """ + + try: + import Adafruit_DHT + + except ImportError: + _LOGGER.exception( + "Unable to import Adafruit_DHT. " + "Did you maybe not install the 'Adafruit_DHT' package?") + + return False + + SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit + unit = hass.config.temperature_unit + available_sensors = { + "DHT11": Adafruit_DHT.DHT11, + "DHT22": Adafruit_DHT.DHT22, + "AM2302": Adafruit_DHT.AM2302 + } + sensor = available_sensors[config['sensor']] + + pin = config['pin'] + + if not sensor or not pin: + _LOGGER.error( + "Config error " + "Please check your settings for DHT, sensor not supported.") + return None + + data = DHTClient(Adafruit_DHT, sensor, pin) + dev = [] + try: + for variable in config['monitored_conditions']: + if variable not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable) + else: + dev.append(DHTSensor(data, variable, unit)) + except KeyError: + pass + + add_devices(dev) + + +# pylint: disable=too-few-public-methods +class DHTSensor(Entity): + + """ Implements an DHT sensor. """ + + def __init__(self, dht_client, sensor_type, temp_unit): + self.client_name = 'DHT sensor' + self._name = SENSOR_TYPES[sensor_type][0] + self.dht_client = dht_client + self.temp_unit = temp_unit + self.type = sensor_type + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + return '{} {}'.format(self.client_name, self._name) + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement + + def update(self): + """ Gets the latest data from the DHT and updates the states. """ + + self.dht_client.update() + data = self.dht_client.data + + if self.type == 'temperature': + self._state = round(data['temperature'], 1) + if self.temp_unit == TEMP_FAHRENHEIT: + self._state = round(data['temperature'] * 1.8 + 32, 1) + elif self.type == 'humidity': + self._state = round(data['humidity'], 1) + + +class DHTClient(object): + + """ Gets the latest data from the DHT sensor. """ + + def __init__(self, adafruit_dht, sensor, pin): + self.adafruit_dht = adafruit_dht + self.sensor = sensor + self.pin = pin + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data the DHT sensor. """ + humidity, temperature = self.adafruit_dht.read_retry(self.sensor, + self.pin) + if temperature: + self.data['temperature'] = temperature + if humidity: + self.data['humidity'] = humidity diff --git a/homeassistant/components/sensor/efergy.py b/homeassistant/components/sensor/efergy.py index d0391e5c37b..893c4403d71 100644 --- a/homeassistant/components/sensor/efergy.py +++ b/homeassistant/components/sensor/efergy.py @@ -1,14 +1,13 @@ """ homeassistant.components.sensor.efergy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Monitors home energy use as measured by an efergy -engage hub using its (unofficial, undocumented) API. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Monitors home energy use as measured by an efergy engage hub using its +(unofficial, undocumented) API. Configuration: -To use the efergy sensor you will need to add something -like the following to your config/configuration.yaml +To use the efergy sensor you will need to add something like the following +to your config/configuration.yaml sensor: platform: efergy @@ -39,13 +38,13 @@ An array specifying the variables to monitor. period *Optional -Some variables take a period argument. Valid options are "day", -1"week", "month", and "year" +Some variables take a period argument. Valid options are "day", "week", +"month", and "year". currency *Optional This is used to display the cost/period as the unit when monitoring the -cost. It should correspond to the actual currency used in your dashboard. +cost. It should correspond to the actual currency used in your dashboard. """ import logging from requests import get diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index abd3cdadb73..b56432ab89b 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.forecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Forecast.io service. Configuration: @@ -50,6 +49,8 @@ Details for the API : https://developer.forecast.io/docs/v2 import logging from datetime import timedelta +REQUIREMENTS = ['python-forecastio==1.3.3'] + try: import forecastio except ImportError: @@ -121,10 +122,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-few-public-methods class ForeCastSensor(Entity): - """ Implements an OpenWeatherMap sensor. """ + """ Implements an Forecast.io sensor. """ def __init__(self, weather_data, sensor_type, unit): - self.client_name = 'Forecast' + self.client_name = 'Weather' self._name = SENSOR_TYPES[sensor_type][0] self.forecast_client = weather_data self._unit = unit @@ -135,7 +136,7 @@ class ForeCastSensor(Entity): @property def name(self): - return '{} - {}'.format(self.client_name, self._name) + return '{} {}'.format(self.client_name, self._name) @property def state(self): @@ -157,10 +158,6 @@ class ForeCastSensor(Entity): try: if self.type == 'summary': self._state = data.summary - # elif self.type == 'sunrise_time': - # self._state = data.sunriseTime - # elif self.type == 'sunset_time': - # self._state = data.sunsetTime elif self.type == 'precip_intensity': if data.precipIntensity == 0: self._state = 'None' @@ -220,5 +217,6 @@ class ForeCastData(object): forecast = forecastio.load_forecast(self._api_key, self.latitude, - self.longitude) + self.longitude, + units='si') self.data = forecast.currently() diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 90593875a54..c1cbf101dc2 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -1,4 +1,6 @@ """ +homeassistant.components.modbus +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for Modbus sensors. Configuration: @@ -18,16 +20,32 @@ sensor: name: My boolean sensor 2: name: My other boolean sensor + coils: + 0: + name: My coil switch -VARIABLES: +Variables: - - "slave" = slave number (ignored and can be omitted if not serial Modbus) - - "unit" = unit to attach to value (optional, ignored for boolean sensors) - - "registers" contains a list of relevant registers to read from - it can contain a "bits" section, listing relevant bits +slave +*Required +Slave number (ignored and can be omitted if not serial Modbus). - - each named register will create an integer sensor - - each named bit will create a boolean sensor +unit +*Required +Unit to attach to value (optional, ignored for boolean sensors). + +registers +*Required +Contains a list of relevant registers to read from. It can contain a +"bits" section, listing relevant bits. + +coils +*Optional +Contains a list of relevant coils to read from. + +Note: +- Each named register will create an integer sensor. +- Each named bit will create a boolean sensor. """ import logging @@ -39,6 +57,7 @@ from homeassistant.const import ( STATE_ON, STATE_OFF) _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['modbus'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -49,21 +68,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("No slave number provided for serial Modbus") return False registers = config.get("registers") - for regnum, register in registers.items(): - if register.get("name"): - sensors.append(ModbusSensor(register.get("name"), + if registers: + for regnum, register in registers.items(): + if register.get("name"): + sensors.append(ModbusSensor(register.get("name"), + slave, + regnum, + None, + register.get("unit"))) + if register.get("bits"): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + sensors.append(ModbusSensor(bit.get("name"), + slave, + regnum, + bitnum)) + coils = config.get("coils") + if coils: + for coilnum, coil in coils.items(): + sensors.append(ModbusSensor(coil.get("name"), slave, - regnum, - None, - register.get("unit"))) - if register.get("bits"): - bits = register.get("bits") - for bitnum, bit in bits.items(): - if bit.get("name"): - sensors.append(ModbusSensor(bit.get("name"), - slave, - regnum, - bitnum)) + coilnum, + coil=True)) + add_devices(sensors) @@ -71,13 +99,14 @@ class ModbusSensor(Entity): # pylint: disable=too-many-arguments """ Represents a Modbus Sensor """ - def __init__(self, name, slave, register, bit=None, unit=None): + def __init__(self, name, slave, register, bit=None, unit=None, coil=False): self._name = name self.slave = int(slave) if slave else 1 self.register = int(register) self.bit = int(bit) if bit else None self._value = None self._unit = unit + self._coil = coil def __str__(self): return "%s: %s" % (self.name, self.state) @@ -118,19 +147,19 @@ class ModbusSensor(Entity): else: return self._unit - @property - def state_attributes(self): - attr = super().state_attributes - return attr - def update(self): - result = modbus.NETWORK.read_holding_registers(unit=self.slave, - address=self.register, - count=1) - val = 0 - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) - if self.bit: - self._value = val & (0x0001 << self.bit) + """ Update the state of the sensor. """ + if self._coil: + result = modbus.NETWORK.read_coils(self.register, 1) + self._value = result.bits[0] else: - self._value = val + result = modbus.NETWORK.read_holding_registers( + unit=self.slave, address=self.register, + count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + if self.bit: + self._value = val & (0x0001 << self.bit) + else: + self._value = val diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py new file mode 100644 index 00000000000..d5dc192e450 --- /dev/null +++ b/homeassistant/components/sensor/mqtt.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +""" +homeassistant.components.sensor.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT sensor. + +This generic sensor implementation uses the MQTT message payload +as the sensor value. If messages in this state_topic are published +with RETAIN flag, the sensor will receive an instant update with +last known value. Otherwise, the initial state will be undefined. + +sensor: + platform: mqtt + name: "MQTT Sensor" + state_topic: "home/bedroom/temperature" + unit_of_measurement: "ºC" + +Variables: + +name +*Optional +The name of the sensor. Default is 'MQTT Sensor'. + +state_topic +*Required +The MQTT topic subscribed to receive sensor values. + +unit_of_measurement +*Optional +Defines the units of measurement of the sensor, if any. + +""" + +import logging +from homeassistant.helpers.entity import Entity +import homeassistant.components.mqtt as mqtt + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "MQTT Sensor" + +DEPENDENCIES = ['mqtt'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add MQTT Sensor """ + + if config.get('state_topic') is None: + _LOGGER.error("Missing required variable: state_topic") + return False + + add_devices_callback([MqttSensor( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic'), + config.get('unit_of_measurement'))]) + + +class MqttSensor(Entity): + """ Represents a sensor that can be updated using MQTT """ + def __init__(self, hass, name, state_topic, unit_of_measurement): + self._state = "-" + self._hass = hass + self._name = name + self._state_topic = state_topic + self._unit_of_measurement = unit_of_measurement + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + self._state = payload + self.update_ha_state() + + mqtt.subscribe(hass, self._state_topic, message_received) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the sensor """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit this state is expressed in. """ + return self._unit_of_measurement + + @property + def state(self): + """ Returns the state of the entity. """ + return self._state diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index ad9649f9966..994b110a585 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -36,15 +36,15 @@ ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/master.zip' - '#egg=pymysensors-0.1'] +REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' + + '35b87d880147a34107da0d40cb815d75e6cb4af7.zip'] def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup the mysensors platform. """ import mysensors.mysensors as mysensors - import mysensors.const as const + import mysensors.const_14 as const devices = {} # keep track of devices added to HA # Just assume celcius means that the user wants metric for now. diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 22720748034..537fc9f59b5 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -48,7 +48,7 @@ from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pywm>=2.2.1'] +REQUIREMENTS = ['pyowm==2.2.1'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'weather': ['Condition', ''], diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index b9762995ea5..8e5a1ad3dca 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -1,17 +1,24 @@ """ homeassistant.components.sensor.rfxtrx -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Shows sensor values from rfxtrx sensors. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shows sensor values from RFXtrx sensors. -Possible config keys: -device="path to rfxtrx device" +Configuration: +To use the rfxtrx sensors you will need to add something like the following to +your config/configuration.yaml Example: -sensor 2: - platform: rfxtrx - device : /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0 +sensor: + platform: rfxtrx + device: PATH_TO_DEVICE +Variables: + +device +*Required +Path to your RFXtrx device. +E.g. /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0 """ import logging from collections import OrderedDict @@ -19,8 +26,8 @@ from collections import OrderedDict from homeassistant.const import (TEMP_CELCIUS) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip' - '#RFXtrx>=0.15'] +REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/' + + 'ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip'] DATA_TYPES = OrderedDict([ ('Temperature', TEMP_CELCIUS), @@ -31,7 +38,7 @@ DATA_TYPES = OrderedDict([ def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup the rfxtrx platform. """ + """ Setup the RFXtrx platform. """ logger = logging.getLogger(__name__) sensors = {} # keep track of sensors added to HA @@ -57,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RfxtrxSensor(Entity): - """ Represents a Rfxtrx Sensor. """ + """ Represents a RFXtrx sensor. """ def __init__(self, event): self.event = event diff --git a/homeassistant/components/sensor/rpi_gpio.py b/homeassistant/components/sensor/rpi_gpio.py new file mode 100644 index 00000000000..f973b24a301 --- /dev/null +++ b/homeassistant/components/sensor/rpi_gpio.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +homeassistant.components.sensor.rpi_gpio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a binary state sensor using RPi GPIO. +Note: To use RPi GPIO, Home Assistant must be run as root. + +sensor: + platform: rpi_gpio + pull_mode: "UP" + value_high: "Active" + value_low: "Inactive" + ports: + 11: PIR Office + 12: PIR Bedroom + +Variables: + +pull_mode +*Optional +The internal pull to use (UP or DOWN). Default is UP. + +value_high +*Optional +The value of the sensor when the port is HIGH. Default is "HIGH". + +value_low +*Optional +The value of the sensor when the port is LOW. Default is "LOW". + +bouncetime +*Optional +The time in milliseconds for port debouncing. Default is 50ms. + +ports +*Required +An array specifying the GPIO ports to use and the name to use in the frontend. + +""" + +import logging +from homeassistant.helpers.entity import Entity +try: + import RPi.GPIO as GPIO +except ImportError: + GPIO = None +from homeassistant.const import (DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) + +DEFAULT_PULL_MODE = "UP" +DEFAULT_VALUE_HIGH = "HIGH" +DEFAULT_VALUE_LOW = "LOW" +DEFAULT_BOUNCETIME = 50 + +REQUIREMENTS = ['RPi.GPIO==0.5.11'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Raspberry PI GPIO ports. """ + if GPIO is None: + _LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.') + return + # pylint: disable=no-member + GPIO.setmode(GPIO.BCM) + + sensors = [] + pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE) + value_high = config.get('value_high', DEFAULT_VALUE_HIGH) + value_low = config.get('value_low', DEFAULT_VALUE_LOW) + bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME) + ports = config.get('ports') + for port_num, port_name in ports.items(): + sensors.append(RPiGPIOSensor( + port_name, port_num, pull_mode, + value_high, value_low, bouncetime)) + add_devices(sensors) + + def cleanup_gpio(event): + """ Stuff to do before stop home assistant. """ + # pylint: disable=no-member + GPIO.cleanup() + + def prepare_gpio(event): + """ Stuff to do when home assistant starts. """ + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class RPiGPIOSensor(Entity): + """ Sets up the Raspberry PI GPIO ports. """ + def __init__(self, port_name, port_num, pull_mode, + value_high, value_low, bouncetime): + # pylint: disable=no-member + self._name = port_name or DEVICE_DEFAULT_NAME + self._port = port_num + self._pull = GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP + self._vhigh = value_high + self._vlow = value_low + self._bouncetime = bouncetime + GPIO.setup(self._port, GPIO.IN, pull_up_down=self._pull) + self._state = self._vhigh if GPIO.input(self._port) else self._vlow + + def edge_callback(channel): + """ port changed state """ + # pylint: disable=no-member + self._state = self._vhigh if GPIO.input(channel) else self._vlow + self.update_ha_state() + + GPIO.add_event_detect( + self._port, + GPIO.BOTH, + callback=edge_callback, + bouncetime=self._bouncetime) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the sensor """ + return self._name + + @property + def state(self): + """ Returns the state of the entity. """ + return self._state diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 1d1bdb1f3b5..b473cc27283 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -66,7 +66,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_ON, STATE_OFF -REQUIREMENTS = ['psutil>=3.0.0'] +REQUIREMENTS = ['psutil==3.0.0'] SENSOR_TYPES = { 'disk_use_percent': ['Disk Use', '%'], 'disk_use': ['Disk Use', 'GiB'], diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index f80a3c19f12..e93c6e4c97f 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -35,6 +35,8 @@ import homeassistant.util as util DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit']) +REQUIREMENTS = ['tellcore-py==1.0.4'] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py new file mode 100644 index 00000000000..8579a922661 --- /dev/null +++ b/homeassistant/components/sensor/temper.py @@ -0,0 +1,71 @@ +""" +homeassistant.components.sensor.temper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for getting temperature from TEMPer devices. + +Configuration: +To use the temper sensors you will need to add something like the following to +your config/configuration.yaml + +Example: + +sensor: + platform: temper +""" +import logging +from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/rkabadi/temper-python/archive/' + + '3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return Temper sensors. """ + try: + # pylint: disable=no-name-in-module, import-error + from temperusb.temper import TemperHandler + except ImportError: + _LOGGER.error('Failed to import temperusb') + return False + + temp_unit = hass.config.temperature_unit + name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) + temper_devices = TemperHandler().get_devices() + add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx)) + for idx, dev in enumerate(temper_devices)]) + + +class TemperSensor(Entity): + """ Represents an Temper temperature sensor. """ + def __init__(self, temper_device, temp_unit, name): + self.temper_device = temper_device + self.temp_unit = temp_unit + self.current_value = None + self._name = name + + @property + def name(self): + """ Returns the name of the temperature sensor. """ + return self._name + + @property + def state(self): + """ Returns the state of the entity. """ + return self.current_value + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self.temp_unit + + def update(self): + """ Retrieve latest state. """ + try: + self.current_value = self.temper_device.get_temperature() + except IOError: + _LOGGER.error('Failed to get temperature due to insufficient ' + 'permissions. Try running with "sudo"') diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index b9ed3ea4e9f..587f5131d9d 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -67,7 +67,7 @@ from transmissionrpc.error import TransmissionError import logging -REQUIREMENTS = ['transmissionrpc>=0.11'] +REQUIREMENTS = ['transmissionrpc==0.11'] SENSOR_TYPES = { 'current_status': ['Status', ''], 'download_speed': ['Down Speed', 'MB/s'], diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py new file mode 100644 index 00000000000..61af1089775 --- /dev/null +++ b/homeassistant/components/sensor/verisure.py @@ -0,0 +1,127 @@ +""" +homeassistant.components.sensor.verisure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Interfaces with Verisure sensors. +""" +import logging + +import homeassistant.components.verisure as verisure + +from homeassistant.helpers.entity import Entity +from homeassistant.const import TEMP_CELCIUS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Verisure platform. """ + + if not verisure.MY_PAGES: + _LOGGER.error('A connection has not been made to Verisure mypages.') + return False + + sensors = [] + + sensors.extend([ + VerisureThermometer(value) + for value in verisure.get_climate_status().values() + if verisure.SHOW_THERMOMETERS and + hasattr(value, 'temperature') and value.temperature + ]) + + sensors.extend([ + VerisureHygrometer(value) + for value in verisure.get_climate_status().values() + if verisure.SHOW_HYGROMETERS and + hasattr(value, 'humidity') and value.humidity + ]) + + sensors.extend([ + VerisureAlarm(value) + for value in verisure.get_alarm_status().values() + if verisure.SHOW_ALARM + ]) + + add_devices(sensors) + + +class VerisureThermometer(Entity): + """ represents a Verisure thermometer within home assistant. """ + + def __init__(self, climate_status): + self._id = climate_status.id + self._device = verisure.MY_PAGES.DEVICE_CLIMATE + + @property + def name(self): + """ Returns the name of the device. """ + return '{} {}'.format( + verisure.STATUS[self._device][self._id].location, + "Temperature") + + @property + def state(self): + """ Returns the state of the device. """ + # remove ° character + return verisure.STATUS[self._device][self._id].temperature[:-1] + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity """ + return TEMP_CELCIUS # can verisure report in fahrenheit? + + def update(self): + ''' update sensor ''' + verisure.update() + + +class VerisureHygrometer(Entity): + """ represents a Verisure hygrometer within home assistant. """ + + def __init__(self, climate_status): + self._id = climate_status.id + self._device = verisure.MY_PAGES.DEVICE_CLIMATE + + @property + def name(self): + """ Returns the name of the device. """ + return '{} {}'.format( + verisure.STATUS[self._device][self._id].location, + "Humidity") + + @property + def state(self): + """ Returns the state of the device. """ + # remove % character + return verisure.STATUS[self._device][self._id].humidity[:-1] + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity """ + return "%" + + def update(self): + ''' update sensor ''' + verisure.update() + + +class VerisureAlarm(Entity): + """ represents a Verisure alarm status within home assistant. """ + + def __init__(self, alarm_status): + self._id = alarm_status.id + self._device = verisure.MY_PAGES.DEVICE_ALARM + + @property + def name(self): + """ Returns the name of the device. """ + return 'Alarm {}'.format(self._id) + + @property + def state(self): + """ Returns the state of the device. """ + return verisure.STATUS[self._device][self._id].label + + def update(self): + ''' update sensor ''' + verisure.update() diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 1d52550aa37..0b3d33cea24 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -1,9 +1,16 @@ -""" Support for Wink sensors. """ +""" +homeassistant.components.sensor.wink +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Wink sensors. +""" import logging from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] + def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Wink platform. """ @@ -24,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkSensorDevice(Entity): - """ represents a wink sensor within home assistant. """ + """ Represents a wink sensor. """ def __init__(self, wink): self.wink = wink diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index d057635d9f6..b63e64156a3 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.zwave ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Interfaces with Z-Wave sensors. """ # pylint: disable=import-error diff --git a/homeassistant/components/simple_alarm.py b/homeassistant/components/simple_alarm.py index 4f2dbd768d5..46cb52fa950 100644 --- a/homeassistant/components/simple_alarm.py +++ b/homeassistant/components/simple_alarm.py @@ -9,6 +9,7 @@ Provides a simple alarm feature: import logging import homeassistant.loader as loader +from homeassistant.helpers.event import track_state_change from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME DOMAIN = "simple_alarm" @@ -83,8 +84,8 @@ def setup(hass, config): if not device_tracker.is_on(hass): unknown_alarm() - hass.states.track_change( - light.ENTITY_ID_ALL_LIGHTS, + track_state_change( + hass, light.ENTITY_ID_ALL_LIGHTS, unknown_alarm_if_lights_on, STATE_OFF, STATE_ON) def ring_known_alarm(entity_id, old_state, new_state): @@ -93,8 +94,8 @@ def setup(hass, config): known_alarm() # Track home coming of each device - hass.states.track_change( - hass.states.entity_ids(device_tracker.DOMAIN), + track_state_change( + hass, hass.states.entity_ids(device_tracker.DOMAIN), ring_known_alarm, STATE_NOT_HOME, STATE_HOME) return True diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index fd2cfa46b72..802eddb4a3a 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -21,14 +21,17 @@ The sun event need to have the type 'sun', which service to call, which event """ import logging from datetime import timedelta +import urllib import homeassistant.util as util import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import ( + track_point_in_utc_time, track_point_in_time) from homeassistant.helpers.entity import Entity from homeassistant.components.scheduler import ServiceEventListener DEPENDENCIES = [] -REQUIREMENTS = ['astral>=0.8.1'] +REQUIREMENTS = ['astral==0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" @@ -129,8 +132,13 @@ def setup(hass, config): if elevation is None: google = GoogleGeocoder() - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info('Retrieved elevation from Google: %s', location.elevation) + try: + google._get_elevation(location) # pylint: disable=protected-access + _LOGGER.info( + 'Retrieved elevation from Google: %s', location.elevation) + except urllib.error.URLError: + # If no internet connection available etc. + pass sun = Sun(hass, location) sun.point_in_time_listener(dt_util.utcnow()) @@ -203,8 +211,8 @@ class Sun(Entity): self.update_ha_state() # Schedule next update at next_change+1 second so sun state has changed - self.hass.track_point_in_utc_time( - self.point_in_time_listener, + track_point_in_utc_time( + self.hass, self.point_in_time_listener, self.next_change + timedelta(seconds=1)) @@ -266,7 +274,7 @@ class SunEventListener(ServiceEventListener): """ Call the execute method. """ self.execute(hass) - hass.track_point_in_time(execute, next_time) + track_point_in_time(hass, execute, next_time) return next_time diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index f245406fb28..48d3e31e9b5 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) -from homeassistant.components import group, discovery, wink, isy994 +from homeassistant.components import group, discovery, wink, isy994, verisure DOMAIN = 'switch' DEPENDENCIES = [] @@ -34,6 +34,7 @@ DISCOVERY_PLATFORMS = { discovery.SERVICE_WEMO: 'wemo', wink.DISCOVER_SWITCHES: 'wink', isy994.DISCOVER_SWITCHES: 'isy994', + verisure.DISCOVER_SWITCHES: 'verisure' } PROP_TO_ATTR = { @@ -49,21 +50,18 @@ _LOGGER = logging.getLogger(__name__) def is_on(hass, entity_id=None): """ Returns if the switch is on based on the statemachine. """ entity_id = entity_id or ENTITY_ID_ALL_SWITCHES - return hass.states.is_state(entity_id, STATE_ON) def turn_on(hass, entity_id=None): """ Turns all or specified switch on. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None): """ Turns all or specified switch off. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @@ -88,7 +86,6 @@ def setup(hass, config): switch.update_ha_state(True) hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service) return True diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py new file mode 100644 index 00000000000..200c5746e27 --- /dev/null +++ b/homeassistant/components/switch/edimax.py @@ -0,0 +1,117 @@ +""" +homeassistant.components.switch.edimax +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Edimax switches. + +Configuration: + +To use the Edimax switch you will need to add something like the following to +your config/configuration.yaml. + +switch: + platform: edimax + host: 192.168.1.32 + username: YOUR_USERNAME + password: YOUR_PASSWORD + name: Edimax Smart Plug + +Variables: + +host +*Required +This is the IP address of your Edimax switch. Example: 192.168.1.32 + +username +*Required +Your username to access your Edimax switch. + +password +*Required +Your password. + +name +*Optional +The name to use when displaying this switch instance. +""" +import logging + +from homeassistant.helpers import validate_config +from homeassistant.components.switch import SwitchDevice, DOMAIN +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD,\ + CONF_NAME + +# constants +DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = '1234' +DEVICE_DEFAULT_NAME = 'Edimax Smart Plug' +REQUIREMENTS = ['https://github.com/rkabadi/pyedimax/archive/' + + '365301ce3ff26129a7910c501ead09ea625f3700.zip'] + +# setup logger +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return Edimax Smart Plugs. """ + try: + # pylint: disable=no-name-in-module, import-error + from pyedimax.smartplug import SmartPlug + except ImportError: + _LOGGER.error('Failed to import pyedimax') + return False + + # pylint: disable=global-statement + # check for required values in configuration file + if not validate_config({DOMAIN: config}, + {DOMAIN: [CONF_HOST]}, + _LOGGER): + return False + + host = config.get(CONF_HOST) + auth = (config.get(CONF_USERNAME, DEFAULT_USERNAME), + config.get(CONF_PASSWORD, DEFAULT_PASSWORD)) + name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) + + add_devices_callback([SmartPlugSwitch(SmartPlug(host, auth), name)]) + + +class SmartPlugSwitch(SwitchDevice): + """ Represents an Edimax Smart Plug switch. """ + def __init__(self, smartplug, name): + self.smartplug = smartplug + self._name = name + + @property + def name(self): + """ Returns the name of the Smart Plug, if any. """ + return self._name + + @property + def current_power_mwh(self): + """ Current power usage in mwh. """ + try: + return float(self.smartplug.now_power) / 1000000.0 + except ValueError: + return None + + @property + def today_power_mw(self): + """ Today total power usage in mw. """ + try: + return float(self.smartplug.now_energy_day) / 1000.0 + except ValueError: + return None + + @property + def is_on(self): + """ True if switch is on. """ + return self.smartplug.state == 'ON' + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + self.smartplug.state = 'ON' + + def turn_off(self): + """ Turns the switch off. """ + self.smartplug.state = 'OFF' diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 388152361d2..6ab82df482a 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -1,24 +1,21 @@ """ homeassistant.components.switch.hikvision -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support turning on/off motion detection on Hikvision cameras. Note: Currently works using default https port only. -CGI API Guide: -http://bit.ly/1RuyUuF +CGI API Guide: http://bit.ly/1RuyUuF Configuration: -To use the Hikvision motion detection -switch you will need to add something like the -following to your config/configuration.yaml +To use the Hikvision motion detection switch you will need to add something +like the following to your config/configuration.yaml switch: platform: hikvisioncam name: Hikvision Cam 1 Motion Detection - host: 192.168.1.26 + host: 192.168.1.32 username: YOUR_USERNAME password: YOUR_PASSWORD @@ -30,16 +27,15 @@ This is the IP address of your Hikvision camera. Example: 192.168.1.32 username *Required -Your Hikvision camera username +Your Hikvision camera username. password *Required -Your Hikvision camera username +Your Hikvision camera username. name *Optional The name to use when displaying this switch instance. - """ from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import STATE_ON, STATE_OFF @@ -53,7 +49,7 @@ except ImportError: hikvision.api = None _LOGGING = logging.getLogger(__name__) -REQUIREMENTS = ['hikvision>=0.4'] +REQUIREMENTS = ['hikvision==0.4'] # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index 6513ba71f4a..dbf9bc83aad 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -18,12 +18,16 @@ sensor: name: My switch 2: name: My other switch + coils: + 0: + name: My coil switch VARIABLES: - "slave" = slave number (ignored and can be omitted if not serial Modbus) - "registers" contains a list of relevant registers to read from - it must contain a "bits" section, listing relevant bits + - "coils" contains a list of relevant coils to read from/write to - each named bit will create a switch """ @@ -34,6 +38,7 @@ import homeassistant.components.modbus as modbus from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['modbus'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -44,25 +49,36 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("No slave number provided for serial Modbus") return False registers = config.get("registers") - for regnum, register in registers.items(): - bits = register.get("bits") - for bitnum, bit in bits.items(): - if bit.get("name"): - switches.append(ModbusSwitch(bit.get("name"), - slave, - regnum, - bitnum)) + if registers: + for regnum, register in registers.items(): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + switches.append(ModbusSwitch(bit.get("name"), + slave, + regnum, + bitnum)) + coils = config.get("coils") + if coils: + for coilnum, coil in coils.items(): + switches.append(ModbusSwitch(coil.get("name"), + slave, + coilnum, + 0, + coil=True)) add_devices(switches) class ModbusSwitch(ToggleEntity): + # pylint: disable=too-many-arguments """ Represents a Modbus switch. """ - def __init__(self, name, slave, register, bit): + def __init__(self, name, slave, register, bit, coil=False): self._name = name self.slave = int(slave) if slave else 1 self.register = int(register) self.bit = int(bit) + self._coil = coil self._is_on = None self.register_value = None @@ -92,33 +108,44 @@ class ModbusSwitch(ToggleEntity): """ Get the name of the switch. """ return self._name - @property - def state_attributes(self): - attr = super().state_attributes - return attr - def turn_on(self, **kwargs): + """ Set switch on. """ if self.register_value is None: self.update() - val = self.register_value | (0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + + if self._coil: + modbus.NETWORK.write_coil(self.register, True) + else: + val = self.register_value | (0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) def turn_off(self, **kwargs): + """ Set switch off. """ if self.register_value is None: self.update() - val = self.register_value & ~(0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + + if self._coil: + modbus.NETWORK.write_coil(self.register, False) + else: + val = self.register_value & ~(0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) def update(self): - result = modbus.NETWORK.read_holding_registers(unit=self.slave, - address=self.register, - count=1) - val = 0 - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) - self.register_value = val - self._is_on = (val & (0x0001 << self.bit) > 0) + """ Update the state of the switch. """ + if self._coil: + result = modbus.NETWORK.read_coils(self.register, 1) + self.register_value = result.bits[0] + self._is_on = self.register_value + else: + result = modbus.NETWORK.read_holding_registers( + unit=self.slave, address=self.register, + count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + self.register_value = val + self._is_on = (val & (0x0001 << self.bit) > 0) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py new file mode 100644 index 00000000000..c6ebdaa2ad6 --- /dev/null +++ b/homeassistant/components/switch/mqtt.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +""" +homeassistant.components.switch.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT switch. + +In an ideal scenario, the MQTT device will have a state topic to publish +state changes. If these messages are published with RETAIN flag, the MQTT +switch will receive an instant state update after subscription and will +start with correct state. Otherwise, the initial state of the switch will +be false/off. + +When a state topic is not available, the switch will work in optimistic mode. +In this mode, the switch will immediately change state after every command. +Otherwise, the switch will wait for state confirmation from device +(message from state_topic). + +Optimistic mode can be forced, even if state topic is available. +Try to enable it, if experiencing incorrect switch operation. + + +Configuration: + +switch: + platform: mqtt + name: "Bedroom Switch" + state_topic: "home/bedroom/switch1" + command_topic: "home/bedroom/switch1/set" + payload_on: "ON" + payload_off: "OFF" + optimistic: false + +Variables: + +name +*Optional +The name of the switch. Default is 'MQTT Switch'. + +state_topic +*Optional +The MQTT topic subscribed to receive state updates. +If not specified, optimistic mode will be forced. + +command_topic +*Required +The MQTT topic to publish commands to change the switch state. + +payload_on +*Optional +The payload that represents enabled state. Default is "ON". + +payload_off +*Optional +The payload that represents disabled state. Default is "OFF". + +optimistic +*Optional +Flag that defines if switch works in optimistic mode. Default is false. + +""" + +import logging +import homeassistant.components.mqtt as mqtt +from homeassistant.components.switch import SwitchDevice + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "MQTT Switch" +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_OPTIMISTIC = False + +DEPENDENCIES = ['mqtt'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add MQTT Switch """ + + if config.get('command_topic') is None: + _LOGGER.error("Missing required variable: command_topic") + return False + + add_devices_callback([MqttSwitch( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic'), + config.get('command_topic'), + config.get('payload_on', DEFAULT_PAYLOAD_ON), + config.get('payload_off', DEFAULT_PAYLOAD_OFF), + config.get('optimistic', DEFAULT_OPTIMISTIC))]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class MqttSwitch(SwitchDevice): + """ Represents a switch that can be togggled using MQTT """ + def __init__(self, hass, name, state_topic, command_topic, + payload_on, payload_off, optimistic): + self._state = False + self._hass = hass + self._name = name + self._state_topic = state_topic + self._command_topic = command_topic + self._payload_on = payload_on + self._payload_off = payload_off + self._optimistic = optimistic + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + if payload == self._payload_on: + self._state = True + self.update_ha_state() + elif payload == self._payload_off: + self._state = False + self.update_ha_state() + + if self._state_topic is None: + # force optimistic mode + self._optimistic = True + else: + # subscribe the state_topic + mqtt.subscribe(hass, self._state_topic, message_received) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the switch """ + return self._name + + @property + def is_on(self): + """ True if device is on. """ + return self._state + + def turn_on(self, **kwargs): + """ Turn the device on. """ + mqtt.publish(self.hass, self._command_topic, self._payload_on) + if self._optimistic: + # optimistically assume that switch has changed state + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + mqtt.publish(self.hass, self._command_topic, self._payload_off) + if self._optimistic: + # optimistically assume that switch has changed state + self._state = False + self.update_ha_state() diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py new file mode 100644 index 00000000000..4afa38aa80a --- /dev/null +++ b/homeassistant/components/switch/rpi_gpio.py @@ -0,0 +1,134 @@ +""" +homeassistant.components.switch.rpi_gpio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to control the GPIO pins of a Raspberry Pi. +Note: To use RPi GPIO, Home Assistant must be run as root. + +Configuration: + +switch: + platform: rpi_gpio + invert_logic: false + ports: + 11: Fan Office + 12: Light Desk + +Variables: + +invert_logic +*Optional +If true, inverts the output logic to ACTIVE LOW. Default is false (ACTIVE HIGH) + +ports +*Required +An array specifying the GPIO ports to use and the name to use in the frontend. +""" + +import logging +try: + import RPi.GPIO as GPIO +except ImportError: + GPIO = None +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import (DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) + +DEFAULT_INVERT_LOGIC = False + +REQUIREMENTS = ['RPi.GPIO==0.5.11'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Raspberry PI GPIO ports. """ + if GPIO is None: + _LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.') + return + # pylint: disable=no-member + GPIO.setmode(GPIO.BCM) + + switches = [] + invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC) + ports = config.get('ports') + for port_num, port_name in ports.items(): + switches.append(RPiGPIOSwitch(port_name, port_num, invert_logic)) + add_devices(switches) + + def cleanup_gpio(event): + """ Stuff to do before stop home assistant. """ + # pylint: disable=no-member + GPIO.cleanup() + + def prepare_gpio(event): + """ Stuff to do when home assistant starts. """ + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + + +class RPiGPIOSwitch(ToggleEntity): + """ Represents a port that can be toggled using Raspberry Pi GPIO. """ + + def __init__(self, name, gpio, invert_logic): + self._name = name or DEVICE_DEFAULT_NAME + self._gpio = gpio + self._active_state = not invert_logic + self._state = not self._active_state + # pylint: disable=no-member + GPIO.setup(gpio, GPIO.OUT) + + @property + def name(self): + """ The name of the port. """ + return self._name + + @property + def should_poll(self): + """ No polling needed. """ + return False + + @property + def is_on(self): + """ True if device is on. """ + return self._state + + def turn_on(self, **kwargs): + """ Turn the device on. """ + if self._switch(self._active_state): + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + if self._switch(not self._active_state): + self._state = False + self.update_ha_state() + + def _switch(self, new_state): + """ Change the output value to Raspberry Pi GPIO port. """ + _LOGGER.info('Setting GPIO %s to %s', self._gpio, new_state) + # pylint: disable=bare-except + try: + # pylint: disable=no-member + GPIO.output(self._gpio, 1 if new_state else 0) + except: + _LOGGER.error('GPIO "%s" output failed', self._gpio) + return False + return True + + # pylint: disable=no-self-use + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + return None + + @property + def state_attributes(self): + """ Returns optional state attributes. """ + data = {} + device_attr = self.device_state_attributes + if device_attr is not None: + data.update(device_attr) + return data diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 6f9070d28fb..230151382e7 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -19,6 +19,8 @@ import tellcore.constants as tellcore_constants SINGAL_REPETITIONS = 1 +REQUIREMENTS = ['tellcore-py==1.0.4'] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index 8638f11a2e9..d5cf716c770 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -1,8 +1,7 @@ """ homeassistant.components.switch.transmission ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Enable or disable Transmission BitTorrent client Turtle Mode +Enable or disable Transmission BitTorrent client Turtle Mode. Configuration: @@ -29,37 +28,32 @@ The port your Transmission daemon uses, defaults to 9091. Example: 8080 username *Optional -Your Transmission username, if you use authentication +Your Transmission username, if you use authentication. password *Optional -Your Transmission username, if you use authentication +Your Transmission username, if you use authentication. name *Optional The name to use when displaying this Transmission instance. - - """ - from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.helpers.entity import ToggleEntity # pylint: disable=no-name-in-module, import-error import transmissionrpc - from transmissionrpc.error import TransmissionError - import logging _LOGGING = logging.getLogger(__name__) -REQUIREMENTS = ['transmissionrpc>=0.11'] +REQUIREMENTS = ['transmissionrpc==0.11'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """ Sets up the sensors. """ + """ Sets up the transmission sensor. """ host = config.get(CONF_HOST) username = config.get(CONF_USERNAME, None) password = config.get(CONF_PASSWORD, None) @@ -87,7 +81,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TransmissionSwitch(ToggleEntity): - """ A Transmission sensor. """ def __init__(self, transmission_client, name): diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 2e472b4358f..77b49ebf826 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -1,9 +1,11 @@ """ +homeassistant.components.switch.vera +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for Vera switches. Configuration: To use the Vera lights you will need to add something like the following to -your config/configuration.yaml +your config/configuration.yaml. switch: platform: vera @@ -15,38 +17,32 @@ switch: 13: name: Another Switch -VARIABLES: +Variables: vera_controller_url *Required This is the base URL of your vera controller including the port number if not -running on 80 -Example: http://192.168.1.21:3480/ - +running on 80. Example: http://192.168.1.21:3480/ device_data *Optional This contains an array additional device info for your Vera devices. It is not required and if not specified all lights configured in your Vera controller will be added with default values. You should use the id of your vera device -as the key for the device within device_data - +as the key for the device within device_data. These are the variables for the device_data array: - name *Optional This parameter allows you to override the name of your Vera device in the HA interface, if not specified the value configured for the device in your Vera -will be used - +will be used. exclude *Optional This parameter allows you to exclude the specified device from homeassistant, -it should be set to "true" if you want this device excluded - +it should be set to "true" if you want this device excluded. """ import logging import time @@ -82,7 +78,7 @@ def get_devices(hass, config): devices = vera_controller.get_devices([ 'Switch', 'Armable Sensor', 'On/Off Switch']) except RequestException: - # There was a network related error connecting to the vera controller + # There was a network related error connecting to the vera controller. _LOGGER.exception("Error communicating with Vera API") return False @@ -103,7 +99,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSwitch(ToggleEntity): - """ Represents a Vera Switch """ + """ Represents a Vera Switch. """ def __init__(self, vera_device, extra_data=None): self.vera_device = vera_device diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py new file mode 100644 index 00000000000..6c8f0352c3f --- /dev/null +++ b/homeassistant/components/switch/verisure.py @@ -0,0 +1,63 @@ +""" +homeassistant.components.switch.verisure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Verisure Smartplugs +""" +import logging + +import homeassistant.components.verisure as verisure +from homeassistant.components.switch import SwitchDevice + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Arduino platform. """ + + if not verisure.MY_PAGES: + _LOGGER.error('A connection has not been made to Verisure mypages.') + return False + + switches = [] + + switches.extend([ + VerisureSmartplug(value) + for value in verisure.get_smartplug_status().values() + if verisure.SHOW_SMARTPLUGS + ]) + + add_devices(switches) + + +class VerisureSmartplug(SwitchDevice): + """ Represents a Verisure smartplug. """ + def __init__(self, smartplug_status): + self._id = smartplug_status.id + self.status_on = verisure.MY_PAGES.SMARTPLUG_ON + self.status_off = verisure.MY_PAGES.SMARTPLUG_OFF + + @property + def name(self): + """ Get the name (location) of the smartplug. """ + return verisure.get_smartplug_status()[self._id].location + + @property + def is_on(self): + """ Returns True if on """ + plug_status = verisure.get_smartplug_status()[self._id].status + return plug_status == self.status_on + + def turn_on(self): + """ Set smartplug status on """ + verisure.MY_PAGES.set_smartplug_status( + self._id, + self.status_on) + + def turn_off(self): + """ Set smartplug status off """ + verisure.MY_PAGES.set_smartplug_status( + self._id, + self.status_off) + + def update(self): + verisure.update() diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index f6fb0c266fe..cf1aed40d52 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -8,6 +8,8 @@ import logging from homeassistant.components.switch import SwitchDevice +REQUIREMENTS = ['pywemo==0.2'] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -16,7 +18,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo.discovery as discovery if discovery_info is not None: - device = discovery.device_from_description(discovery_info) + device = discovery.device_from_description(discovery_info[2]) if device: add_devices_callback([WemoSwitch(device)]) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 792de50a855..c9fb045d9c0 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -9,6 +9,9 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] + def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Wink platform. """ diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 40e392709f2..bbc0979e38c 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -10,6 +10,7 @@ from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util from homeassistant.helpers.entity import Entity +from homeassistant.helpers.temperature import convert from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, TEMP_CELCIUS) @@ -86,7 +87,9 @@ def setup(hass, config): return for thermostat in target_thermostats: - thermostat.set_temperature(temperature) + thermostat.set_temperature(convert( + temperature, hass.config.temperature_unit, + thermostat.unit_of_measurement)) for thermostat in target_thermostats: thermostat.update_ha_state(True) @@ -118,9 +121,20 @@ class ThermostatDevice(Entity): @property def state_attributes(self): """ Returns optional state attributes. """ + + thermostat_unit = self.unit_of_measurement + user_unit = self.hass.config.temperature_unit + data = { - ATTR_CURRENT_TEMPERATURE: self.hass.config.temperature( - self.current_temperature, self.unit_of_measurement)[0] + ATTR_CURRENT_TEMPERATURE: round(convert(self.current_temperature, + thermostat_unit, + user_unit), 1), + ATTR_MIN_TEMP: round(convert(self.min_temp, + thermostat_unit, + user_unit), 0), + ATTR_MAX_TEMP: round(convert(self.max_temp, + thermostat_unit, + user_unit), 0) } is_away = self.is_away_mode_on @@ -133,11 +147,13 @@ class ThermostatDevice(Entity): if device_attr is not None: data.update(device_attr) - data[ATTR_MIN_TEMP] = self.min_temp - data[ATTR_MAX_TEMP] = self.max_temp - return data + @property + def unit_of_measurement(self): + """ Unit of measurement this thermostat expresses itself in. """ + return NotImplementedError + @property def current_temperature(self): """ Returns the current temperature. """ @@ -171,9 +187,9 @@ class ThermostatDevice(Entity): @property def min_temp(self): """ Return minimum temperature. """ - return self.hass.config.temperature(7, TEMP_CELCIUS)[0] + return convert(7, TEMP_CELCIUS, self.unit_of_measurement) @property def max_temp(self): """ Return maxmum temperature. """ - return self.hass.config.temperature(35, TEMP_CELCIUS)[0] + return convert(35, TEMP_CELCIUS, self.unit_of_measurement) diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 54e7cc3e3a5..7eb3030f0d4 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -64,6 +64,7 @@ import homeassistant.components as core import homeassistant.util as util from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.helpers.event import track_state_change from homeassistant.const import TEMP_CELCIUS, STATE_ON, STATE_OFF TOL_TEMP = 0.3 @@ -108,12 +109,12 @@ class HeatControl(ThermostatDevice): self._away = False self._heater_manual_changed = True - hass.states.track_change(self.heater_entity_id, - self._heater_turned_on, - STATE_OFF, STATE_ON) - hass.states.track_change(self.heater_entity_id, - self._heater_turned_off, - STATE_ON, STATE_OFF) + track_state_change(hass, self.heater_entity_id, + self._heater_turned_on, + STATE_OFF, STATE_ON) + track_state_change(hass, self.heater_entity_id, + self._heater_turned_off, + STATE_ON, STATE_OFF) @property def name(self): diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index b2e48b96bcd..1de729b590d 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -6,7 +6,7 @@ import logging from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) -REQUIREMENTS = ['python-nest>=2.3.1'] +REQUIREMENTS = ['python-nest==2.4.0'] # pylint: disable=unused-argument @@ -50,11 +50,19 @@ class NestThermostat(ThermostatDevice): @property def name(self): """ Returns the name of the nest, if any. """ - return self.device.name + location = self.device.where + name = self.device.name + if location is None: + return name + else: + if name == '': + return location.capitalize() + else: + return location.capitalize() + '(' + name + ')' @property def unit_of_measurement(self): - """ Returns the unit of measurement. """ + """ Unit of measurement this thermostat expresses itself in. """ return TEMP_CELCIUS @property @@ -109,6 +117,24 @@ class NestThermostat(ThermostatDevice): """ Turns away off. """ self.structure.away = False + @property + def min_temp(self): + """ Identifies min_temp in Nest API or defaults if not available. """ + temp = self.device.away_temperature.low + if temp is None: + return super().min_temp + else: + return temp + + @property + def max_temp(self): + """ Identifies mxn_temp in Nest API or defaults if not available. """ + temp = self.device.away_temperature.high + if temp is None: + return super().max_temp + else: + return temp + def update(self): """ Python-nest has its own mechanism for staying up to date. """ pass diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py new file mode 100644 index 00000000000..d716c8c46ad --- /dev/null +++ b/homeassistant/components/verisure.py @@ -0,0 +1,180 @@ +""" +components.verisure +~~~~~~~~~~~~~~~~~~ + +Provides support for verisure components + +Configuration: + +verisure: + username: user@example.com + password: password + alarm: 1 + hygrometers: 0 + smartplugs: 1 + thermometers: 0 + + +Variables: + +username +*Required +Username to verisure mypages + +password +*Required +Password to verisure mypages + +alarm +*Opional +Set to 1 to show alarm, 0 to disable. Default 1 + +hygrometers +*Opional +Set to 1 to show hygrometers, 0 to disable. Default 1 + +smartplugs +*Opional +Set to 1 to show smartplugs, 0 to disable. Default 1 + +thermometers +*Opional +Set to 1 to show thermometers, 0 to disable. Default 1 +""" +import logging +from datetime import timedelta + +from homeassistant import bootstrap +from homeassistant.loader import get_component + +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.const import ( + EVENT_PLATFORM_DISCOVERED, + ATTR_SERVICE, ATTR_DISCOVERED, + CONF_USERNAME, CONF_PASSWORD) + + +DOMAIN = "verisure" +DISCOVER_SENSORS = 'verisure.sensors' +DISCOVER_SWITCHES = 'verisure.switches' + +DEPENDENCIES = [] +REQUIREMENTS = [ + 'https://github.com/persandstrom/python-verisure/archive/' + + '9873c4527f01b1ba1f72ae60f7f35854390d59be.zip' + ] + +_LOGGER = logging.getLogger(__name__) + +MY_PAGES = None +STATUS = {} + +VERISURE_LOGIN_ERROR = None +VERISURE_ERROR = None + +SHOW_THERMOMETERS = True +SHOW_HYGROMETERS = True +SHOW_ALARM = True +SHOW_SMARTPLUGS = True + +# if wrong password was given don't try again +WRONG_PASSWORD_GIVEN = False + +MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=5) + + +def setup(hass, config): + """ Setup the Verisure component. """ + + if not validate_config(config, + {DOMAIN: [CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return False + + from verisure import MyPages, LoginError, Error + + STATUS[MyPages.DEVICE_ALARM] = {} + STATUS[MyPages.DEVICE_CLIMATE] = {} + STATUS[MyPages.DEVICE_SMARTPLUG] = {} + + global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS + SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1')) + SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) + SHOW_ALARM = int(config[DOMAIN].get('alarm', '1')) + SHOW_SMARTPLUGS = int(config[DOMAIN].get('smartplugs', '1')) + + global MY_PAGES + MY_PAGES = MyPages( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD]) + global VERISURE_LOGIN_ERROR, VERISURE_ERROR + VERISURE_LOGIN_ERROR = LoginError + VERISURE_ERROR = Error + + try: + MY_PAGES.login() + except (ConnectionError, Error) as ex: + _LOGGER.error('Could not log in to verisure mypages, %s', ex) + return False + + update() + + # Load components for the devices in the ISY controller that we support + for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), + ('switch', DISCOVER_SWITCHES))): + component = get_component(comp_name) + _LOGGER.info(config[DOMAIN]) + bootstrap.setup_component(hass, component.DOMAIN, config) + + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: discovery, + ATTR_DISCOVERED: {}}) + + return True + + +def get_alarm_status(): + ''' return a list of status overviews for alarm components ''' + return STATUS[MY_PAGES.DEVICE_ALARM] + + +def get_climate_status(): + ''' return a list of status overviews for alarm components ''' + return STATUS[MY_PAGES.DEVICE_CLIMATE] + + +def get_smartplug_status(): + ''' return a list of status overviews for alarm components ''' + return STATUS[MY_PAGES.DEVICE_SMARTPLUG] + + +def reconnect(): + ''' reconnect to verisure mypages ''' + try: + MY_PAGES.login() + except VERISURE_LOGIN_ERROR as ex: + _LOGGER.error("Could not login to Verisure mypages, %s", ex) + global WRONG_PASSWORD_GIVEN + WRONG_PASSWORD_GIVEN = True + except (ConnectionError, VERISURE_ERROR) as ex: + _LOGGER.error("Could not login to Verisure mypages, %s", ex) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update(): + ''' Updates the status of verisure components ''' + if WRONG_PASSWORD_GIVEN: + # Is there any way to inform user? + return + + try: + for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_ALARM): + STATUS[MY_PAGES.DEVICE_ALARM][overview.id] = overview + for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_CLIMATE): + STATUS[MY_PAGES.DEVICE_CLIMATE][overview.id] = overview + for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_SMARTPLUG): + STATUS[MY_PAGES.DEVICE_SMARTPLUG][overview.id] = overview + except ConnectionError as ex: + _LOGGER.error('Caught connection error %s, tries to reconnect', ex) + reconnect() diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index d56a244b84c..eb2beac508a 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -16,8 +16,8 @@ from homeassistant.const import ( DOMAIN = "wink" DEPENDENCIES = [] -REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/master.zip' - '#pywink>=0.1'] +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index c5967ae0f34..ef7e7308959 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -1,7 +1,6 @@ """ homeassistant.components.zwave ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Connects Home Assistant to a Z-Wave network. """ from pprint import pprint @@ -13,7 +12,7 @@ from homeassistant.const import ( DOMAIN = "zwave" DEPENDENCIES = [] -REQUIREMENTS = ['pydispatcher>=2.0.5'] +REQUIREMENTS = ['pydispatcher==2.0.5'] CONF_USB_STICK_PATH = "usb_path" DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" diff --git a/homeassistant/config.py b/homeassistant/config.py index ec177776d8f..78da7f2a0d1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,20 +7,18 @@ Module to help with parsing and generating configuration files. import logging import os -from homeassistant import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) import homeassistant.util.location as loc_util - _LOGGER = logging.getLogger(__name__) - YAML_CONFIG_FILE = 'configuration.yaml' -CONF_CONFIG_FILE = 'home-assistant.conf' +CONFIG_DIR_NAME = '.homeassistant' -DEFAULT_CONFIG = [ +DEFAULT_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' 'running'), @@ -30,9 +28,23 @@ DEFAULT_CONFIG = [ (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celcius, F for Fahrenheit'), (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), -] -DEFAULT_COMPONENTS = [ - 'discovery', 'frontend', 'conversation', 'history', 'logbook', 'sun'] +) +DEFAULT_COMPONENTS = { + 'introduction': 'Show links to resources in log and frontend', + 'frontend': 'Enables the frontend', + 'discovery': 'Discover some devices automatically', + 'conversation': 'Allows you to issue voice commands from the frontend', + 'history': 'Enables support for tracking state changes over time.', + 'logbook': 'View all events in a logbook', + 'sun': 'Track the sun', +} + + +def get_default_config_dir(): + """ Put together the default configuration directory based on OS. """ + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + return os.path.join(data_dir, CONFIG_DIR_NAME) def ensure_config_exists(config_dir, detect_location=True): @@ -42,7 +54,8 @@ def ensure_config_exists(config_dir, detect_location=True): config_path = find_config_file(config_dir) if config_path is None: - _LOGGER.info("Unable to find configuration. Creating default one") + print("Unable to find configuration. Creating default one at", + config_dir) config_path = create_default_config(config_dir, detect_location) return config_path @@ -81,38 +94,27 @@ def create_default_config(config_dir, detect_location=True): config_file.write("\n") - for component in DEFAULT_COMPONENTS: + for component, description in DEFAULT_COMPONENTS.items(): + config_file.write("# {}\n".format(description)) config_file.write("{}:\n\n".format(component)) return config_path except IOError: - _LOGGER.exception( - 'Unable to write default configuration file %s', config_path) - + print('Unable to create default configuration file', config_path) return None def find_config_file(config_dir): """ Looks in given directory for supported config files. """ - for filename in (YAML_CONFIG_FILE, CONF_CONFIG_FILE): - config_path = os.path.join(config_dir, filename) + config_path = os.path.join(config_dir, YAML_CONFIG_FILE) - if os.path.isfile(config_path): - return config_path - - return None + return config_path if os.path.isfile(config_path) else None def load_config_file(config_path): """ Loads given config file. """ - config_ext = os.path.splitext(config_path)[1] - - if config_ext == '.yaml': - return load_yaml_config_file(config_path) - - elif config_ext == '.conf': - return load_conf_config_file(config_path) + return load_yaml_config_file(config_path) def load_yaml_config_file(config_path): @@ -120,17 +122,16 @@ def load_yaml_config_file(config_path): import yaml def parse(fname): - """ Actually parse the file. """ + """ Parse a YAML file. """ try: with open(fname) as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict - conf_dict = yaml.load(conf_file) or {} + return yaml.load(conf_file) or {} except yaml.YAMLError: - _LOGGER.exception('Error reading YAML configuration file %s', - fname) - raise HomeAssistantError() - return conf_dict + error = 'Error reading YAML configuration file {}'.format(fname) + _LOGGER.exception(error) + raise HomeAssistantError(error) def yaml_include(loader, node): """ @@ -153,21 +154,3 @@ def load_yaml_config_file(config_path): raise HomeAssistantError() return conf_dict - - -def load_conf_config_file(config_path): - """ Parse the old style conf configuration. """ - import configparser - - config_dict = {} - - config = configparser.ConfigParser() - config.read(config_path) - - for section in config.sections(): - config_dict[section] = {} - - for key, val in config.items(section): - config_dict[section][key] = val - - return config_dict diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d58dbb01d2..a3b9cc8d396 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,7 @@ """ Constants used by Home Assistant components. """ + +__version__ = "0.7.0" + # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' diff --git a/homeassistant/core.py b/homeassistant/core.py new file mode 100644 index 00000000000..c04e9a9ab63 --- /dev/null +++ b/homeassistant/core.py @@ -0,0 +1,788 @@ +""" +homeassistant +~~~~~~~~~~~~~ + +Home Assistant is a Home Automation framework for observing the state +of entities and react to changes. +""" + +import os +import time +import logging +import threading +import enum +import re +import functools as ft +from collections import namedtuple + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, + EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, + EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, + TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) +from homeassistant.exceptions import ( + HomeAssistantError, InvalidEntityFormatError) +import homeassistant.util as util +import homeassistant.util.dt as date_util +import homeassistant.helpers.temperature as temp_helper +from homeassistant.config import get_default_config_dir + +DOMAIN = "homeassistant" + +# How often time_changed event should fire +TIMER_INTERVAL = 1 # seconds + +# How long we wait for the result of a service call +SERVICE_CALL_LIMIT = 10 # seconds + +# Define number of MINIMUM worker threads. +# During bootstrap of HA (see bootstrap._setup_component()) worker threads +# will be added for each component that polls devices. +MIN_WORKER_THREAD = 2 + +# Pattern for validating entity IDs (format: .) +ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") + +_LOGGER = logging.getLogger(__name__) + +# Temporary to support deprecated methods +_MockHA = namedtuple("MockHomeAssistant", ['bus']) + + +class HomeAssistant(object): + """ Core class to route all communication to right components. """ + + def __init__(self): + self.pool = pool = create_worker_pool() + self.bus = EventBus(pool) + self.services = ServiceRegistry(self.bus, pool) + self.states = StateMachine(self.bus) + self.config = Config() + + def start(self): + """ Start home assistant. """ + _LOGGER.info( + "Starting Home Assistant (%d threads)", self.pool.worker_count) + + create_timer(self) + self.bus.fire(EVENT_HOMEASSISTANT_START) + + def block_till_stopped(self): + """ Will register service homeassistant/stop and + will block until called. """ + request_shutdown = threading.Event() + + def stop_homeassistant(service): + """ Stops Home Assistant. """ + request_shutdown.set() + + self.services.register( + DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) + + while not request_shutdown.isSet(): + try: + time.sleep(1) + except KeyboardInterrupt: + break + + self.stop() + + def stop(self): + """ Stops Home Assistant and shuts down all threads. """ + _LOGGER.info("Stopping") + + self.bus.fire(EVENT_HOMEASSISTANT_STOP) + + # Wait till all responses to homeassistant_stop are done + self.pool.block_till_done() + + self.pool.stop() + + def track_point_in_time(self, action, point_in_time): + """Deprecated method as of 8/4/2015 to track point in time.""" + _LOGGER.warning( + 'hass.track_point_in_time is deprecated. ' + 'Please use homeassistant.helpers.event.track_point_in_time') + import homeassistant.helpers.event as helper + helper.track_point_in_time(self, action, point_in_time) + + def track_point_in_utc_time(self, action, point_in_time): + """Deprecated method as of 8/4/2015 to track point in UTC time.""" + _LOGGER.warning( + 'hass.track_point_in_utc_time is deprecated. ' + 'Please use homeassistant.helpers.event.track_point_in_utc_time') + import homeassistant.helpers.event as helper + helper.track_point_in_utc_time(self, action, point_in_time) + + def track_utc_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None): + """Deprecated method as of 8/4/2015 to track UTC time change.""" + # pylint: disable=too-many-arguments + _LOGGER.warning( + 'hass.track_utc_time_change is deprecated. ' + 'Please use homeassistant.helpers.event.track_utc_time_change') + import homeassistant.helpers.event as helper + helper.track_utc_time_change(self, action, year, month, day, hour, + minute, second) + + def track_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None, utc=False): + """Deprecated method as of 8/4/2015 to track time change.""" + # pylint: disable=too-many-arguments + _LOGGER.warning( + 'hass.track_time_change is deprecated. ' + 'Please use homeassistant.helpers.event.track_time_change') + import homeassistant.helpers.event as helper + helper.track_time_change(self, action, year, month, day, hour, + minute, second) + + +class JobPriority(util.OrderedEnum): + """ Provides priorities for bus events. """ + # pylint: disable=no-init,too-few-public-methods + + EVENT_CALLBACK = 0 + EVENT_SERVICE = 1 + EVENT_STATE = 2 + EVENT_TIME = 3 + EVENT_DEFAULT = 4 + + @staticmethod + def from_event_type(event_type): + """ Returns a priority based on event type. """ + if event_type == EVENT_TIME_CHANGED: + return JobPriority.EVENT_TIME + elif event_type == EVENT_STATE_CHANGED: + return JobPriority.EVENT_STATE + elif event_type == EVENT_CALL_SERVICE: + return JobPriority.EVENT_SERVICE + elif event_type == EVENT_SERVICE_EXECUTED: + return JobPriority.EVENT_CALLBACK + else: + return JobPriority.EVENT_DEFAULT + + +class EventOrigin(enum.Enum): + """ Distinguish between origin of event. """ + # pylint: disable=no-init,too-few-public-methods + + local = "LOCAL" + remote = "REMOTE" + + def __str__(self): + return self.value + + +# pylint: disable=too-few-public-methods +class Event(object): + """ Represents an event within the Bus. """ + + __slots__ = ['event_type', 'data', 'origin', 'time_fired'] + + def __init__(self, event_type, data=None, origin=EventOrigin.local, + time_fired=None): + self.event_type = event_type + self.data = data or {} + self.origin = origin + self.time_fired = date_util.strip_microseconds( + time_fired or date_util.utcnow()) + + def as_dict(self): + """ Returns a dict representation of this Event. """ + return { + 'event_type': self.event_type, + 'data': dict(self.data), + 'origin': str(self.origin), + 'time_fired': date_util.datetime_to_str(self.time_fired), + } + + def __repr__(self): + # pylint: disable=maybe-no-member + if self.data: + return "".format( + self.event_type, str(self.origin)[0], + util.repr_helper(self.data)) + else: + return "".format(self.event_type, + str(self.origin)[0]) + + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self.event_type == other.event_type and + self.data == other.data and + self.origin == other.origin and + self.time_fired == other.time_fired) + + +class EventBus(object): + """ Class that allows different components to communicate via services + and events. + """ + + def __init__(self, pool=None): + self._listeners = {} + self._lock = threading.Lock() + self._pool = pool or create_worker_pool() + + @property + def listeners(self): + """ Dict with events that is being listened for and the number + of listeners. + """ + with self._lock: + return {key: len(self._listeners[key]) + for key in self._listeners} + + def fire(self, event_type, event_data=None, origin=EventOrigin.local): + """ Fire an event. """ + if not self._pool.running: + raise HomeAssistantError('Home Assistant has shut down.') + + with self._lock: + # Copy the list of the current listeners because some listeners + # remove themselves as a listener while being executed which + # causes the iterator to be confused. + get = self._listeners.get + listeners = get(MATCH_ALL, []) + get(event_type, []) + + event = Event(event_type, event_data, origin) + + if event_type != EVENT_TIME_CHANGED: + _LOGGER.info("Bus:Handling %s", event) + + if not listeners: + return + + job_priority = JobPriority.from_event_type(event_type) + + for func in listeners: + self._pool.add_job(job_priority, (func, event)) + + def listen(self, event_type, listener): + """ Listen for all events or events of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + """ + with self._lock: + if event_type in self._listeners: + self._listeners[event_type].append(listener) + else: + self._listeners[event_type] = [listener] + + def listen_once(self, event_type, listener): + """ Listen once for event of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + Returns registered listener that can be used with remove_listener. + """ + @ft.wraps(listener) + def onetime_listener(event): + """ Removes listener from eventbus and then fires listener. """ + if hasattr(onetime_listener, 'run'): + return + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. + # This will make sure the second time it does nothing. + onetime_listener.run = True + + self.remove_listener(event_type, onetime_listener) + + listener(event) + + self.listen(event_type, onetime_listener) + + return onetime_listener + + def remove_listener(self, event_type, listener): + """ Removes a listener of a specific event_type. """ + with self._lock: + try: + self._listeners[event_type].remove(listener) + + # delete event_type list if empty + if not self._listeners[event_type]: + self._listeners.pop(event_type) + + except (KeyError, ValueError): + # KeyError is key event_type listener did not exist + # ValueError if listener did not exist within event_type + pass + + +class State(object): + """ + Object to represent a state within the state machine. + + entity_id: the entity that is represented. + state: the state of the entity + attributes: extra information on entity and state + last_changed: last time the state was changed, not the attributes. + last_updated: last time this object was updated. + """ + + __slots__ = ['entity_id', 'state', 'attributes', + 'last_changed', 'last_updated'] + + # pylint: disable=too-many-arguments + def __init__(self, entity_id, state, attributes=None, last_changed=None, + last_updated=None): + if not ENTITY_ID_PATTERN.match(entity_id): + raise InvalidEntityFormatError(( + "Invalid entity id encountered: {}. " + "Format should be .").format(entity_id)) + + self.entity_id = entity_id.lower() + self.state = state + self.attributes = attributes or {} + self.last_updated = date_util.strip_microseconds( + last_updated or date_util.utcnow()) + + # Strip microsecond from last_changed else we cannot guarantee + # state == State.from_dict(state.as_dict()) + # This behavior occurs because to_dict uses datetime_to_str + # which does not preserve microseconds + self.last_changed = date_util.strip_microseconds( + last_changed or self.last_updated) + + @property + def domain(self): + """ Returns domain of this state. """ + return util.split_entity_id(self.entity_id)[0] + + @property + def object_id(self): + """ Returns object_id of this state. """ + return util.split_entity_id(self.entity_id)[1] + + @property + def name(self): + """ Name to represent this state. """ + return ( + self.attributes.get(ATTR_FRIENDLY_NAME) or + self.object_id.replace('_', ' ')) + + def copy(self): + """ Creates a copy of itself. """ + return State(self.entity_id, self.state, + dict(self.attributes), self.last_changed) + + def as_dict(self): + """ Converts State to a dict to be used within JSON. + Ensures: state == State.from_dict(state.as_dict()) """ + + return {'entity_id': self.entity_id, + 'state': self.state, + 'attributes': self.attributes, + 'last_changed': date_util.datetime_to_str(self.last_changed), + 'last_updated': date_util.datetime_to_str(self.last_updated)} + + @classmethod + def from_dict(cls, json_dict): + """ Static method to create a state from a dict. + Ensures: state == State.from_json_dict(state.to_json_dict()) """ + + if not (json_dict and + 'entity_id' in json_dict and + 'state' in json_dict): + return None + + last_changed = json_dict.get('last_changed') + + if last_changed: + last_changed = date_util.str_to_datetime(last_changed) + + last_updated = json_dict.get('last_updated') + + if last_updated: + last_updated = date_util.str_to_datetime(last_updated) + + return cls(json_dict['entity_id'], json_dict['state'], + json_dict.get('attributes'), last_changed, last_updated) + + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self.entity_id == other.entity_id and + self.state == other.state and + self.attributes == other.attributes) + + def __repr__(self): + attr = "; {}".format(util.repr_helper(self.attributes)) \ + if self.attributes else "" + + return "".format( + self.entity_id, self.state, attr, + date_util.datetime_to_local_str(self.last_changed)) + + +class StateMachine(object): + """ Helper class that tracks the state of different entities. """ + + def __init__(self, bus): + self._states = {} + self._bus = bus + self._lock = threading.Lock() + + def entity_ids(self, domain_filter=None): + """ List of entity ids that are being tracked. """ + if domain_filter is None: + return list(self._states.keys()) + + domain_filter = domain_filter.lower() + + return [state.entity_id for key, state + in self._states.items() + if util.split_entity_id(key)[0] == domain_filter] + + def all(self): + """ Returns a list of all states. """ + with self._lock: + return [state.copy() for state in self._states.values()] + + def get(self, entity_id): + """ Returns the state of the specified entity. """ + state = self._states.get(entity_id.lower()) + + # Make a copy so people won't mutate the state + return state.copy() if state else None + + def is_state(self, entity_id, state): + """ Returns True if entity exists and is specified state. """ + entity_id = entity_id.lower() + + return (entity_id in self._states and + self._states[entity_id].state == state) + + def remove(self, entity_id): + """ Removes an entity from the state machine. + + Returns boolean to indicate if an entity was removed. """ + entity_id = entity_id.lower() + + with self._lock: + return self._states.pop(entity_id, None) is not None + + def set(self, entity_id, new_state, attributes=None): + """ Set the state of an entity, add entity if it does not exist. + + Attributes is an optional dict to specify attributes of this state. + + If you just update the attributes and not the state, last changed will + not be affected. + """ + entity_id = entity_id.lower() + new_state = str(new_state) + attributes = attributes or {} + + with self._lock: + old_state = self._states.get(entity_id) + + is_existing = old_state is not None + same_state = is_existing and old_state.state == new_state + same_attr = is_existing and old_state.attributes == attributes + + if same_state and same_attr: + return + + # If state did not exist or is different, set it + last_changed = old_state.last_changed if same_state else None + + state = State(entity_id, new_state, attributes, last_changed) + self._states[entity_id] = state + + event_data = {'entity_id': entity_id, 'new_state': state} + + if old_state: + event_data['old_state'] = old_state + + self._bus.fire(EVENT_STATE_CHANGED, event_data) + + def track_change(self, entity_ids, action, from_state=None, to_state=None): + """ + DEPRECATED AS OF 8/4/2015 + """ + _LOGGER.warning( + 'hass.states.track_change is deprecated. ' + 'Use homeassistant.helpers.event.track_state_change instead.') + import homeassistant.helpers.event as helper + helper.track_state_change(_MockHA(self._bus), entity_ids, action, + from_state, to_state) + + +# pylint: disable=too-few-public-methods +class ServiceCall(object): + """ Represents a call to a service. """ + + __slots__ = ['domain', 'service', 'data'] + + def __init__(self, domain, service, data=None): + self.domain = domain + self.service = service + self.data = data or {} + + def __repr__(self): + if self.data: + return "".format( + self.domain, self.service, util.repr_helper(self.data)) + else: + return "".format(self.domain, self.service) + + +class ServiceRegistry(object): + """ Offers services over the eventbus. """ + + def __init__(self, bus, pool=None): + self._services = {} + self._lock = threading.Lock() + self._pool = pool or create_worker_pool() + self._bus = bus + self._cur_id = 0 + bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) + + @property + def services(self): + """ Dict with per domain a list of available services. """ + with self._lock: + return {domain: list(self._services[domain].keys()) + for domain in self._services} + + def has_service(self, domain, service): + """ Returns True if specified service exists. """ + return service in self._services.get(domain, []) + + def register(self, domain, service, service_func): + """ Register a service. """ + with self._lock: + if domain in self._services: + self._services[domain][service] = service_func + else: + self._services[domain] = {service: service_func} + + self._bus.fire( + EVENT_SERVICE_REGISTERED, + {ATTR_DOMAIN: domain, ATTR_SERVICE: service}) + + def call(self, domain, service, service_data=None, blocking=False): + """ + Calls specified service. + Specify blocking=True to wait till service is executed. + Waits a maximum of SERVICE_CALL_LIMIT. + + If blocking = True, will return boolean if service executed + succesfully within SERVICE_CALL_LIMIT. + + This method will fire an event to call the service. + This event will be picked up by this ServiceRegistry and any + other ServiceRegistry that is listening on the EventBus. + + Because the service is sent as an event you are not allowed to use + the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. + """ + call_id = self._generate_unique_id() + event_data = service_data or {} + event_data[ATTR_DOMAIN] = domain + event_data[ATTR_SERVICE] = service + event_data[ATTR_SERVICE_CALL_ID] = call_id + + if blocking: + executed_event = threading.Event() + + def service_executed(call): + """ + Called when a service is executed. + Will set the event if matches our service call. + """ + if call.data[ATTR_SERVICE_CALL_ID] == call_id: + executed_event.set() + + self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) + + self._bus.fire(EVENT_CALL_SERVICE, event_data) + + if blocking: + success = executed_event.wait(SERVICE_CALL_LIMIT) + self._bus.remove_listener( + EVENT_SERVICE_EXECUTED, service_executed) + return success + + def _event_to_service_call(self, event): + """ Calls a service from an event. """ + service_data = dict(event.data) + domain = service_data.pop(ATTR_DOMAIN, None) + service = service_data.pop(ATTR_SERVICE, None) + + if not self.has_service(domain, service): + return + + service_handler = self._services[domain][service] + service_call = ServiceCall(domain, service, service_data) + + # Add a job to the pool that calls _execute_service + self._pool.add_job(JobPriority.EVENT_SERVICE, + (self._execute_service, + (service_handler, service_call))) + + def _execute_service(self, service_and_call): + """ Executes a service and fires a SERVICE_EXECUTED event. """ + service, call = service_and_call + service(call) + + if ATTR_SERVICE_CALL_ID in call.data: + self._bus.fire( + EVENT_SERVICE_EXECUTED, + {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) + + def _generate_unique_id(self): + """ Generates a unique service call id. """ + self._cur_id += 1 + return "{}-{}".format(id(self), self._cur_id) + + +class Config(object): + """ Configuration settings for Home Assistant. """ + + # pylint: disable=too-many-instance-attributes + def __init__(self): + self.latitude = None + self.longitude = None + self.temperature_unit = None + self.location_name = None + self.time_zone = None + + # List of loaded components + self.components = [] + + # Remote.API object pointing at local API + self.api = None + + # Directory that holds the configuration + self.config_dir = get_default_config_dir() + + def path(self, *path): + """ Returns path to the file within the config dir. """ + return os.path.join(self.config_dir, *path) + + def temperature(self, value, unit): + """ Converts temperature to user preferred unit if set. """ + if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and + self.temperature_unit and unit != self.temperature_unit): + return value, unit + + try: + temp = float(value) + except ValueError: # Could not convert value to float + return value, unit + + return ( + round(temp_helper.convert(temp, unit, self.temperature_unit), 1), + self.temperature_unit) + + def as_dict(self): + """ Converts config to a dictionary. """ + time_zone = self.time_zone or date_util.UTC + + return { + 'latitude': self.latitude, + 'longitude': self.longitude, + 'temperature_unit': self.temperature_unit, + 'location_name': self.location_name, + 'time_zone': time_zone.zone, + 'components': self.components, + } + + +def create_timer(hass, interval=TIMER_INTERVAL): + """ Creates a timer. Timer will start on HOMEASSISTANT_START. """ + # We want to be able to fire every time a minute starts (seconds=0). + # We want this so other modules can use that to make sure they fire + # every minute. + assert 60 % interval == 0, "60 % TIMER_INTERVAL should be 0!" + + def timer(): + """Send an EVENT_TIME_CHANGED on interval.""" + stop_event = threading.Event() + + def stop_timer(event): + """Stop the timer.""" + stop_event.set() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) + + _LOGGER.info("Timer:starting") + + last_fired_on_second = -1 + + calc_now = date_util.utcnow + + while not stop_event.isSet(): + now = calc_now() + + # First check checks if we are not on a second matching the + # timer interval. Second check checks if we did not already fire + # this interval. + if now.second % interval or \ + now.second == last_fired_on_second: + + # Sleep till it is the next time that we have to fire an event. + # Aim for halfway through the second that fits TIMER_INTERVAL. + # If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds. + # This will yield the best results because time.sleep() is not + # 100% accurate because of non-realtime OS's + slp_seconds = interval - now.second % interval + \ + .5 - now.microsecond/1000000.0 + + time.sleep(slp_seconds) + + now = calc_now() + + last_fired_on_second = now.second + + # Event might have been set while sleeping + if not stop_event.isSet(): + try: + hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + except HomeAssistantError: + # HA raises error if firing event after it has shut down + break + + def start_timer(event): + """Start the timer.""" + thread = threading.Thread(target=timer) + thread.daemon = True + thread.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_timer) + + +def create_worker_pool(worker_count=MIN_WORKER_THREAD): + """ Creates a worker pool to be used. """ + + def job_handler(job): + """ Called whenever a job is available to do. """ + try: + func, arg = job + func(arg) + except Exception: # pylint: disable=broad-except + # Catch any exception our service/event_listener might throw + # We do not want to crash our ThreadPool + _LOGGER.exception("BusHandler:Exception doing job") + + def busy_callback(worker_count, current_jobs, pending_jobs_count): + """ Callback to be called when the pool queue gets too big. """ + + _LOGGER.warning( + "WorkerPool:All %d threads are busy and %d jobs pending", + worker_count, pending_jobs_count) + + for start, job in current_jobs: + _LOGGER.warning("WorkerPool:Current job from %s: %s", + date_util.datetime_to_local_str(start), job) + + return util.ThreadPool(job_handler, worker_count, busy_callback) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py new file mode 100644 index 00000000000..bd32d356670 --- /dev/null +++ b/homeassistant/exceptions.py @@ -0,0 +1,16 @@ +""" Exceptions used by Home Assistant """ + + +class HomeAssistantError(Exception): + """ General Home Assistant exception occured. """ + pass + + +class InvalidEntityFormatError(HomeAssistantError): + """ When an invalid formatted entity is encountered. """ + pass + + +class NoEntitySpecifiedError(HomeAssistantError): + """ When no entity is specified. """ + pass diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 086cddc35e2..286eed4654e 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -6,10 +6,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME) from homeassistant.util import ensure_unique_string, slugify -# Deprecated 3/5/2015 - Moved to homeassistant.helpers.entity -# pylint: disable=unused-import -from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa - def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): """ Generate a unique entity ID based on given entity IDs or used ids. """ diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py deleted file mode 100644 index 4c713693c43..00000000000 --- a/homeassistant/helpers/device.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Deprecated since 3/21/2015 - please use helpers.entity -""" -import logging - -# pylint: disable=unused-import -from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa - -logging.getLogger(__name__).warning( - 'This file is deprecated. Please use helpers.entity') diff --git a/homeassistant/helpers/device_component.py b/homeassistant/helpers/device_component.py deleted file mode 100644 index 248297a9694..00000000000 --- a/homeassistant/helpers/device_component.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Deprecated since 3/21/2015 - please use helpers.entity_component -""" -import logging - -# pylint: disable=unused-import -from .entity_component import EntityComponent as DeviceComponent # noqa - -logging.getLogger(__name__).warning( - 'This file is deprecated. Please use helpers.entity_component') diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f9751ffc14c..b29379049d3 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,7 +7,7 @@ Provides ABC for entities in HA. from collections import defaultdict -from homeassistant import NoEntitySpecifiedError +from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN, diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 381584fabb8..80084178fe0 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,6 +7,7 @@ Provides helpers for components that manage entities. from homeassistant.bootstrap import prepare_setup_platform from homeassistant.helpers import ( generate_entity_id, config_per_platform, extract_entity_ids) +from homeassistant.helpers.event import track_utc_time_change from homeassistant.components import group, discovery from homeassistant.const import ATTR_ENTITY_ID @@ -115,8 +116,8 @@ class EntityComponent(object): self.is_polling = True - self.hass.track_time_change( - self._update_entity_states, + track_utc_time_change( + self.hass, self._update_entity_states, second=range(0, 60, self.scan_interval)) def _setup_platform(self, platform_type, platform_config, @@ -135,22 +136,6 @@ class EntityComponent(object): self.hass, platform_config, self.add_entities, discovery_info) self.hass.config.components.append(platform_name) - - except AttributeError: - # AttributeError if setup_platform does not exist - # Support old deprecated method for now - 3/1/2015 - if hasattr(platform, 'get_devices'): - self.logger.warning( - 'Please upgrade %s to return new entities using ' - 'setup_platform. See %s/demo.py for an example.', - platform_name, self.domain) - self.add_entities( - platform.get_devices(self.hass, platform_config)) - - else: - self.logger.exception( - 'Error while setting up platform %s', platform_type) - except Exception: # pylint: disable=broad-except self.logger.exception( 'Error while setting up platform %s', platform_type) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py new file mode 100644 index 00000000000..60377fd1f5d --- /dev/null +++ b/homeassistant/helpers/event.py @@ -0,0 +1,163 @@ +""" +Helpers for listening to events +""" +import functools as ft + +from ..util import dt as dt_util +from ..const import ( + ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) + + +def track_state_change(hass, entity_ids, action, from_state=None, + to_state=None): + """ + Track specific state changes. + entity_ids, from_state and to_state can be string or list. + Use list to match multiple. + + Returns the listener that listens on the bus for EVENT_STATE_CHANGED. + Pass the return value into hass.bus.remove_listener to remove it. + """ + from_state = _process_match_param(from_state) + to_state = _process_match_param(to_state) + + # Ensure it is a lowercase list with entity ids we want to match on + if isinstance(entity_ids, str): + entity_ids = (entity_ids.lower(),) + else: + entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) + + @ft.wraps(action) + def state_change_listener(event): + """ The listener that listens for specific state changes. """ + if event.data['entity_id'] not in entity_ids: + return + + if 'old_state' in event.data: + old_state = event.data['old_state'].state + else: + old_state = None + + if _matcher(old_state, from_state) and \ + _matcher(event.data['new_state'].state, to_state): + + action(event.data['entity_id'], + event.data.get('old_state'), + event.data['new_state']) + + hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener) + + return state_change_listener + + +def track_point_in_time(hass, action, point_in_time): + """ + Adds a listener that fires once after a spefic point in time. + """ + utc_point_in_time = dt_util.as_utc(point_in_time) + + @ft.wraps(action) + def utc_converter(utc_now): + """ Converts passed in UTC now to local now. """ + action(dt_util.as_local(utc_now)) + + return track_point_in_utc_time(hass, utc_converter, utc_point_in_time) + + +def track_point_in_utc_time(hass, action, point_in_time): + """ + Adds a listener that fires once after a specific point in UTC time. + """ + # Ensure point_in_time is UTC + point_in_time = dt_util.as_utc(point_in_time) + + @ft.wraps(action) + def point_in_time_listener(event): + """ Listens for matching time_changed events. """ + now = event.data[ATTR_NOW] + + if now >= point_in_time and \ + not hasattr(point_in_time_listener, 'run'): + + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. This will make + # sure the second time it does nothing. + point_in_time_listener.run = True + + hass.bus.remove_listener(EVENT_TIME_CHANGED, + point_in_time_listener) + + action(now) + + hass.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) + return point_in_time_listener + + +# pylint: disable=too-many-arguments +def track_utc_time_change(hass, action, year=None, month=None, day=None, + hour=None, minute=None, second=None, local=False): + """ Adds a listener that will fire if time matches a pattern. """ + # We do not have to wrap the function with time pattern matching logic + # if no pattern given + if all(val is None for val in (year, month, day, hour, minute, second)): + @ft.wraps(action) + def time_change_listener(event): + """ Fires every time event that comes in. """ + action(event.data[ATTR_NOW]) + + hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener) + return time_change_listener + + pmp = _process_match_param + year, month, day = pmp(year), pmp(month), pmp(day) + hour, minute, second = pmp(hour), pmp(minute), pmp(second) + + @ft.wraps(action) + def pattern_time_change_listener(event): + """ Listens for matching time_changed events. """ + now = event.data[ATTR_NOW] + + if local: + now = dt_util.as_local(now) + + mat = _matcher + + if mat(now.year, year) and \ + mat(now.month, month) and \ + mat(now.day, day) and \ + mat(now.hour, hour) and \ + mat(now.minute, minute) and \ + mat(now.second, second): + + action(now) + + hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener) + return pattern_time_change_listener + + +# pylint: disable=too-many-arguments +def track_time_change(hass, action, year=None, month=None, day=None, + hour=None, minute=None, second=None): + """ Adds a listener that will fire if UTC time matches a pattern. """ + track_utc_time_change(hass, action, year, month, day, hour, minute, second, + local=True) + + +def _process_match_param(parameter): + """ Wraps parameter in a tuple if it is not one and returns it. """ + if parameter is None or parameter == MATCH_ALL: + return MATCH_ALL + elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): + return (parameter,) + else: + return tuple(parameter) + + +def _matcher(subject, pattern): + """ Returns True if subject matches the pattern. + + Pattern is either a tuple of allowed subjects or a `MATCH_ALL`. + """ + return MATCH_ALL == pattern or subject in pattern diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 18c68808e94..d87ee48930c 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -6,7 +6,7 @@ Helpers that help with state related things. """ import logging -from homeassistant import State +from homeassistant.core import State import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) @@ -30,7 +30,16 @@ class TrackStates(object): return self.states def __exit__(self, exc_type, exc_value, traceback): - self.states.extend(self.hass.states.get_since(self.now)) + self.states.extend(get_changed_since(self.hass.states.all(), self.now)) + + +def get_changed_since(states, utc_point_in_time): + """ + Returns all states that have been changed since utc_point_in_time. + """ + point_in_time = dt_util.strip_microseconds(utc_point_in_time) + + return [state for state in states if state.last_updated >= point_in_time] def reproduce_state(hass, states, blocking=False): diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py new file mode 100644 index 00000000000..eaf1f78d927 --- /dev/null +++ b/homeassistant/helpers/temperature.py @@ -0,0 +1,19 @@ +""" +homeassistant.helpers.temperature +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Methods to help handle temperature in Home Assistant. +""" + +from homeassistant.const import TEMP_CELCIUS +import homeassistant.util.temperature as temp_util + + +def convert(temperature, unit, to_unit): + """ Converts temperature to correct unit. """ + if unit == to_unit: + return temperature + elif unit == TEMP_CELCIUS: + return temp_util.celcius_to_fahrenheit(temperature) + + return temp_util.fahrenheit_to_celcius(temperature) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index d38e0df4465..7b755214252 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -61,11 +61,10 @@ def prepare(hass): # python components. If this assumption is not true, HA won't break, # just might output more errors. for fil in os.listdir(custom_path): - if os.path.isdir(os.path.join(custom_path, fil)): - if fil != '__pycache__': - AVAILABLE_COMPONENTS.append( - 'custom_components.{}'.format(fil)) - + if fil == '__pycache__': + continue + elif os.path.isdir(os.path.join(custom_path, fil)): + AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) else: # For files we will strip out .py extension AVAILABLE_COMPONENTS.append( @@ -167,9 +166,10 @@ def load_order_components(components): key=lambda order: 'group' in order): load_order.update(comp_load_order) - # Push recorder to first place in load order - if 'recorder' in load_order: - load_order.promote('recorder') + # Push some to first place in load order + for comp in ('recorder', 'introduction'): + if comp in load_order: + load_order.promote(comp) return load_order @@ -195,24 +195,24 @@ def _load_order_component(comp_name, load_order, loading): for dependency in component.DEPENDENCIES: # Check not already loaded - if dependency not in load_order: - # If we are already loading it, we have a circular dependency - if dependency in loading: - _LOGGER.error('Circular dependency detected: %s -> %s', - comp_name, dependency) + if dependency in load_order: + continue - return OrderedSet() + # If we are already loading it, we have a circular dependency + if dependency in loading: + _LOGGER.error('Circular dependency detected: %s -> %s', + comp_name, dependency) + return OrderedSet() - dep_load_order = _load_order_component( - dependency, load_order, loading) + dep_load_order = _load_order_component(dependency, load_order, loading) - # length == 0 means error loading dependency or children - if len(dep_load_order) == 0: - _LOGGER.error('Error loading %s dependency: %s', - comp_name, dependency) - return OrderedSet() + # length == 0 means error loading dependency or children + if len(dep_load_order) == 0: + _LOGGER.error('Error loading %s dependency: %s', + comp_name, dependency) + return OrderedSet() - load_order.update(dep_load_order) + load_order.update(dep_load_order) load_order.add(comp_name) loading.remove(comp_name) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index bd576f50e48..2193ede86e7 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -17,7 +17,8 @@ import urllib.parse import requests -import homeassistant as ha +import homeassistant.core as ha +from homeassistant.exceptions import HomeAssistantError import homeassistant.bootstrap as bootstrap from homeassistant.const import ( @@ -84,12 +85,12 @@ class API(object): except requests.exceptions.ConnectionError: _LOGGER.exception("Error connecting to server") - raise ha.HomeAssistantError("Error connecting to server") + raise HomeAssistantError("Error connecting to server") except requests.exceptions.Timeout: error = "Timeout when talking to {}".format(self.host) _LOGGER.exception(error) - raise ha.HomeAssistantError(error) + raise HomeAssistantError(error) def __repr__(self): return "API({}, {}, {})".format( @@ -102,7 +103,7 @@ class HomeAssistant(ha.HomeAssistant): def __init__(self, remote_api, local_api=None): if not remote_api.validate_api(): - raise ha.HomeAssistantError( + raise HomeAssistantError( "Remote API at {}:{} not valid: {}".format( remote_api.host, remote_api.port, remote_api.status)) @@ -120,10 +121,11 @@ class HomeAssistant(ha.HomeAssistant): def start(self): # Ensure a local API exists to connect with remote if self.config.api is None: - bootstrap.setup_component(self, 'http') - bootstrap.setup_component(self, 'api') + if not bootstrap.setup_component(self, 'api'): + raise HomeAssistantError( + 'Unable to setup local API to receive events') - ha.Timer(self) + ha.create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) @@ -131,7 +133,7 @@ class HomeAssistant(ha.HomeAssistant): # Setup that events from remote_api get forwarded to local_api # Do this after we fire START, otherwise HTTP is not started if not connect_remote_events(self.remote_api, self.config.api): - raise ha.HomeAssistantError(( + raise HomeAssistantError(( 'Could not setup event forwarding from api {} to ' 'local api {}').format(self.remote_api, self.config.api)) @@ -292,7 +294,7 @@ def validate_api(api): else: return APIStatus.UNKNOWN - except ha.HomeAssistantError: + except HomeAssistantError: return APIStatus.CANNOT_CONNECT @@ -317,7 +319,7 @@ def connect_remote_events(from_api, to_api): return False - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error setting up event forwarding") return False @@ -341,7 +343,7 @@ def disconnect_remote_events(from_api, to_api): return False - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error removing an event forwarder") return False @@ -353,7 +355,7 @@ def get_event_listeners(api): return req.json() if req.status_code == 200 else {} - except (ha.HomeAssistantError, ValueError): + except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json _LOGGER.exception("Unexpected result retrieving event listeners") @@ -370,7 +372,7 @@ def fire_event(api, event_type, data=None): _LOGGER.error("Error firing event: %d - %d", req.status_code, req.text) - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error firing event") @@ -386,7 +388,7 @@ def get_state(api, entity_id): return ha.State.from_dict(req.json()) \ if req.status_code == 200 else None - except (ha.HomeAssistantError, ValueError): + except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json _LOGGER.exception("Error fetching state") @@ -403,7 +405,7 @@ def get_states(api): return [ha.State.from_dict(item) for item in req.json()] - except (ha.HomeAssistantError, ValueError, AttributeError): + except (HomeAssistantError, ValueError, AttributeError): # ValueError if req.json() can't parse the json _LOGGER.exception("Error fetching states") @@ -433,7 +435,7 @@ def set_state(api, entity_id, new_state, attributes=None): else: return True - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error setting state") return False @@ -456,7 +458,7 @@ def get_services(api): return req.json() if req.status_code == 200 else {} - except (ha.HomeAssistantError, ValueError): + except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json _LOGGER.exception("Got unexpected services result") @@ -474,5 +476,5 @@ def call_service(api, domain, service, service_data=None): _LOGGER.error("Error calling service: %d - %s", req.status_code, req.text) - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error calling service") diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 90476088d25..2e399384e63 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -16,11 +16,7 @@ import random import string from functools import wraps -# DEPRECATED AS OF 4/27/2015 - moved to homeassistant.util.dt package -# pylint: disable=unused-import -from .dt import ( # noqa - datetime_to_str, str_to_datetime, strip_microseconds, - datetime_to_local_str, utcnow) +from .dt import datetime_to_local_str, utcnow RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') @@ -94,13 +90,12 @@ def get_local_ip(): # Use Google Public DNS server to determine own IP sock.connect(('8.8.8.8', 80)) - ip_addr = sock.getsockname()[0] - sock.close() - - return ip_addr + return sock.getsockname()[0] except socket.error: return socket.gethostbyname(socket.gethostname()) + finally: + sock.close() # Taken from http://stackoverflow.com/a/23728630 diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index d220a5a7e61..802e3834b90 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,22 +1,18 @@ """Helpers to install PyPi packages.""" +import os import subprocess import sys -from . import environment as env -# If we are not in a virtual environment, install in user space -INSTALL_USER = not env.is_virtual() - - -def install_package(package, upgrade=False, user=INSTALL_USER): +def install_package(package, upgrade=False, target=None): """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successfull.""" # Not using 'import pip; pip.main([])' because it breaks the logger args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] if upgrade: args.append('--upgrade') - if user: - args.append('--user') + if target: + args += ['--target', os.path.abspath(target)] try: return 0 == subprocess.call(args) except subprocess.SubprocessError: diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py new file mode 100644 index 00000000000..658639aae55 --- /dev/null +++ b/homeassistant/util/temperature.py @@ -0,0 +1,16 @@ +""" +homeassistant.util.temperature +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Temperature util functions. +""" + + +def fahrenheit_to_celcius(fahrenheit): + """ Convert a Fahrenheit temperature to Celcius. """ + return (fahrenheit - 32.0) / 1.8 + + +def celcius_to_fahrenheit(celcius): + """ Convert a Celcius temperature to Fahrenheit. """ + return celcius * 1.8 + 32.0 diff --git a/pylintrc b/pylintrc index 54b1f80cdc5..888fb50ee0f 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,5 @@ [MASTER] -ignore=external +ignore=external,setup.py reports=no # Reasons disabled: diff --git a/requirements.txt b/requirements.txt index f0f49e5a248..1b7d2396971 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,93 +1,4 @@ -# Required for Home Assistant core -requests>=2.0 -pyyaml>=3.11 -pytz>=2015.2 - -# Optional, needed for specific components - -# Sun (sun) -astral>=0.8.1 - -# Philips Hue library (lights.hue) -phue>=0.8 - -# Limitlessled/Easybulb/Milight library (lights.limitlessled) -ledcontroller>=1.0.7 - -# Chromecast bindings (media_player.cast) -pychromecast>=0.6.9 - -# Keyboard (keyboard) -pyuserinput>=0.1.9 - -# Tellstick bindings (*.tellstick) -tellcore-py>=1.0.4 - -# Nmap bindings (device_tracker.nmap) -python-libnmap>=0.6.2 - -# PushBullet bindings (notify.pushbullet) -pushbullet.py>=0.7.1 - -# Nest Thermostat bindings (thermostat.nest) -python-nest>=2.3.1 - -# Z-Wave (*.zwave) -pydispatcher>=2.0.5 - -# ISY994 bindings (*.isy994) -PyISY>=1.0.5 - -# PSutil (sensor.systemmonitor) -psutil>=3.0.0 - -# Pushover bindings (notify.pushover) -python-pushover>=0.2 - -# Transmission Torrent Client (*.transmission) -transmissionrpc>=0.11 - -# OpenWeatherMap Web API (sensor.openweathermap) -pyowm>=2.2.1 - -# XMPP Bindings (notify.xmpp) -sleekxmpp>=1.3.1 - -# Blockchain (sensor.bitcoin) -blockchain>=1.1.2 - -# MPD Bindings (media_player.mpd) -python-mpd2>=0.5.4 - -# Hikvision (switch.hikvisioncam) -hikvision>=0.4 - -# console log coloring -colorlog>=2.6.0 - -# JSON-RPC interface (media_player.kodi) -jsonrpc-requests>=0.1 - -# Forecast.io Bindings (sensor.forecast) -python-forecastio>=1.3.3 - -# Firmata Bindings (*.arduino) -PyMata==2.07a - -#Rfxtrx sensor -https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip - -# Mysensors -https://github.com/theolind/pymysensors/archive/master.zip#egg=pymysensors-0.1 - -# Netgear (device_tracker.netgear) -pynetgear>=0.1 - -# Netdisco (discovery) -netdisco>=0.1 - -# Wemo (switch.wemo) -pywemo>=0.1 - -# Wink (*.wink) -https://github.com/balloob/python-wink/archive/master.zip#pywink>=0.1 +requests>=2,<3 +pyyaml>=3.11,<4 +pytz>=2015.4 +pip>=7.0.0 diff --git a/requirements_all.txt b/requirements_all.txt new file mode 100644 index 00000000000..9798a95f030 --- /dev/null +++ b/requirements_all.txt @@ -0,0 +1,121 @@ +# Required for Home Assistant core +requests>=2,<3 +pyyaml>=3.11,<4 +pytz>=2015.4 +pip>=7.0.0 + +# Optional, needed for specific components + +# Sun (sun) +astral==0.8.1 + +# Philips Hue library (lights.hue) +phue==0.8 + +# Limitlessled/Easybulb/Milight library (lights.limitlessled) +ledcontroller==1.0.7 + +# Chromecast bindings (media_player.cast) +pychromecast==0.6.12 + +# Keyboard (keyboard) +pyuserinput==0.1.9 + +# Tellstick bindings (*.tellstick) +tellcore-py==1.0.4 + +# Nmap bindings (device_tracker.nmap) +python-libnmap==0.6.3 + +# PushBullet bindings (notify.pushbullet) +pushbullet.py==0.7.1 + +# Nest Thermostat bindings (thermostat.nest) +python-nest==2.4.0 + +# Z-Wave (*.zwave) +pydispatcher==2.0.5 + +# ISY994 bindings (*.isy994) +PyISY==1.0.5 + +# PSutil (sensor.systemmonitor) +psutil==3.0.0 + +# Pushover bindings (notify.pushover) +python-pushover==0.2 + +# Transmission Torrent Client (*.transmission) +transmissionrpc==0.11 + +# OpenWeatherMap Web API (sensor.openweathermap) +pyowm==2.2.1 + +# XMPP Bindings (notify.xmpp) +sleekxmpp==1.3.1 +dnspython3==1.12.0 + +# Blockchain (sensor.bitcoin) +blockchain==1.1.2 + +# MPD Bindings (media_player.mpd) +python-mpd2==0.5.4 + +# Hikvision (switch.hikvisioncam) +hikvision==0.4 + +# console log coloring +colorlog==2.6.0 + +# JSON-RPC interface (media_player.kodi) +jsonrpc-requests==0.1 + +# Forecast.io Bindings (sensor.forecast) +python-forecastio==1.3.3 + +# Firmata Bindings (*.arduino) +PyMata==2.07a + +# Rfxtrx sensor (sensor.rfxtrx) +https://github.com/Danielhiversen/pyRFXtrx/archive/ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip + +# Mysensors +https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75e6cb4af7.zip + +# Netgear (device_tracker.netgear) +pynetgear==0.3 + +# Netdisco (discovery) +netdisco==0.3 + +# Wemo (switch.wemo) +pywemo==0.2 + +# Wink (*.wink) +https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip + +# Slack notifier (notify.slack) +slacker==0.6.8 + +# Temper sensors (sensor.temper) +https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip + +# PyEdimax +https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip + +# RPI-GPIO platform (*.rpi_gpio) +# Uncomment for Raspberry Pi +# RPi.GPIO ==0.5.11 + +# Adafruit temperature/humidity sensor +# uncomment on a Raspberry Pi / Beaglebone +# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip + +# PAHO MQTT Binding (mqtt) +paho-mqtt==1.1 + +# PyModbus (modbus) +https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip + +# Verisure (verisure) +https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip diff --git a/scripts/homeassistant.daemon b/scripts/homeassistant.daemon index bef4cd90f4a..4dd6b37a9c5 100755 --- a/scripts/homeassistant.daemon +++ b/scripts/homeassistant.daemon @@ -79,7 +79,7 @@ case "$1" in uninstall) uninstall ;; - retart) + restart) stop start ;; diff --git a/scripts/update b/scripts/update index 7f2b59147bd..afeacbb1235 100755 --- a/scripts/update +++ b/scripts/update @@ -3,6 +3,4 @@ if [ ${PWD##*/} == "scripts" ]; then cd .. fi -git pull --recurse-submodules=yes -git submodule update --init --recursive -python3 -m pip install -r requirements.txt +git pull diff --git a/setup.py b/setup.py new file mode 100755 index 00000000000..337b3767c1e --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +import os +import re +from setuptools import setup, find_packages + +PACKAGE_NAME = 'homeassistant' +HERE = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(HERE, PACKAGE_NAME, 'const.py')) as fp: + VERSION = re.search("__version__ = ['\"]([^']+)['\"]\n", fp.read()).group(1) +DOWNLOAD_URL = \ + 'https://github.com/balloob/home-assistant/archive/{}.zip'.format(VERSION) + +PACKAGES = find_packages() + \ + ['homeassistant.external', 'homeassistant.external.noop', + 'homeassistant.external.nzbclients', 'homeassistant.external.vera'] + +PACKAGE_DATA = \ + {'homeassistant.components.frontend': ['index.html.template'], + 'homeassistant.components.frontend.www_static': ['*.*'], + 'homeassistant.components.frontend.www_static.images': ['*.*']} + +REQUIRES = \ + [line.strip() for line in open('requirements.txt', 'r')] + +setup( + name=PACKAGE_NAME, + version=VERSION, + license='MIT License', + url='https://home-assistant.io/', + download_url=DOWNLOAD_URL, + author='Paulus Schoutsen', + author_email='paulus@paulusschoutsen.nl', + description='Open-source home automation platform running on Python 3.', + packages=PACKAGES, + include_package_data=True, + package_data=PACKAGE_DATA, + zip_safe=False, + platforms='any', + install_requires=REQUIRES, + keywords=['home', 'automation'], + entry_points={ + 'console_scripts': [ + 'hass = homeassistant.__main__:main' + ] + }, + classifiers=[ + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.4', + 'Topic :: Home Automation' + ] +) diff --git a/tests/helpers.py b/tests/common.py similarity index 71% rename from tests/helpers.py rename to tests/common.py index 65fd0659108..be6aa623a25 100644 --- a/tests/helpers.py +++ b/tests/common.py @@ -6,14 +6,16 @@ Helper method for writing tests. """ import os from datetime import timedelta +from unittest import mock -import homeassistant as ha +import homeassistant.core as ha +import homeassistant.util.location as location_util import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED) -from homeassistant.components import sun +from homeassistant.components import sun, mqtt def get_test_config_dir(): @@ -39,6 +41,23 @@ def get_test_home_assistant(num_threads=None): return hass +def mock_detect_location_info(): + """ Mock implementation of util.detect_location_info. """ + return location_util.LocationInfo( + ip='1.1.1.1', + country_code='US', + country_name='United States', + region_code='CA', + region_name='California', + city='San Diego', + zip_code='92122', + time_zone='America/Los_Angeles', + latitude='2.0', + longitude='1.0', + use_fahrenheit=True, + ) + + def mock_service(hass, domain, service): """ Sets up a fake service. @@ -52,30 +71,36 @@ def mock_service(hass, domain, service): return calls +def fire_mqtt_message(hass, topic, payload, qos=0): + hass.bus.fire(mqtt.EVENT_MQTT_MESSAGE_RECEIVED, { + mqtt.ATTR_TOPIC: topic, + mqtt.ATTR_PAYLOAD: payload, + mqtt.ATTR_QOS: qos, + }) + + +def fire_time_changed(hass, time): + hass.bus.fire(EVENT_TIME_CHANGED, {'now': time}) + + def trigger_device_tracker_scan(hass): """ Triggers the device tracker to scan. """ - hass.bus.fire( - EVENT_TIME_CHANGED, - {'now': - dt_util.utcnow().replace(second=0) + timedelta(hours=1)}) + fire_time_changed( + hass, dt_util.utcnow().replace(second=0) + timedelta(hours=1)) def ensure_sun_risen(hass): """ Trigger sun to rise if below horizon. """ - if not sun.is_on(hass): - hass.bus.fire( - EVENT_TIME_CHANGED, - {'now': - sun.next_rising_utc(hass) + timedelta(seconds=10)}) + if sun.is_on(hass): + return + fire_time_changed(hass, sun.next_rising_utc(hass) + timedelta(seconds=10)) def ensure_sun_set(hass): """ Trigger sun to set if above horizon. """ - if sun.is_on(hass): - hass.bus.fire( - EVENT_TIME_CHANGED, - {'now': - sun.next_setting_utc(hass) + timedelta(seconds=10)}) + if not sun.is_on(hass): + return + fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10)) def mock_state_change_event(hass, new_state, old_state=None): @@ -95,6 +120,16 @@ def mock_http_component(hass): hass.config.components.append('http') +def mock_mqtt_component(hass): + with mock.patch('homeassistant.components.mqtt.MQTT'): + mqtt.setup(hass, { + mqtt.DOMAIN: { + mqtt.CONF_BROKER: 'mock-broker', + } + }) + hass.config.components.append(mqtt.DOMAIN) + + class MockHTTP(object): """ Mocks the HTTP module. """ diff --git a/tests/components/__init__.py b/tests/components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/automation/__init__.py b/tests/components/automation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py new file mode 100644 index 00000000000..a2c36283c9a --- /dev/null +++ b/tests/components/automation/test_event.py @@ -0,0 +1,78 @@ +""" +tests.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.automation as automation +import homeassistant.components.automation.event as event +from homeassistant.const import CONF_PLATFORM + + +class TestAutomationEvent(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_fails_setup_if_no_event_type(self): + self.assertFalse(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + automation.CONF_SERVICE: 'test.automation' + } + })) + + def test_if_fires_on_event(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_event_with_data(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + event.CONF_EVENT_DATA: {'some_attr': 'some_value'}, + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.bus.fire('test_event', {'some_attr': 'some_value'}) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_if_event_data_not_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + event.CONF_EVENT_DATA: {'some_attr': 'some_value'}, + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.bus.fire('test_event', {'some_attr': 'some_other_value'}) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py new file mode 100644 index 00000000000..507c37dc20a --- /dev/null +++ b/tests/components/automation/test_init.py @@ -0,0 +1,95 @@ +""" +tests.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.automation as automation +import homeassistant.components.automation.event as event +from homeassistant.const import CONF_PLATFORM, ATTR_ENTITY_ID + + +class TestAutomationEvent(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setup_fails_if_unknown_platform(self): + self.assertFalse(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'i_do_not_exist' + } + })) + + def test_service_data_not_a_dict(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_SERVICE_DATA: 100 + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_service_specify_data(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_SERVICE_DATA: {'some': 'data'} + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('data', self.calls[0].data['some']) + + def test_service_specify_entity_id(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_SERVICE_ENTITY_ID: 'hello.world' + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual(['hello.world'], self.calls[0].data[ATTR_ENTITY_ID]) + + def test_service_specify_entity_id_list(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_SERVICE_ENTITY_ID: ['hello.world', 'hello.world2'] + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual(['hello.world', 'hello.world2'], self.calls[0].data[ATTR_ENTITY_ID]) diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py new file mode 100644 index 00000000000..9402b5300b6 --- /dev/null +++ b/tests/components/automation/test_mqtt.py @@ -0,0 +1,81 @@ +""" +tests.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.automation as automation +import homeassistant.components.automation.mqtt as mqtt +from homeassistant.const import CONF_PLATFORM + +from tests.common import mock_mqtt_component, fire_mqtt_message + + +class TestAutomationState(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + mock_mqtt_component(self.hass) + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setup_fails_if_no_topic(self): + self.assertFalse(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'mqtt', + automation.CONF_SERVICE: 'test.automation' + } + })) + + def test_if_fires_on_topic_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'mqtt', + mqtt.CONF_TOPIC: 'test-topic', + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_mqtt_message(self.hass, 'test-topic', '') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_topic_and_payload_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'mqtt', + mqtt.CONF_TOPIC: 'test-topic', + mqtt.CONF_PAYLOAD: 'hello', + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_mqtt_message(self.hass, 'test-topic', 'hello') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_topic_but_no_payload_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'mqtt', + mqtt.CONF_TOPIC: 'test-topic', + mqtt.CONF_PAYLOAD: 'hello', + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_mqtt_message(self.hass, 'test-topic', 'no-hello') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py new file mode 100644 index 00000000000..47d612cbb02 --- /dev/null +++ b/tests/components/automation/test_state.py @@ -0,0 +1,139 @@ +""" +tests.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.automation as automation +import homeassistant.components.automation.state as state +from homeassistant.const import CONF_PLATFORM + + +class TestAutomationState(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.hass.states.set('test.entity', 'hello') + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setup_fails_if_no_entity_id(self): + self.assertFalse(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + automation.CONF_SERVICE: 'test.automation' + } + })) + + def test_if_fires_on_entity_change(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.entity', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_with_from_filter(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.entity', + state.CONF_FROM: 'hello', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_with_to_filter(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.entity', + state.CONF_TO: 'world', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_with_both_filters(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.entity', + state.CONF_FROM: 'hello', + state.CONF_TO: 'world', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_if_to_filter_not_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.entity', + state.CONF_FROM: 'hello', + state.CONF_TO: 'world', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'moon') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_if_from_filter_not_match(self): + self.hass.states.set('test.entity', 'bye') + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.entity', + state.CONF_FROM: 'hello', + state.CONF_TO: 'world', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_if_entity_not_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: 'test.another_entity', + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py new file mode 100644 index 00000000000..0f11a2a67c5 --- /dev/null +++ b/tests/components/automation/test_time.py @@ -0,0 +1,97 @@ +""" +tests.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.loader as loader +import homeassistant.util.dt as dt_util +import homeassistant.components.automation as automation +import homeassistant.components.automation.time as time +from homeassistant.const import CONF_PLATFORM + +from tests.common import fire_time_changed + + +class TestAutomationTime(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + loader.prepare(self.hass) + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_if_fires_when_hour_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'time', + time.CONF_HOURS: 0, + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_when_minute_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'time', + time.CONF_MINUTES: 0, + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace(minute=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_when_second_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'time', + time.CONF_SECONDS: 0, + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_when_all_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'time', + time.CONF_HOURS: 0, + time.CONF_MINUTES: 0, + time.CONF_SECONDS: 0, + automation.CONF_SERVICE: 'test.automation' + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace( + hour=0, minute=0, second=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) diff --git a/tests/test_component_api.py b/tests/components/test_api.py similarity index 99% rename from tests/test_component_api.py rename to tests/components/test_api.py index ff25b476d32..93b1cd06abe 100644 --- a/tests/test_component_api.py +++ b/tests/components/test_api.py @@ -10,7 +10,7 @@ import json import requests -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http diff --git a/tests/test_component_configurator.py b/tests/components/test_configurator.py similarity index 99% rename from tests/test_component_configurator.py rename to tests/components/test_configurator.py index c64fc39e50a..f41a5319ffd 100644 --- a/tests/test_component_configurator.py +++ b/tests/components/test_configurator.py @@ -7,7 +7,7 @@ Tests Configurator component. # pylint: disable=too-many-public-methods,protected-access import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.configurator as configurator from homeassistant.const import EVENT_TIME_CHANGED diff --git a/tests/test_component_demo.py b/tests/components/test_demo.py similarity index 92% rename from tests/test_component_demo.py rename to tests/components/test_demo.py index d7a64167622..0abd546e4c4 100644 --- a/tests/test_component_demo.py +++ b/tests/components/test_demo.py @@ -6,10 +6,10 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.demo as demo -from helpers import mock_http_component +from tests.common import mock_http_component class TestDemo(unittest.TestCase): diff --git a/tests/test_component_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py similarity index 99% rename from tests/test_component_device_sun_light_trigger.py rename to tests/components/test_device_sun_light_trigger.py index 05452a830ec..05b2bf11bea 100644 --- a/tests/test_component_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -14,7 +14,7 @@ from homeassistant.components import ( device_tracker, light, sun, device_sun_light_trigger) -from helpers import ( +from tests.common import ( get_test_home_assistant, ensure_sun_risen, ensure_sun_set, trigger_device_tracker_scan) diff --git a/tests/test_component_device_tracker.py b/tests/components/test_device_tracker.py similarity index 98% rename from tests/test_component_device_tracker.py rename to tests/components/test_device_tracker.py index 143c28c9cdb..66fd97c4730 100644 --- a/tests/test_component_device_tracker.py +++ b/tests/components/test_device_tracker.py @@ -10,7 +10,7 @@ from datetime import timedelta import logging import os -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader import homeassistant.util.dt as dt_util from homeassistant.const import ( @@ -18,7 +18,7 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME) import homeassistant.components.device_tracker as device_tracker -from helpers import get_test_home_assistant +from tests.common import get_test_home_assistant def setUpModule(): # pylint: disable=invalid-name diff --git a/tests/test_component_frontend.py b/tests/components/test_frontend.py similarity index 98% rename from tests/test_component_frontend.py rename to tests/components/test_frontend.py index d6431f5f5df..65fcb5b6091 100644 --- a/tests/test_component_frontend.py +++ b/tests/components/test_frontend.py @@ -10,7 +10,7 @@ import unittest import requests -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.components.http as http from homeassistant.const import HTTP_HEADER_HA_AUTH diff --git a/tests/test_component_group.py b/tests/components/test_group.py similarity index 97% rename from tests/test_component_group.py rename to tests/components/test_group.py index a476efdeea0..d66a24606a3 100644 --- a/tests/test_component_group.py +++ b/tests/components/test_group.py @@ -8,7 +8,7 @@ Tests the group compoments. import unittest import logging -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN import homeassistant.components.group as group @@ -199,8 +199,7 @@ class TestComponentsGroup(unittest.TestCase): self.hass, { group.DOMAIN: { - 'second_group': ','.join((self.group_entity_id, - 'light.Bowl')) + 'second_group': self.group_entity_id + ',light.Bowl' } })) diff --git a/tests/test_component_history.py b/tests/components/test_history.py similarity index 98% rename from tests/test_component_history.py rename to tests/components/test_history.py index 7ad657f54b5..12d10c52744 100644 --- a/tests/test_component_history.py +++ b/tests/components/test_history.py @@ -9,11 +9,11 @@ import time import os import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder -from helpers import ( +from tests.common import ( mock_http_component, mock_state_change_event, get_test_home_assistant) diff --git a/tests/test_component_core.py b/tests/components/test_init.py similarity index 98% rename from tests/test_component_core.py rename to tests/components/test_init.py index 8c00616bbb4..0074b75e148 100644 --- a/tests/test_component_core.py +++ b/tests/components/test_init.py @@ -7,7 +7,7 @@ Tests core compoments. # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) diff --git a/tests/test_component_light.py b/tests/components/test_light.py similarity index 99% rename from tests/test_component_light.py rename to tests/components/test_light.py index 07f8e8e14c9..e56dcbb02ad 100644 --- a/tests/test_component_light.py +++ b/tests/components/test_light.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components.light as light -from helpers import mock_service, get_test_home_assistant +from tests.common import mock_service, get_test_home_assistant class TestLight(unittest.TestCase): diff --git a/tests/test_component_logbook.py b/tests/components/test_logbook.py similarity index 96% rename from tests/test_component_logbook.py rename to tests/components/test_logbook.py index 33b7b5f915b..16f6ba8aa33 100644 --- a/tests/test_component_logbook.py +++ b/tests/components/test_logbook.py @@ -8,13 +8,13 @@ Tests the logbook component. import unittest from datetime import timedelta -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.util.dt as dt_util from homeassistant.components import logbook -from helpers import get_test_home_assistant, mock_http_component +from tests.common import get_test_home_assistant, mock_http_component class TestComponentHistory(unittest.TestCase): diff --git a/tests/test_component_media_player.py b/tests/components/test_media_player.py similarity index 97% rename from tests/test_component_media_player.py rename to tests/components/test_media_player.py index b7f0b847e80..1fd406dc026 100644 --- a/tests/test_component_media_player.py +++ b/tests/components/test_media_player.py @@ -8,14 +8,14 @@ Tests media_player component. import logging import unittest -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import ( STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) import homeassistant.components.media_player as media_player -from helpers import mock_service +from tests.common import mock_service def setUpModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_mqtt.py b/tests/components/test_mqtt.py new file mode 100644 index 00000000000..4c3dbb1d20a --- /dev/null +++ b/tests/components/test_mqtt.py @@ -0,0 +1,138 @@ +""" +tests.test_component_mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests MQTT component. +""" +import unittest +from unittest import mock +import socket + +import homeassistant.components.mqtt as mqtt +from homeassistant.const import ( + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) + +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) + + +class TestDemo(unittest.TestCase): + """ Test the demo module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant(1) + mock_mqtt_component(self.hass) + self.calls = [] + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def record_calls(self, *args): + self.calls.append(args) + + def test_client_starts_on_home_assistant_start(self): + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.pool.block_till_done() + self.assertTrue(mqtt.MQTT_CLIENT.start.called) + + def test_client_stops_on_home_assistant_start(self): + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.pool.block_till_done() + self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + self.hass.pool.block_till_done() + self.assertTrue(mqtt.MQTT_CLIENT.stop.called) + + def test_setup_fails_if_no_broker_config(self): + self.assertFalse(mqtt.setup(self.hass, {mqtt.DOMAIN: {}})) + + def test_setup_fails_if_no_connect_broker(self): + with mock.patch('homeassistant.components.mqtt.MQTT', + side_effect=socket.error()): + self.assertFalse(mqtt.setup(self.hass, {mqtt.DOMAIN: { + mqtt.CONF_BROKER: 'test-broker', + }})) + + def test_publish_calls_service(self): + self.hass.bus.listen_once(EVENT_CALL_SERVICE, self.record_calls) + + mqtt.publish(self.hass, 'test-topic', 'test-payload') + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + self.assertEqual('test-topic', self.calls[0][0].data[mqtt.ATTR_TOPIC]) + self.assertEqual('test-payload', self.calls[0][0].data[mqtt.ATTR_PAYLOAD]) + + def test_service_call_without_topic_does_not_publush(self): + self.hass.bus.fire(EVENT_CALL_SERVICE, { + ATTR_DOMAIN: mqtt.DOMAIN, + ATTR_SERVICE: mqtt.SERVICE_PUBLISH + }) + self.hass.pool.block_till_done() + self.assertTrue(not mqtt.MQTT_CLIENT.publish.called) + + def test_subscribe_topic(self): + mqtt.subscribe(self.hass, 'test-topic', self.record_calls) + + fire_mqtt_message(self.hass, 'test-topic', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('test-topic', self.calls[0][0]) + self.assertEqual('test-payload', self.calls[0][1]) + + def test_subscribe_topic_not_match(self): + mqtt.subscribe(self.hass, 'test-topic', self.record_calls) + + fire_mqtt_message(self.hass, 'another-test-topic', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_subscribe_topic_level_wildcard(self): + mqtt.subscribe(self.hass, 'test-topic/+/on', self.record_calls) + + fire_mqtt_message(self.hass, 'test-topic/bier/on', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('test-topic/bier/on', self.calls[0][0]) + self.assertEqual('test-payload', self.calls[0][1]) + + def test_subscribe_topic_level_wildcard_no_subtree_match(self): + mqtt.subscribe(self.hass, 'test-topic/+/on', self.record_calls) + + fire_mqtt_message(self.hass, 'test-topic/bier', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_subscribe_topic_subtree_wildcard_subtree_topic(self): + mqtt.subscribe(self.hass, 'test-topic/#', self.record_calls) + + fire_mqtt_message(self.hass, 'test-topic/bier/on', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('test-topic/bier/on', self.calls[0][0]) + self.assertEqual('test-payload', self.calls[0][1]) + + def test_subscribe_topic_subtree_wildcard_root_topic(self): + mqtt.subscribe(self.hass, 'test-topic/#', self.record_calls) + + fire_mqtt_message(self.hass, 'test-topic', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('test-topic', self.calls[0][0]) + self.assertEqual('test-payload', self.calls[0][1]) + + def test_subscribe_topic_subtree_wildcard_no_match(self): + mqtt.subscribe(self.hass, 'test-topic/#', self.record_calls) + + fire_mqtt_message(self.hass, 'another-test-topic', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) diff --git a/tests/test_component_recorder.py b/tests/components/test_recorder.py similarity index 97% rename from tests/test_component_recorder.py rename to tests/components/test_recorder.py index 68c63b637d0..26e5fdfd6b7 100644 --- a/tests/test_component_recorder.py +++ b/tests/components/test_recorder.py @@ -11,7 +11,7 @@ import os from homeassistant.const import MATCH_ALL from homeassistant.components import recorder -from helpers import get_test_home_assistant +from tests.common import get_test_home_assistant class TestRecorder(unittest.TestCase): diff --git a/tests/test_component_sun.py b/tests/components/test_sun.py similarity index 98% rename from tests/test_component_sun.py rename to tests/components/test_sun.py index 705caadcd3a..9d2ae38fdd6 100644 --- a/tests/test_component_sun.py +++ b/tests/components/test_sun.py @@ -10,7 +10,7 @@ from datetime import timedelta from astral import Astral -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util.dt as dt_util import homeassistant.components.sun as sun diff --git a/tests/test_component_switch.py b/tests/components/test_switch.py similarity index 98% rename from tests/test_component_switch.py rename to tests/components/test_switch.py index cbc161be853..642c7f45aa9 100644 --- a/tests/test_component_switch.py +++ b/tests/components/test_switch.py @@ -11,7 +11,7 @@ import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM import homeassistant.components.switch as switch -from helpers import get_test_home_assistant +from tests.common import get_test_home_assistant class TestSwitch(unittest.TestCase): diff --git a/tests/config/custom_components/light/test.py b/tests/config/custom_components/light/test.py index f7f355c4b30..1512d080b05 100644 --- a/tests/config/custom_components/light/test.py +++ b/tests/config/custom_components/light/test.py @@ -7,7 +7,7 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.helpers import MockToggleDevice +from tests.common import MockToggleDevice DEVICES = [] diff --git a/tests/config/custom_components/switch/test.py b/tests/config/custom_components/switch/test.py index 178faf7fcdc..bb95154a94b 100644 --- a/tests/config/custom_components/switch/test.py +++ b/tests/config/custom_components/switch/test.py @@ -7,7 +7,7 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.helpers import MockToggleDevice +from tests.common import MockToggleDevice DEVICES = [] diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_helper_entity.py b/tests/helpers/test_entity.py similarity index 98% rename from tests/test_helper_entity.py rename to tests/helpers/test_entity.py index 14559ded39a..b8823f23a5a 100644 --- a/tests/test_helper_entity.py +++ b/tests/helpers/test_entity.py @@ -7,7 +7,7 @@ Tests the entity helper. # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.helpers.entity as entity from homeassistant.const import ATTR_HIDDEN diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py new file mode 100644 index 00000000000..89711e2584e --- /dev/null +++ b/tests/helpers/test_event.py @@ -0,0 +1,126 @@ +""" +tests.helpers.event_test +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests event helpers. +""" +# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=too-few-public-methods +import unittest +from datetime import datetime + +import homeassistant.core as ha +from homeassistant.helpers.event import * + + +class TestEventHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = ha.HomeAssistant() + self.hass.states.set("light.Bowl", "on") + self.hass.states.set("switch.AC", "off") + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_track_point_in_time(self): + """ Test track point in time. """ + before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + + runs = [] + + track_point_in_utc_time( + self.hass, lambda x: runs.append(1), birthday_paulus) + + self._send_time_changed(before_birthday) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + + self._send_time_changed(birthday_paulus) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + + # A point in time tracker will only fire once, this should do nothing + self._send_time_changed(birthday_paulus) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + + track_point_in_time( + self.hass, lambda x: runs.append(1), birthday_paulus) + + self._send_time_changed(after_birthday) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + + def test_track_time_change(self): + """ Test tracking time change. """ + wildcard_runs = [] + specific_runs = [] + + track_time_change(self.hass, lambda x: wildcard_runs.append(1)) + track_utc_time_change( + self.hass, lambda x: specific_runs.append(1), second=[0, 30]) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) + self.hass.pool.block_till_done() + self.assertEqual(2, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + + def test_track_state_change(self): + """ Test track_state_change. """ + # 2 lists to track how often our callbacks get called + specific_runs = [] + wildcard_runs = [] + + track_state_change( + self.hass, 'light.Bowl', lambda a, b, c: specific_runs.append(1), + 'on', 'off') + + track_state_change( + self.hass, 'light.Bowl', lambda a, b, c: wildcard_runs.append(1), + ha.MATCH_ALL, ha.MATCH_ALL) + + # Set same state should not trigger a state change/listener + self.hass.states.set('light.Bowl', 'on') + self.hass.pool.block_till_done() + self.assertEqual(0, len(specific_runs)) + self.assertEqual(0, len(wildcard_runs)) + + # State change off -> on + self.hass.states.set('light.Bowl', 'off') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + # State change off -> off + self.hass.states.set('light.Bowl', 'off', {"some_attr": 1}) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + # State change off -> on + self.hass.states.set('light.Bowl', 'on') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + + def _send_time_changed(self, now): + """ Send a time changed event. """ + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) diff --git a/tests/test_helpers.py b/tests/helpers/test_init.py similarity index 95% rename from tests/test_helpers.py rename to tests/helpers/test_init.py index adc4b0d0788..c1af6ba8ccc 100644 --- a/tests/test_helpers.py +++ b/tests/helpers/test_init.py @@ -7,9 +7,9 @@ Tests component helpers. # pylint: disable=protected-access,too-many-public-methods import unittest -from helpers import get_test_home_assistant +from common import get_test_home_assistant -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import extract_entity_ids diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py new file mode 100644 index 00000000000..df05964a79f --- /dev/null +++ b/tests/test_bootstrap.py @@ -0,0 +1,41 @@ +""" +tests.test_bootstrap +~~~~~~~~~~~~~~~~~~~~ + +Tests bootstrap. +""" +# pylint: disable=too-many-public-methods,protected-access +import tempfile +import unittest +from unittest import mock + +from homeassistant import bootstrap +import homeassistant.util.dt as dt_util + +from tests.common import mock_detect_location_info + + +class TestBootstrap(unittest.TestCase): + """ Test the bootstrap utils. """ + + def setUp(self): + self.orig_timezone = dt_util.DEFAULT_TIME_ZONE + + def tearDown(self): + dt_util.DEFAULT_TIME_ZONE = self.orig_timezone + + def test_from_config_file(self): + components = ['browser', 'conversation', 'script'] + with tempfile.NamedTemporaryFile() as fp: + for comp in components: + fp.write('{}:\n'.format(comp).encode('utf-8')) + fp.flush() + + with mock.patch('homeassistant.util.location.detect_location_info', + mock_detect_location_info): + hass = bootstrap.from_config_file(fp.name) + + components.append('group') + + self.assertEqual(sorted(components), + sorted(hass.config.components)) diff --git a/tests/test_config.py b/tests/test_config.py index 0ea18eead82..f683fac890c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,18 +9,16 @@ import unittest import unittest.mock as mock import os -from homeassistant import DOMAIN, HomeAssistantError -import homeassistant.util.location as location_util +from homeassistant.core import DOMAIN, HomeAssistantError import homeassistant.config as config_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) -from helpers import get_test_config_dir +from common import get_test_config_dir, mock_detect_location_info CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) -CONF_PATH = os.path.join(CONFIG_DIR, config_util.CONF_CONFIG_FILE) def create_file(path): @@ -29,31 +27,13 @@ def create_file(path): pass -def mock_detect_location_info(): - """ Mock implementation of util.detect_location_info. """ - return location_util.LocationInfo( - ip='1.1.1.1', - country_code='US', - country_name='United States', - region_code='CA', - region_name='California', - city='San Diego', - zip_code='92122', - time_zone='America/Los_Angeles', - latitude='2.0', - longitude='1.0', - use_fahrenheit=True, - ) - - class TestConfig(unittest.TestCase): """ Test the config utils. """ def tearDown(self): # pylint: disable=invalid-name """ Clean up. """ - for path in (YAML_PATH, CONF_PATH): - if os.path.isfile(path): - os.remove(path) + if os.path.isfile(YAML_PATH): + os.remove(YAML_PATH) def test_create_default_config(self): """ Test creationg of default config. """ @@ -69,21 +49,6 @@ class TestConfig(unittest.TestCase): self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR)) - def test_find_config_file_conf(self): - """ Test if it finds the old CONF config file. """ - - create_file(CONF_PATH) - - self.assertEqual(CONF_PATH, config_util.find_config_file(CONFIG_DIR)) - - def test_find_config_file_prefers_yaml_over_conf(self): - """ Test if find config prefers YAML over CONF if both exist. """ - - create_file(YAML_PATH) - create_file(CONF_PATH) - - self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR)) - def test_ensure_config_exists_creates_config(self): """ Test that calling ensure_config_exists creates a new config file if none exists. """ @@ -135,20 +100,6 @@ class TestConfig(unittest.TestCase): self.assertEqual({'hello': 'world'}, config_util.load_config_file(YAML_PATH)) - def test_load_config_loads_conf_config(self): - """ Test correct YAML config loading. """ - create_file(CONF_PATH) - - self.assertEqual({}, config_util.load_config_file(CONF_PATH)) - - def test_conf_config_file(self): - """ Test correct CONF config loading. """ - with open(CONF_PATH, 'w') as f: - f.write('[ha]\ntime_zone=America/Los_Angeles') - - self.assertEqual({'ha': {'time_zone': 'America/Los_Angeles'}}, - config_util.load_conf_config_file(CONF_PATH)) - def test_create_default_config_detect_location(self): """ Test that detect location sets the correct config keys. """ with mock.patch('homeassistant.util.location.detect_location_info', diff --git a/tests/test_core.py b/tests/test_core.py index 58052fe43f0..1aab679805a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,18 +8,29 @@ Provides tests to verify that Home Assistant core works. # pylint: disable=too-few-public-methods import os import unittest +import unittest.mock as mock import time import threading from datetime import datetime -import homeassistant as ha +import pytz + +import homeassistant.core as ha +from homeassistant.exceptions import ( + HomeAssistantError, InvalidEntityFormatError) +import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_state_change +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + ATTR_FRIENDLY_NAME, TEMP_CELCIUS, + TEMP_FAHRENHEIT) + +PST = pytz.timezone('America/Los_Angeles') class TestHomeAssistant(unittest.TestCase): """ Tests the Home Assistant core classes. - Currently only includes tests to test cases that do not - get tested in the API integration tests. """ def setUp(self): # pylint: disable=invalid-name @@ -32,17 +43,17 @@ class TestHomeAssistant(unittest.TestCase): """ Stop down stuff we started. """ try: self.hass.stop() - except ha.HomeAssistantError: + except HomeAssistantError: # Already stopped after the block till stopped test pass - def test_get_config_path(self): - """ Test get_config_path method. """ - self.assertEqual(os.path.join(os.getcwd(), "config"), - self.hass.config.config_dir) - - self.assertEqual(os.path.join(os.getcwd(), "config", "test.conf"), - self.hass.config.path("test.conf")) + def test_start(self): + calls = [] + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, + lambda event: calls.append(1)) + self.hass.start() + self.hass.pool.block_till_done() + self.assertEqual(1, len(calls)) def test_block_till_stoped(self): """ Test if we can block till stop service is called. """ @@ -51,28 +62,48 @@ class TestHomeAssistant(unittest.TestCase): self.assertFalse(blocking_thread.is_alive()) blocking_thread.start() - # Python will now give attention to the other thread - time.sleep(1) + + # Threads are unpredictable, try 20 times if we're ready + wait_loops = 0 + while not blocking_thread.is_alive() and wait_loops < 20: + wait_loops += 1 + time.sleep(0.05) self.assertTrue(blocking_thread.is_alive()) self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) self.hass.pool.block_till_done() - # hass.block_till_stopped checks every second if it should quit - # we have to wait worst case 1 second + # Threads are unpredictable, try 20 times if we're ready wait_loops = 0 - while blocking_thread.is_alive() and wait_loops < 50: + while blocking_thread.is_alive() and wait_loops < 20: wait_loops += 1 - time.sleep(0.1) + time.sleep(0.05) self.assertFalse(blocking_thread.is_alive()) + def test_stopping_with_keyboardinterrupt(self): + calls = [] + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: calls.append(1)) + + def raise_keyboardinterrupt(length): + # We don't want to patch the sleep of the timer. + if length == 1: + raise KeyboardInterrupt + + self.hass.start() + + with mock.patch('time.sleep', raise_keyboardinterrupt): + self.hass.block_till_stopped() + + self.assertEqual(1, len(calls)) + def test_track_point_in_time(self): """ Test track point in time. """ - before_birthday = datetime(1985, 7, 9, 12, 0, 0) - birthday_paulus = datetime(1986, 7, 9, 12, 0, 0) - after_birthday = datetime(1987, 7, 9, 12, 0, 0) + before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) runs = [] @@ -92,7 +123,7 @@ class TestHomeAssistant(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) - self.hass.track_point_in_utc_time( + self.hass.track_point_in_time( lambda x: runs.append(1), birthday_paulus) self._send_time_changed(after_birthday) @@ -105,7 +136,7 @@ class TestHomeAssistant(unittest.TestCase): specific_runs = [] self.hass.track_time_change(lambda x: wildcard_runs.append(1)) - self.hass.track_time_change( + self.hass.track_utc_time_change( lambda x: specific_runs.append(1), second=[0, 30]) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) @@ -130,6 +161,16 @@ class TestHomeAssistant(unittest.TestCase): class TestEvent(unittest.TestCase): """ Test Event class. """ + def test_eq(self): + now = dt_util.utcnow() + data = {'some': 'attr'} + event1, event2 = [ + ha.Event('some_type', data, time_fired=now) + for _ in range(2) + ] + + self.assertEqual(event1, event2) + def test_repr(self): """ Test that repr method works. #MoreCoverage """ self.assertEqual( @@ -142,13 +183,27 @@ class TestEvent(unittest.TestCase): {"beer": "nice"}, ha.EventOrigin.remote))) + def test_as_dict(self): + event_type = 'some_type' + now = dt_util.utcnow() + data = {'some': 'attr'} + + event = ha.Event(event_type, data, ha.EventOrigin.local, now) + expected = { + 'event_type': event_type, + 'data': data, + 'origin': 'LOCAL', + 'time_fired': dt_util.datetime_to_str(now), + } + self.assertEqual(expected, event.as_dict()) + class TestEventBus(unittest.TestCase): """ Test EventBus methods. """ def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ - self.bus = ha.EventBus() + self.bus = ha.EventBus(ha.create_worker_pool(0)) self.bus.listen('test_event', lambda x: len) def tearDown(self): # pylint: disable=invalid-name @@ -157,6 +212,7 @@ class TestEventBus(unittest.TestCase): def test_add_remove_listener(self): """ Test remove_listener method. """ + self.bus._pool.add_worker() old_count = len(self.bus.listeners) listener = lambda x: len @@ -182,11 +238,10 @@ class TestEventBus(unittest.TestCase): self.bus.listen_once('test_event', lambda x: runs.append(1)) self.bus.fire('test_event') - self.bus._pool.block_till_done() - self.assertEqual(1, len(runs)) - # Second time it should not increase runs self.bus.fire('test_event') + + self.bus._pool.add_worker() self.bus._pool.block_till_done() self.assertEqual(1, len(runs)) @@ -197,9 +252,40 @@ class TestState(unittest.TestCase): def test_init(self): """ Test state.init """ self.assertRaises( - ha.InvalidEntityFormatError, ha.State, + InvalidEntityFormatError, ha.State, 'invalid_entity_format', 'test_state') + def test_domain(self): + state = ha.State('some_domain.hello', 'world') + self.assertEqual('some_domain', state.domain) + + def test_object_id(self): + state = ha.State('domain.hello', 'world') + self.assertEqual('hello', state.object_id) + + def test_name_if_no_friendly_name_attr(self): + state = ha.State('domain.hello_world', 'world') + self.assertEqual('hello world', state.name) + + def test_name_if_friendly_name_attr(self): + name = 'Some Unique Name' + state = ha.State('domain.hello_world', 'world', + {ATTR_FRIENDLY_NAME: name}) + self.assertEqual(name, state.name) + + def test_copy(self): + state = ha.State('domain.hello', 'world', {'some': 'attr'}) + self.assertEqual(state, state.copy()) + + def test_dict_conversion(self): + state = ha.State('domain.hello', 'world', {'some': 'attr'}) + self.assertEqual(state, ha.State.from_dict(state.as_dict())) + + def test_dict_conversion_with_wrong_data(self): + self.assertIsNone(ha.State.from_dict(None)) + self.assertIsNone(ha.State.from_dict({'state': 'yes'})) + self.assertIsNone(ha.State.from_dict({'entity_id': 'yes'})) + def test_repr(self): """ Test state.repr """ self.assertEqual("", @@ -218,14 +304,15 @@ class TestStateMachine(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ - self.bus = ha.EventBus() + self.pool = ha.create_worker_pool(0) + self.bus = ha.EventBus(self.pool) self.states = ha.StateMachine(self.bus) self.states.set("light.Bowl", "on") self.states.set("switch.AC", "off") def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.bus._pool.stop() + self.pool.stop() def test_is_state(self): """ Test is_state method. """ @@ -244,6 +331,10 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(1, len(ent_ids)) self.assertTrue('light.bowl' in ent_ids) + def test_all(self): + states = sorted(state.entity_id for state in self.states.all()) + self.assertEqual(['light.bowl', 'switch.ac'], states) + def test_remove(self): """ Test remove method. """ self.assertTrue('light.bowl' in self.states.entity_ids()) @@ -255,6 +346,8 @@ class TestStateMachine(unittest.TestCase): def test_track_change(self): """ Test states.track_change. """ + self.pool.add_worker() + # 2 lists to track how often our callbacks got called specific_runs = [] wildcard_runs = [] @@ -291,10 +384,11 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(3, len(wildcard_runs)) def test_case_insensitivty(self): + self.pool.add_worker() runs = [] - self.states.track_change( - 'light.BoWl', lambda a, b, c: runs.append(1), + track_state_change( + ha._MockHA(self.bus), 'light.BoWl', lambda a, b, c: runs.append(1), ha.MATCH_ALL, ha.MATCH_ALL) self.states.set('light.BOWL', 'off') @@ -332,16 +426,159 @@ class TestServiceRegistry(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ - self.pool = ha.create_worker_pool() + self.pool = ha.create_worker_pool(0) self.bus = ha.EventBus(self.pool) self.services = ha.ServiceRegistry(self.bus, self.pool) - self.services.register("test_domain", "test_service", lambda x: len) + self.services.register("test_domain", "test_service", lambda x: None) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.pool.stop() + if self.pool.worker_count: + self.pool.stop() def test_has_service(self): """ Test has_service method. """ self.assertTrue( self.services.has_service("test_domain", "test_service")) + self.assertFalse( + self.services.has_service("test_domain", "non_existing")) + self.assertFalse( + self.services.has_service("non_existing", "test_service")) + + def test_services(self): + expected = { + 'test_domain': ['test_service'] + } + self.assertEqual(expected, self.services.services) + + def test_call_with_blocking_done_in_time(self): + self.pool.add_worker() + self.pool.add_worker() + calls = [] + self.services.register("test_domain", "register_calls", + lambda x: calls.append(1)) + + self.assertTrue( + self.services.call('test_domain', 'register_calls', blocking=True)) + self.assertEqual(1, len(calls)) + + def test_call_with_blocking_not_done_in_time(self): + calls = [] + self.services.register("test_domain", "register_calls", + lambda x: calls.append(1)) + + orig_limit = ha.SERVICE_CALL_LIMIT + ha.SERVICE_CALL_LIMIT = 0.01 + self.assertFalse( + self.services.call('test_domain', 'register_calls', blocking=True)) + self.assertEqual(0, len(calls)) + ha.SERVICE_CALL_LIMIT = orig_limit + + def test_call_non_existing_with_blocking(self): + self.pool.add_worker() + self.pool.add_worker() + orig_limit = ha.SERVICE_CALL_LIMIT + ha.SERVICE_CALL_LIMIT = 0.01 + self.assertFalse( + self.services.call('test_domain', 'i_do_not_exist', blocking=True)) + ha.SERVICE_CALL_LIMIT = orig_limit + + +class TestConfig(unittest.TestCase): + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.config = ha.Config() + + def test_config_dir_set_correct(self): + """ Test config dir set correct. """ + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + self.assertEqual(os.path.join(data_dir, ".homeassistant"), + self.config.config_dir) + + def test_path_with_file(self): + """ Test get_config_path method. """ + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + self.assertEqual(os.path.join(data_dir, ".homeassistant", "test.conf"), + self.config.path("test.conf")) + + def test_path_with_dir_and_file(self): + """ Test get_config_path method. """ + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + self.assertEqual( + os.path.join(data_dir, ".homeassistant", "dir", "test.conf"), + self.config.path("dir", "test.conf")) + + def test_temperature_not_convert_if_no_preference(self): + """ No unit conversion to happen if no preference. """ + self.assertEqual( + (25, TEMP_CELCIUS), + self.config.temperature(25, TEMP_CELCIUS)) + self.assertEqual( + (80, TEMP_FAHRENHEIT), + self.config.temperature(80, TEMP_FAHRENHEIT)) + + def test_temperature_not_convert_if_invalid_value(self): + """ No unit conversion to happen if no preference. """ + self.config.temperature_unit = TEMP_FAHRENHEIT + self.assertEqual( + ('25a', TEMP_CELCIUS), + self.config.temperature('25a', TEMP_CELCIUS)) + + def test_temperature_not_convert_if_invalid_unit(self): + """ No unit conversion to happen if no preference. """ + self.assertEqual( + (25, 'Invalid unit'), + self.config.temperature(25, 'Invalid unit')) + + def test_temperature_to_convert_to_celcius(self): + self.config.temperature_unit = TEMP_CELCIUS + + self.assertEqual( + (25, TEMP_CELCIUS), + self.config.temperature(25, TEMP_CELCIUS)) + self.assertEqual( + (26.7, TEMP_CELCIUS), + self.config.temperature(80, TEMP_FAHRENHEIT)) + + def test_temperature_to_convert_to_fahrenheit(self): + self.config.temperature_unit = TEMP_FAHRENHEIT + + self.assertEqual( + (77, TEMP_FAHRENHEIT), + self.config.temperature(25, TEMP_CELCIUS)) + self.assertEqual( + (80, TEMP_FAHRENHEIT), + self.config.temperature(80, TEMP_FAHRENHEIT)) + + def test_as_dict(self): + expected = { + 'latitude': None, + 'longitude': None, + 'temperature_unit': None, + 'location_name': None, + 'time_zone': 'UTC', + 'components': [], + } + + self.assertEqual(expected, self.config.as_dict()) + + +class TestWorkerPool(unittest.TestCase): + def test_exception_during_job(self): + pool = ha.create_worker_pool(1) + + def malicious_job(_): + raise Exception("Test breaking worker pool") + + calls = [] + + def register_call(_): + calls.append(1) + + pool.add_job(ha.JobPriority.EVENT_DEFAULT, (malicious_job, None)) + pool.add_job(ha.JobPriority.EVENT_DEFAULT, (register_call, None)) + pool.block_till_done() + self.assertEqual(1, len(calls)) diff --git a/tests/test_loader.py b/tests/test_loader.py index dd80587b247..03bd7e7419c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,7 +10,7 @@ import unittest import homeassistant.loader as loader import homeassistant.components.http as http -from helpers import get_test_home_assistant, MockModule +from common import get_test_home_assistant, MockModule class TestLoader(unittest.TestCase): diff --git a/tests/test_remote.py b/tests/test_remote.py index 7c00cbfd526..e5bfd71199f 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -9,7 +9,7 @@ Uses port 8125 as a port that nothing runs on # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http @@ -130,9 +130,12 @@ class TestRemoteMethods(unittest.TestCase): def test_set_state(self): """ Test Python API set_state. """ - self.assertTrue(remote.set_state(master_api, 'test.test', 'set_test')) + hass.states.set('test.test', 'set_test') - self.assertEqual('set_test', hass.states.get('test.test').state) + state = hass.states.get('test.test') + + self.assertIsNotNone(state) + self.assertEqual('set_test', state.state) self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test')) diff --git a/tests/util/__init__.py b/tests/util/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_util_color.py b/tests/util/test_color.py similarity index 100% rename from tests/test_util_color.py rename to tests/util/test_color.py diff --git a/tests/test_util_dt.py b/tests/util/test_dt.py similarity index 100% rename from tests/test_util_dt.py rename to tests/util/test_dt.py diff --git a/tests/test_util.py b/tests/util/test_init.py similarity index 100% rename from tests/test_util.py rename to tests/util/test_init.py