diff --git a/.coveragerc b/.coveragerc index d97befce096..ee40c987ea2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -31,14 +31,19 @@ omit = homeassistant/components/keyboard.py homeassistant/components/light/hue.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/mpd.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushover.py + homeassistant/components/notify/smtp.py + homeassistant/components/notify/syslog.py homeassistant/components/notify/xmpp.py + homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48523c1a4fe..3f2fd110a1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,41 @@ -# Adding support for a new device +# Contributing to Home Assistant -For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/). +Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them? + +The process is straight-forward. + + - Fork the Home Assistant [git repository](https://github.com/balloob/home-assistant). + - Write the code for your device, notification service, sensor, or IoT thing. + - Check it with ``pylint`` and ``flake8``. + - Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant. + +Still interested? Then you should read the next sections and get more details. + +## Adding support for a new device + +For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/). After you finish adding support for your device: - - update the supported devices in README.md. - - add any new dependencies to requirements.txt. - - Make sure all your code passes Pylint, flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`. + - Update the supported devices in the `README.md` file. + - Add any new dependencies to `requirements.txt`. + - Update the `.coveragerc` file. + - Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io). + - Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`. + - Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant. + - Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/). If you've added a component: - - update the file [`domain-icon.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/http/www_static/polymer/domain-icon.html) with an icon for your domain ([pick from this list](https://www.polymer-project.org/0.5/components/core-elements/demo.html#core-icon)) - - update the demo component with two states that it provides + - Update the file [`home-assistant-icons.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)). + - Update the demo component with two states that it provides - Add your component to home-assistant.conf.example -Since you've updated domain-icon.html, you've made changes to the frontend: +Since you've updated `home-assistant-icons.html`, you've made changes to the frontend: - - run `build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit. + - Run `build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit. -## Setting states +### Setting states It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices. @@ -31,9 +48,9 @@ A state can have several attributes that will help the frontend in displaying yo These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25). -## Proper Visibility Handling ## +### Proper Visibility Handling -Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py) Class. If this is done, visibility will be handled for you. +Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you. You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following. ```python @@ -44,12 +61,12 @@ This will SUGGEST that the active frontend hides the entity. This requires that Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa). -## Working on the frontend +### Working on the frontend -The frontend is composed of Polymer web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the http-component in your config. +The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config. When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works. -## Notes on PyLint and PEP8 validation +### Notes on PyLint and PEP8 validation In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change. diff --git a/README.md b/README.md index 43c4d58a33d..05fe01340bc 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,20 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: - * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index)) - * Track and control [Philips Hue](http://meethue.com) lights - * Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) - * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) - * Track running services by monitoring `ps` output - * Track and control [Tellstick devices and sensors](http://www.telldus.se/products/tellstick) + * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index)) + * Track and control [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 + * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/) + * 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/) + * Track running system services and monitoring your system stats (Memory, disk usage, and more) + * Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands * Turn on the lights when people get home after sun set * Turn on lights slowly during sun set to compensate for light loss * Turn off all lights and devices when everybody leaves the house * Offers web interface to monitor and control Home Assistant * Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects * [Ability to have multiple instances of Home Assistant work together](https://home-assistant.io/developers/architecture.html) + * 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 to display details about a running [Transmission](http://www.transmissionbt.com/) client, the [Bitcoin](https://bitcoin.org) network, local meteorological data from [OpenWeatherMap](http://openweathermap.org/), the time, the date, and the downloads from [SABnzbd](http://sabnzbd.org) Home Assistant also includes functionality for controlling HTPCs: @@ -33,17 +35,17 @@ If you run into issues while using Home Assistant or during development of a com ## Installation instructions / Quick-start guide -Running Home Assistant requires that python 3.4 and the package requests are installed. Run the following code to install and start Home Assistant: +Running Home Assistant requires that [Python](https://www.python.org/) 3.4 and the package [requests](http://docs.python-requests.org/en/latest/) are installed. Run the following code to install and start Home Assistant: ```python git clone --recursive https://github.com/balloob/home-assistant.git cd home-assistant -pip3 install -r requirements.txt +python3 -m pip install --user -r requirements.txt python3 -m homeassistant --open-ui ``` -The last command will start the Home Assistant server and launch its webinterface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists. +The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. 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 Asssitant. +Please see [the getting started guide](https://home-assistant.io/getting-started/) on how to further configure Home Assistant. diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 524078b7765..c99f760f21f 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -68,7 +68,7 @@ device_sun_light_trigger: # A comma separated list of states that have to be tracked as a single group # Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME) -group: +group: living_room: - light.Bowl - light.Ceiling diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 132482d4bfa..09069924e6b 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -961,12 +961,14 @@ class Config(object): 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': self.time_zone.zone, + 'time_zone': time_zone.zone, 'components': self.components, } diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 83a8c499718..316529ce74e 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -141,9 +141,9 @@ def main(): def open_browser(event): """ Open the webinterface in a browser. """ - if hass.local_api is not None: + if hass.config.api is not None: import webbrowser - webbrowser.open(hass.local_api.base_url) + webbrowser.open(hass.config.api.base_url) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 787e0f80562..ee2ee54df79 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -235,8 +235,13 @@ def process_ha_core_config(hass, config): set_time_zone(config.get(CONF_TIME_ZONE)) - for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): - Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) + customize = config.get(CONF_CUSTOMIZE) + + if isinstance(customize, dict): + for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): + if not isinstance(attrs, dict): + continue + Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) if CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 40dd6e5150e..611136aac5b 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -17,7 +17,7 @@ import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, - CONF_PLATFORM) + CONF_PLATFORM, DEVICE_DEFAULT_NAME) from homeassistant.components import group DOMAIN = "device_tracker" @@ -41,6 +41,8 @@ CONF_SECONDS = "interval_seconds" DEFAULT_CONF_SECONDS = 12 +TRACK_NEW_DEVICES = "track_new_devices" + _LOGGER = logging.getLogger(__name__) @@ -78,7 +80,10 @@ def setup(hass, config): seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int, DEFAULT_CONF_SECONDS) - tracker = DeviceTracker(hass, device_scanner, seconds) + track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False + _LOGGER.info("Tracking new devices: %s", track_new_devices) + + tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices) # We only succeeded if we got to parse the known devices file return not tracker.invalid_known_devices_file @@ -87,13 +92,16 @@ def setup(hass, config): class DeviceTracker(object): """ Class that tracks which devices are home and which are not. """ - def __init__(self, hass, device_scanner, seconds): + def __init__(self, hass, device_scanner, seconds, track_new_devices): self.hass = hass self.device_scanner = device_scanner self.lock = threading.Lock() + # Do we track new devices by default? + self.track_new_devices = track_new_devices + # Dictionary to keep track of known devices and devices we track self.tracked = {} self.untracked_devices = set() @@ -161,57 +169,28 @@ class DeviceTracker(object): if not self.lock.acquire(False): return - found_devices = set(dev.upper() for dev in - self.device_scanner.scan_devices()) + try: + found_devices = set(dev.upper() for dev in + self.device_scanner.scan_devices()) - for device in self.tracked: - is_home = device in found_devices + for device in self.tracked: + is_home = device in found_devices - self._update_state(now, device, is_home) + self._update_state(now, device, is_home) - if is_home: - found_devices.remove(device) + if is_home: + found_devices.remove(device) - # Did we find any devices that we didn't know about yet? - new_devices = found_devices - self.untracked_devices + # Did we find any devices that we didn't know about yet? + new_devices = found_devices - self.untracked_devices - if new_devices: - self.untracked_devices.update(new_devices) + if new_devices: + if not self.track_new_devices: + self.untracked_devices.update(new_devices) - # Write new devices to known devices file - if not self.invalid_known_devices_file: - - known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) - - try: - # If file does not exist we will write the header too - is_new_file = not os.path.isfile(known_dev_path) - - with open(known_dev_path, 'a') as outp: - _LOGGER.info( - "Found %d new devices, updating %s", - len(new_devices), known_dev_path) - - writer = csv.writer(outp) - - if is_new_file: - writer.writerow(( - "device", "name", "track", "picture")) - - for device in new_devices: - # See if the device scanner knows the name - # else defaults to unknown device - dname = self.device_scanner.get_device_name(device) - name = dname or "unknown device" - - writer.writerow((device, name, 0, "")) - - except IOError: - _LOGGER.exception( - "Error updating %s with %d new devices", - known_dev_path, len(new_devices)) - - self.lock.release() + self._update_known_devices_file(new_devices) + finally: + self.lock.release() # pylint: disable=too-many-branches def _read_known_devices_file(self): @@ -227,7 +206,6 @@ class DeviceTracker(object): self.untracked_devices.clear() with open(known_dev_path) as inp: - default_last_seen = dt_util.utcnow().replace(year=1990) # To track which devices need an entity_id assigned need_entity_id = [] @@ -248,10 +226,7 @@ class DeviceTracker(object): # We found a new device need_entity_id.append(device) - self.tracked[device] = { - 'name': row['name'], - 'last_seen': default_last_seen - } + self._track_device(device, row['name']) # Update state_attr with latest from file state_attr = { @@ -276,21 +251,7 @@ class DeviceTracker(object): self.tracked.pop(device) - # Setup entity_ids for the new devices - used_entity_ids = [info['entity_id'] for device, info - in self.tracked.items() - if device not in need_entity_id] - - for device in need_entity_id: - name = self.tracked[device]['name'] - - entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(name)), - used_entity_ids) - - used_entity_ids.append(entity_id) - - self.tracked[device]['entity_id'] = entity_id + self._generate_entity_ids(need_entity_id) if not self.tracked: _LOGGER.warning( @@ -309,3 +270,72 @@ class DeviceTracker(object): finally: self.lock.release() + + def _update_known_devices_file(self, new_devices): + """ Add new devices to known devices file. """ + if not self.invalid_known_devices_file: + known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) + + try: + # If file does not exist we will write the header too + is_new_file = not os.path.isfile(known_dev_path) + + with open(known_dev_path, 'a') as outp: + _LOGGER.info("Found %d new devices, updating %s", + len(new_devices), known_dev_path) + + writer = csv.writer(outp) + + if is_new_file: + writer.writerow(("device", "name", "track", "picture")) + + for device in new_devices: + # See if the device scanner knows the name + # else defaults to unknown device + name = self.device_scanner.get_device_name(device) or \ + DEVICE_DEFAULT_NAME + + track = 0 + if self.track_new_devices: + self._track_device(device, name) + track = 1 + + writer.writerow((device, name, track, "")) + + if self.track_new_devices: + self._generate_entity_ids(new_devices) + + except IOError: + _LOGGER.exception("Error updating %s with %d new devices", + known_dev_path, len(new_devices)) + + def _track_device(self, device, name): + """ + Add a device to the list of tracked devices. + Does not generate the entity id yet. + """ + default_last_seen = dt_util.utcnow().replace(year=1990) + + self.tracked[device] = { + 'name': name, + 'last_seen': default_last_seen, + 'state_attr': {ATTR_FRIENDLY_NAME: name} + } + + def _generate_entity_ids(self, need_entity_id): + """ Generate entity ids for a list of devices. """ + # Setup entity_ids for the new devices + used_entity_ids = [info['entity_id'] for device, info + in self.tracked.items() + if device not in need_entity_id] + + for device in need_entity_id: + name = self.tracked[device]['name'] + + entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(name)), + used_entity_ids) + + used_entity_ids.append(entity_id) + + self.tracked[device]['entity_id'] = entity_id diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 6ff3d996a47..893c9070526 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -114,12 +114,13 @@ class LuciDeviceScanner(object): hosts = [x for x in result.values() if x['.type'] == 'host' and 'mac' in x and 'name' in x] - mac2name_list = [(x['mac'], x['name']) for x in hosts] + mac2name_list = [ + (x['mac'].upper(), x['name']) for x in hosts] self.mac2name = dict(mac2name_list) else: # Error, handled in the _req_json_rpc return - return self.mac2name.get(device, None) + return self.mac2name.get(device.upper(), None) @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index ccd04f6cda1..e3af9ad5c44 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -120,7 +120,7 @@ class NmapDeviceScanner(object): else: mac = _arp(host.ipv4) if mac: - device = Device(mac, name, host.ipv4, now) + device = Device(mac.upper(), name, host.ipv4, now) self.last_results.append(device) _LOGGER.info("nmap scan successful") return True diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index 4844eb46760..f84c8653b31 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -9,19 +9,19 @@ - - - +

Initializing Home Assistant

- + diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index a1c8f55f236..f0ef54755c8 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 = "28c0680cf6ebd969dc5710c22d9c4075" +VERSION = "ddf42f54c15daf472d4c8641fab8d418" diff --git a/homeassistant/components/frontend/www_static/favicon-apple-180x180.png b/homeassistant/components/frontend/www_static/favicon-apple-180x180.png new file mode 100644 index 00000000000..20117d00f22 Binary files /dev/null and b/homeassistant/components/frontend/www_static/favicon-apple-180x180.png differ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index a4c206a4ce4..3528c02eb0f 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,24 +1,26760 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file +`paper-dialog-common.css` provide styles for a header, content area, and an action area for buttons. +Use the `

` tag for the header and the `buttons` class for the action area. You can use the +`paper-dialog-scrollable` element (in its own repository) if you need a scrolling content area. + +Use the `dialog-dismiss` and `dialog-confirm` attributes on interactive controls to close the +dialog. If the user dismisses the dialog with `dialog-confirm`, the `closingReason` will update +to include `confirmed: true`. + +### Styling + +The following custom properties and mixins are available for styling. + +Custom property | Description | Default +----------------|-------------|---------- +`--paper-dialog-background-color` | Dialog background color | `--primary-background-color` +`--paper-dialog-color` | Dialog foreground color | `--primary-text-color` +`--paper-dialog` | Mixin applied to the dialog | `{}` +`--paper-dialog-title` | Mixin applied to the title (`

`) element | `{}` +`--paper-dialog-button-color` | Button area foreground color | `--default-primary-color` + +### Accessibility + +This element has `role="dialog"` by default. Depending on the context, it may be more appropriate +to override this attribute with `role="alertdialog"`. The header (a `

` element) will + +If `modal` is set, the element will set `aria-modal` and prevent the focus from exiting the element. +It will also ensure that focus remains in the dialog. + +The `aria-labelledby` attribute will be set to the header element, if one exists. + +@hero hero.svg +@demo demo/index.html +@polymerBehavior Polymer.PaperDialogBehavior +*/ + + Polymer.PaperDialogBehaviorImpl = { + + hostAttributes: { + 'role': 'dialog', + 'tabindex': '-1' + }, + + properties: { + + /** + * If `modal` is true, this implies `no-cancel-on-outside-click` and `with-backdrop`. + */ + modal: { + observer: '_modalChanged', + type: Boolean, + value: false + }, + + _lastFocusedElement: { + type: Node + }, + + _boundOnFocus: { + type: Function, + value: function() { + return this._onFocus.bind(this); + } + }, + + _boundOnBackdropClick: { + type: Function, + value: function() { + return this._onBackdropClick.bind(this); + } + } + + }, + + listeners: { + 'click': '_onDialogClick', + 'iron-overlay-opened': '_onIronOverlayOpened', + 'iron-overlay-closed': '_onIronOverlayClosed' + }, + + attached: function() { + this._observer = this._observe(this); + this._updateAriaLabelledBy(); + }, + + detached: function() { + if (this._observer) { + this._observer.disconnect(); + } + }, + + _observe: function(node) { + var observer = new MutationObserver(function() { + this._updateAriaLabelledBy(); + }.bind(this)); + observer.observe(node, { + childList: true, + subtree: true + }); + return observer; + }, + + _modalChanged: function() { + if (this.modal) { + this.setAttribute('aria-modal', 'true'); + } else { + this.setAttribute('aria-modal', 'false'); + } + // modal implies noCancelOnOutsideClick and withBackdrop if true, don't overwrite + // those properties otherwise. + if (this.modal) { + this.noCancelOnOutsideClick = true; + this.withBackdrop = true; + } + }, + + _updateAriaLabelledBy: function() { + var header = Polymer.dom(this).querySelector('h2'); + if (!header) { + this.removeAttribute('aria-labelledby'); + return; + } + var headerId = header.getAttribute('id'); + if (headerId && this.getAttribute('aria-labelledby') === headerId) { + return; + } + // set aria-describedBy to the header element + var labelledById; + if (headerId) { + labelledById = headerId; + } else { + labelledById = 'paper-dialog-header-' + new Date().getUTCMilliseconds(); + header.setAttribute('id', labelledById); + } + this.setAttribute('aria-labelledby', labelledById); + }, + + _updateClosingReasonConfirmed: function(confirmed) { + this.closingReason = this.closingReason || {}; + this.closingReason.confirmed = confirmed; + }, + + _onDialogClick: function(event) { + var target = event.target; + while (target !== this) { + if (target.hasAttribute('dialog-dismiss')) { + this._updateClosingReasonConfirmed(false); + this.close(); + break; + } else if (target.hasAttribute('dialog-confirm')) { + this._updateClosingReasonConfirmed(true); + this.close(); + break; + } + target = target.parentNode; + } + }, + + _onIronOverlayOpened: function() { + if (this.modal) { + document.body.addEventListener('focus', this._boundOnFocus, true); + this.backdropElement.addEventListener('click', this._boundOnBackdropClick); + } + }, + + _onIronOverlayClosed: function() { + document.body.removeEventListener('focus', this._boundOnFocus, true); + this.backdropElement.removeEventListener('click', this._boundOnBackdropClick); + }, + + _onFocus: function(event) { + if (this.modal) { + var target = event.target; + while (target && target !== this && target !== document.body) { + target = target.parentNode; + } + if (target) { + if (target === document.body) { + if (this._lastFocusedElement) { + this._lastFocusedElement.focus(); + } else { + this._focusNode.focus(); + } + } else { + this._lastFocusedElement = event.target; + } + } + } + }, + + _onBackdropClick: function() { + if (this.modal) { + if (this._lastFocusedElement) { + this._lastFocusedElement.focus(); + } else { + this._focusNode.focus(); + } + } + } + + }; + + /** @polymerBehavior */ + Polymer.PaperDialogBehavior = [Polymer.IronOverlayBehavior, Polymer.PaperDialogBehaviorImpl]; + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/bower.json b/homeassistant/components/frontend/www_static/polymer/bower.json index 230f81ad96a..b5ee548776c 100644 --- a/homeassistant/components/frontend/www_static/polymer/bower.json +++ b/homeassistant/components/frontend/www_static/polymer/bower.json @@ -10,38 +10,36 @@ "ignore": [ "bower_components" ], - "dependencies": { - "webcomponentsjs": "Polymer/webcomponentsjs#~0.6", - "font-roboto": "Polymer/font-roboto#~0.5.5", - "core-header-panel": "polymer/core-header-panel#~0.5.5", - "core-toolbar": "polymer/core-toolbar#~0.5.5", - "core-tooltip": "Polymer/core-tooltip#~0.5.5", - "core-menu": "polymer/core-menu#~0.5.5", - "core-item": "Polymer/core-item#~0.5.5", - "core-input": "Polymer/core-input#~0.5.5", - "core-icons": "polymer/core-icons#~0.5.5", - "core-image": "polymer/core-image#~0.5.5", - "core-style": "polymer/core-style#~0.5.5", - "core-label": "polymer/core-label#~0.5.5", - "paper-toast": "Polymer/paper-toast#~0.5.5", - "paper-dialog": "Polymer/paper-dialog#~0.5.5", - "paper-spinner": "Polymer/paper-spinner#~0.5.5", - "paper-button": "Polymer/paper-button#~0.5.5", - "paper-input": "Polymer/paper-input#~0.5.5", - "paper-toggle-button": "polymer/paper-toggle-button#~0.5.5", - "paper-icon-button": "polymer/paper-icon-button#~0.5.5", - "paper-menu-button": "polymer/paper-menu-button#~0.5.5", - "paper-dropdown": "polymer/paper-dropdown#~0.5.5", - "paper-item": "polymer/paper-item#~0.5.5", - "paper-slider": "polymer/paper-slider#~0.5.5", - "paper-checkbox": "polymer/paper-checkbox#~0.5.5", - "color-picker-element": "~0.0.2", - "google-apis": "GoogleWebComponents/google-apis#~0.4.4", - "core-drawer-panel": "polymer/core-drawer-panel#~0.5.5", - "core-scroll-header-panel": "polymer/core-scroll-header-panel#~0.5.5", - "moment": "~2.10.2" + "devDependencies": { + "polymer": "Polymer/polymer#^1.0.0", + "webcomponentsjs": "Polymer/webcomponentsjs#^0.7", + "paper-header-panel": "PolymerElements/paper-header-panel#^1.0.0", + "paper-toolbar": "PolymerElements/paper-toolbar#^1.0.0", + "paper-menu": "PolymerElements/paper-menu#^1.0.0", + "iron-input": "PolymerElements/iron-input#^1.0.0", + "iron-icons": "PolymerElements/iron-icons#^1.0.0", + "iron-image": "PolymerElements/iron-image#^1.0.0", + "paper-toast": "PolymerElements/paper-toast#^1.0.0", + "paper-dialog": "PolymerElements/paper-dialog#^1.0.0", + "paper-dialog-scrollable": "polymerelements/paper-dialog-scrollable#^1.0.0", + "paper-spinner": "PolymerElements/paper-spinner#^1.0.0", + "paper-button": "PolymerElements/paper-button#^1.0.0", + "paper-input": "PolymerElements/paper-input#^1.0.0", + "paper-toggle-button": "PolymerElements/paper-toggle-button#^1.0.0", + "paper-icon-button": "PolymerElements/paper-icon-button#^1.0.0", + "paper-item": "PolymerElements/paper-item#^1.0.0", + "paper-slider": "PolymerElements/paper-slider#^1.0.0", + "paper-checkbox": "PolymerElements/paper-checkbox#^1.0.0", + "paper-drawer-panel": "PolymerElements/paper-drawer-panel#^1.0.0", + "paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#~1.0", + "google-apis": "GoogleWebComponents/google-apis#0.8-preview", + "moment": "^2.10.3", + "layout": "Polymer/layout", + "color-picker-element": "~0.0.3", + "paper-styles": "polymerelements/paper-styles#~1.0" }, "resolutions": { - "webcomponentsjs": "~0.6" + "polymer": "^1.0.0", + "webcomponentsjs": "^0.7.0" } } diff --git a/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html b/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html index 195a9cb1109..6a25d41f945 100644 --- a/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html +++ b/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html @@ -1,15 +1,29 @@ - + - - + + - +