diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..012084af5ce 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,36 +10,47 @@ 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/mqtt.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/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/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 @@ -48,12 +59,18 @@ omit = homeassistant/components/sensor/forecast.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/rfxtrx.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/.gitmodules b/.gitmodules index 174bba680f0..a627e522d8f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,3 @@ -[submodule "homeassistant/external/pynetgear"] - path = homeassistant/external/pynetgear - url = https://github.com/balloob/pynetgear.git -[submodule "homeassistant/external/pywemo"] - path = homeassistant/external/pywemo - url = https://github.com/balloob/pywemo.git -[submodule "homeassistant/external/netdisco"] - path = homeassistant/external/netdisco - url = https://github.com/balloob/netdisco.git [submodule "homeassistant/external/noop"] path = homeassistant/external/noop url = https://github.com/balloob/noop.git @@ -16,9 +7,6 @@ [submodule "homeassistant/external/nzbclients"] path = homeassistant/external/nzbclients url = https://github.com/jamespcole/home-assistant-nzb-clients.git -[submodule "homeassistant/external/pymysensors"] - path = homeassistant/external/pymysensors - url = https://github.com/theolind/pymysensors [submodule "homeassistant/components/frontend/www_static/home-assistant-polymer"] path = homeassistant/components/frontend/www_static/home-assistant-polymer url = https://github.com/balloob/home-assistant-polymer.git diff --git a/README.md b/README.md index 18a01345741..00676577281 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Check out [the website](https://home-assistant.io) for installation instructions 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/) + * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) * [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/) @@ -22,7 +22,7 @@ Built home automation on top of your devices: * 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) + * 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). 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..bf21dc0a15d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -13,6 +13,7 @@ import threading import enum import re import functools as ft +from collections import namedtuple from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, @@ -32,7 +33,7 @@ TIMER_INTERVAL = 1 # seconds SERVICE_CALL_LIMIT = 10 # seconds # Define number of MINIMUM worker threads. -# During bootstrap of HA (see bootstrap.from_config_dict()) 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 @@ -41,6 +42,9 @@ 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. """ @@ -52,40 +56,12 @@ class HomeAssistant(object): 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) - + create_timer(self) self.bus.fire(EVENT_HOMEASSISTANT_START) def block_till_stopped(self): @@ -93,110 +69,21 @@ class HomeAssistant(object): will block until called. """ request_shutdown = threading.Event() - self.services.register(DOMAIN, SERVICE_HOMEASSISTANT_STOP, - lambda service: request_shutdown.set()) + 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 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") @@ -208,76 +95,45 @@ class HomeAssistant(object): self.pool.stop() - def get_entity_ids(self, domain_filter=None): - """ - Returns known entity ids. - - THIS METHOD IS DEPRECATED. Use hass.states.entity_ids - """ + 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.get_entiy_ids is deprecated. Use hass.states.entity_ids") + '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) - 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. - """ + 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.listen_once_event is deprecated. Use hass.bus.listen_once") + '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) - self.bus.listen_once(event_type, listener) + 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_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 + 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): @@ -305,33 +161,6 @@ class JobPriority(util.OrderedEnum): 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 @@ -354,7 +183,7 @@ class Event(object): self.event_type = event_type self.data = data or {} self.origin = origin - self.time_fired = util.strip_microseconds( + self.time_fired = date_util.strip_microseconds( time_fired or date_util.utcnow()) def as_dict(self): @@ -446,25 +275,28 @@ class EventBus(object): 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. + 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 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 + 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) + self.remove_listener(event_type, onetime_listener) - listener(event) + 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: @@ -596,18 +428,19 @@ class StateMachine(object): 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: + 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. """ - return [state.copy() for state in self._states.values()] + with self._lock: + return [state.copy() for state in self._states.values()] def get(self, entity_id): """ Returns the state of the specified entity. """ @@ -616,16 +449,6 @@ class StateMachine(object): # 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() @@ -661,59 +484,32 @@ class StateMachine(object): 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 - if not (same_state and same_attr): - last_changed = old_state.last_changed if same_state else None + 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 + state = State(entity_id, new_state, attributes, last_changed) + self._states[entity_id] = state - event_data = {'entity_id': entity_id, 'new_state': state} + event_data = {'entity_id': entity_id, 'new_state': state} - if old_state: - event_data['old_state'] = old_state + if old_state: + event_data['old_state'] = old_state - self._bus.fire(EVENT_STATE_CHANGED, event_data) + 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. + DEPRECATED AS OF 8/4/2015 """ - 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 + _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 @@ -802,23 +598,15 @@ class ServiceRegistry(object): 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 + 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. """ @@ -826,15 +614,16 @@ class ServiceRegistry(object): 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) + if not self.has_service(domain, service): + return - # 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))) + 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. """ @@ -843,9 +632,8 @@ class ServiceRegistry(object): service(call) self._bus.fire( - EVENT_SERVICE_EXECUTED, { - ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID] - }) + EVENT_SERVICE_EXECUTED, + {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) def _generate_unique_id(self): """ Generates a unique service call id. """ @@ -853,70 +641,6 @@ class ServiceRegistry(object): 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. """ @@ -943,8 +667,8 @@ class Config(object): 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): + if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and + self.temperature_unit and unit != self.temperature_unit): return value, unit try: @@ -986,3 +710,93 @@ class InvalidEntityFormatError(HomeAssistantError): class NoEntitySpecifiedError(HomeAssistantError): """ When no entity is specified. """ pass + + +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/bootstrap.py b/homeassistant/bootstrap.py index 2da2f4fb7b5..514c1adce57 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -63,12 +63,15 @@ def setup_component(hass, domain, config=None): def _handle_requirements(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): + _LOGGER.error('Not initializing %s because could not install ' + 'dependency %s', name, req) + return False + return True @@ -83,33 +86,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): 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): diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 21bea96201b..c7fa1c12d4b 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 @@ -27,7 +27,7 @@ def setup(hass, config): """ Sets up automation. """ 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) 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/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..452480b12c9 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers import validate_config 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) @@ -134,7 +135,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, 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 102bc78ff47..3fe11f99fe6 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -43,6 +43,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pynetgear>=0.1'] def get_scanner(hass, config): @@ -64,22 +65,10 @@ class NetgearDeviceScanner(object): """ This class queries a Netgear wireless router using the SOAP-API. """ def __init__(self, host, username, password): + import pynetgear + self.last_results = [] - try: - # Pylint does not play nice if not every folders has an __init__.py - # pylint: disable=no-name-in-module, import-error - import homeassistant.external.pynetgear.pynetgear as pynetgear - except ImportError: - _LOGGER.exception( - ("Failed to import pynetgear. " - "Did you maybe not run `git submodule init` " - "and `git submodule update`?")) - - self.success_init = False - - return - self._api = pynetgear.Netgear(host, username, password) self.lock = threading.Lock() diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index e3af9ad5c44..8876ed3d488 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -43,6 +43,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.2'] + def get_scanner(hass, config): """ Validates config and returns a Nmap scanner. """ diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 63c9a0af74f..0aa7312bfd7 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -12,9 +12,6 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired. import logging import threading -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.netdisco.netdisco.const as services - from homeassistant import bootstrap from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED, @@ -22,14 +19,20 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['zeroconf>=0.16.0'] +REQUIREMENTS = ['netdisco>=0.1'] SCAN_INTERVAL = 300 # seconds +# Next 3 lines for now a mirror from netdisco.const +# Should setup a mapping netdisco.const -> own constants +SERVICE_WEMO = 'belkin_wemo' +SERVICE_HUE = 'philips_hue' +SERVICE_CAST = 'google_cast' + SERVICE_HANDLERS = { - services.BELKIN_WEMO: "switch", - services.GOOGLE_CAST: "media_player", - services.PHILIPS_HUE: "light", + SERVICE_WEMO: "switch", + SERVICE_CAST: "media_player", + SERVICE_HUE: "light", } @@ -56,14 +59,7 @@ def setup(hass, config): """ Starts a discovery service. """ logger = logging.getLogger(__name__) - try: - from homeassistant.external.netdisco.netdisco.service import \ - DiscoveryService - except ImportError: - logger.exception( - "Unable to import netdisco. " - "Did you install all the zeroconf dependency?") - return False + from netdisco.service import DiscoveryService # Disable zeroconf logging, it spams logging.getLogger('zeroconf').setLevel(logging.CRITICAL) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 4a67ed0fc0e..988adf16158 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 = "4f94fd4404583fbf27cc899c024d26ff" +VERSION = "d3d94fe65a29aa438887d368bca61d68" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 023347483cb..35d66fc43ee 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,6 @@ -