Merge branch 'dev' into refactor-media-player
commit
4cc8b0bda5
|
@ -41,6 +41,7 @@ omit =
|
|||
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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Adding support for a new device
|
||||
|
||||
For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
|
||||
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:
|
||||
|
||||
|
|
14
README.md
14
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:
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
@ -169,66 +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:
|
||||
if not self.track_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"
|
||||
|
||||
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))
|
||||
|
||||
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):
|
||||
|
@ -309,6 +271,44 @@ 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.
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
<link rel='shortcut icon' href='/static/favicon.ico' />
|
||||
<link rel='icon' type='image/png'
|
||||
href='/static/favicon-192x192.png' sizes='192x192'>
|
||||
<link rel='apple-touch-icon' sizes='192x192'
|
||||
href='/static/favicon-192x192.png'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "2d15135e9bfd0ee5b023d9abb79be62d"
|
||||
VERSION = "ed339673ca129a1a51dcc3975d0a492d"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -12326,9 +12326,9 @@ window.hass.uiUtil.formatDate = function(dateObj) {
|
|||
--paper-deep-orange-a400: #ff3d00;
|
||||
--paper-deep-orange-a700: #dd2c00;
|
||||
|
||||
--paper-brown-50: #795548;
|
||||
--paper-brown-100: #efebe9;
|
||||
--paper-brown-200: #d7ccc8;
|
||||
--paper-brown-50: #efebe9;
|
||||
--paper-brown-100: #d7ccc8;
|
||||
--paper-brown-200: #bcaaa4;
|
||||
--paper-brown-300: #a1887f;
|
||||
--paper-brown-400: #8d6e63;
|
||||
--paper-brown-500: #795548;
|
||||
|
@ -12569,13 +12569,18 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||
_previousValidInput: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
|
||||
_patternAlreadyChecked: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'input': '_onInput',
|
||||
'keydown': '_onKeydown'
|
||||
'keypress': '_onKeypress'
|
||||
},
|
||||
|
||||
get _patternRegExp() {
|
||||
|
@ -12600,33 +12605,54 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||
|
||||
_bindValueChanged: function() {
|
||||
if (this.value !== this.bindValue) {
|
||||
this.value = this.bindValue;
|
||||
this.value = !this.bindValue ? '' : this.bindValue;
|
||||
}
|
||||
// manually notify because we don't want to notify until after setting value
|
||||
this.fire('bind-value-changed', {value: this.bindValue});
|
||||
},
|
||||
|
||||
_onInput: function() {
|
||||
// Need to validate each of the characters pasted if they haven't
|
||||
// been validated inside `_onKeypress` already.
|
||||
if (this.preventInvalidInput && !this._patternAlreadyChecked) {
|
||||
var valid = this._checkPatternValidity();
|
||||
if (!valid) {
|
||||
this.value = this._previousValidInput;
|
||||
}
|
||||
}
|
||||
|
||||
this.bindValue = this.value;
|
||||
this._previousValidInput = this.value;
|
||||
this._patternAlreadyChecked = false;
|
||||
},
|
||||
|
||||
_isPrintable: function(keyCode) {
|
||||
var printable =
|
||||
(keyCode > 47 && keyCode < 58) || // number keys
|
||||
keyCode == 32 || keyCode == 13 || // spacebar & return key
|
||||
(keyCode > 64 && keyCode < 91) || // letter keys
|
||||
(keyCode > 95 && keyCode < 112) || // numpad keys
|
||||
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
|
||||
(keyCode > 218 && keyCode < 223); // [\]' (in order)
|
||||
return printable;
|
||||
_isPrintable: function(event) {
|
||||
// What a control/printable character is varies wildly based on the browser.
|
||||
// - most control characters (arrows, backspace) do not send a `keypress` event
|
||||
// in Chrome, but the *do* on Firefox
|
||||
// - in Firefox, when they do send a `keypress` event, control chars have
|
||||
// a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
|
||||
// - printable characters always send a keypress event.
|
||||
// - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
|
||||
// always matches the charCode.
|
||||
// None of this makes any sense.
|
||||
|
||||
var nonPrintable =
|
||||
(event.keyCode == 8) || // backspace
|
||||
(event.keyCode == 19) || // pause
|
||||
(event.keyCode == 20) || // caps lock
|
||||
(event.keyCode == 27) || // escape
|
||||
(event.keyCode == 45) || // insert
|
||||
(event.keyCode == 46) || // delete
|
||||
(event.keyCode == 144) || // num lock
|
||||
(event.keyCode == 145) || // scroll lock
|
||||
(event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, home, arrows
|
||||
(event.keyCode > 111 && event.keyCode < 124); // fn keys
|
||||
|
||||
return !(event.charCode == 0 && nonPrintable);
|
||||
},
|
||||
|
||||
// convert printable numpad keys to number keys
|
||||
_correctNumpadKeys: function(keyCode) {
|
||||
return (keyCode > 95 && keyCode < 112) ? keyCode - 48 : keyCode;
|
||||
},
|
||||
|
||||
_onKeydown: function(event) {
|
||||
_onKeypress: function(event) {
|
||||
if (!this.preventInvalidInput && this.type !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
@ -12634,12 +12660,33 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||
if (!regexp) {
|
||||
return;
|
||||
}
|
||||
var thisChar = String.fromCharCode(this._correctNumpadKeys(event.keyCode));
|
||||
if (this._isPrintable(event.keyCode) && !regexp.test(thisChar)) {
|
||||
|
||||
// Handle special keys and backspace
|
||||
if (event.metaKey || event.ctrlKey || event.altKey)
|
||||
return;
|
||||
|
||||
// Check the pattern either here or in `_onInput`, but not in both.
|
||||
this._patternAlreadyChecked = true;
|
||||
|
||||
var thisChar = String.fromCharCode(event.charCode);
|
||||
if (this._isPrintable(event) && !regexp.test(thisChar)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
_checkPatternValidity: function() {
|
||||
var regexp = this._patternRegExp;
|
||||
if (!regexp) {
|
||||
return true;
|
||||
}
|
||||
for (var i = 0; i < this.value.length; i++) {
|
||||
if (!regexp.test(this.value[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if `value` is valid. The validator provided in `validator` will be used first,
|
||||
* then any constraints.
|
||||
|
@ -12649,7 +12696,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||
// Empty, non-required input is valid.
|
||||
if (!this.required && this.value == '')
|
||||
return true;
|
||||
|
||||
|
||||
var valid;
|
||||
if (this.hasValidator()) {
|
||||
valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
|
||||
|
@ -13660,6 +13707,47 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
||||
Polymer({
|
||||
is: 'state-card-content',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (!newVal) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var newCardType = uiUtil.stateCardType(newVal);
|
||||
|
||||
if (!oldVal || uiUtil.stateCardType(oldVal) != newCardType) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + newCardType);
|
||||
stateCard.stateObj = newVal;
|
||||
root.appendChild(stateCard);
|
||||
} else {
|
||||
root.lastChild.stateObj = newVal;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
"use strict";
|
||||
|
@ -21995,7 +22083,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||
<div class="horizontal justified layout">
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
|
||||
<paper-toggle-button class="self-center" checked="[[toggleChecked]]" on-change="toggleChanged" on-click="toggleClicked">
|
||||
<paper-toggle-button class="self-center" checked="[[toggleChecked]]" on-change="toggleChanged" on-tap="toggleTapped">
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
|
||||
|
@ -22026,6 +22114,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||
this.forceStateChange();
|
||||
},
|
||||
|
||||
toggleTapped: function(ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
toggleChanged: function(ev) {
|
||||
var newVal = ev.target.checked;
|
||||
|
||||
|
@ -22187,6 +22279,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
|
@ -22236,55 +22329,6 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||
});
|
||||
})();
|
||||
</script>
|
||||
<dom-module id="state-card-content" assetpath="cards/">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
||||
Polymer({
|
||||
is: 'state-card-content',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (!newVal) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var newCardType = uiUtil.stateCardType(newVal);
|
||||
|
||||
if (!oldVal || uiUtil.stateCardType(oldVal) != newCardType) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + newCardType);
|
||||
stateCard.stateObj = newVal;
|
||||
root.appendChild(stateCard);
|
||||
} else {
|
||||
root.lastChild.stateObj = newVal;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<dom-module id="state-card" assetpath="cards/">
|
||||
<style>
|
||||
:host {
|
||||
|
@ -22320,10 +22364,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||
},
|
||||
|
||||
listeners: {
|
||||
'click': 'cardClicked',
|
||||
'tap': 'cardTapped',
|
||||
},
|
||||
|
||||
cardClicked: function() {
|
||||
cardTapped: function() {
|
||||
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
||||
},
|
||||
});
|
||||
|
@ -25792,7 +25836,6 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||
if (newVal) {
|
||||
this.volumeSliderValue = newVal.attributes.media_volume * 100;
|
||||
this.isMuted = newVal.attributes.media_is_volume_muted;
|
||||
console.log(this.isMuted);
|
||||
}
|
||||
|
||||
this.debounce('more-info-volume-animation-finish', function() {
|
||||
|
|
|
@ -7,14 +7,6 @@
|
|||
<link rel="import" href="state-card-scene.html">
|
||||
<link rel="import" href="state-card-media_player.html">
|
||||
|
||||
<dom-module id="state-card-content">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<paper-toggle-button class='self-center'
|
||||
checked="[[toggleChecked]]"
|
||||
on-change="toggleChanged"
|
||||
on-click="toggleClicked">
|
||||
on-tap="toggleTapped">
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
|
||||
|
@ -47,6 +47,10 @@
|
|||
this.forceStateChange();
|
||||
},
|
||||
|
||||
toggleTapped: function(ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
toggleChanged: function(ev) {
|
||||
var newVal = ev.target.checked;
|
||||
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
},
|
||||
|
||||
listeners: {
|
||||
'click': 'cardClicked',
|
||||
'tap': 'cardTapped',
|
||||
},
|
||||
|
||||
cardClicked: function() {
|
||||
cardTapped: function() {
|
||||
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
homeassistant.components.sensor.swiss_public_transport
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Swiss public transport sensor will give you the next two departure times
|
||||
from a given location to another one. This sensor is limited to Switzerland.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Swiss public transport sensor you will need to add something like
|
||||
the following to your config/configuration.yaml
|
||||
|
||||
sensor:
|
||||
platform: swiss_public_transport
|
||||
from: STATION_ID
|
||||
to: STATION_ID
|
||||
|
||||
Variables:
|
||||
|
||||
from
|
||||
*Required
|
||||
Start station/stop of your trip. To search for the ID of the station, use the
|
||||
an URL like this: http://transport.opendata.ch/v1/locations?query=Wankdorf
|
||||
to query for the station. If the score is 100 ("score":"100" in the response),
|
||||
it is a perfect match.
|
||||
|
||||
to
|
||||
*Required
|
||||
Destination station/stop of the trip. Same procedure as for the start station.
|
||||
|
||||
Details for the API : http://transport.opendata.ch
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from requests import get
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RESOURCE = 'http://transport.opendata.ch/v1/'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the Swiss public transport sensor. """
|
||||
|
||||
# journal contains [0] Station ID start, [1] Station ID destination
|
||||
# [2] Station name start, and [3] Station name destination
|
||||
journey = [config.get('from'), config.get('to')]
|
||||
try:
|
||||
for location in [config.get('from', None), config.get('to', None)]:
|
||||
# transport.opendata.ch doesn't play nice with requests.Session
|
||||
result = get(_RESOURCE + 'locations?query=%s' % location)
|
||||
journey.append(result.json()['stations'][0]['name'])
|
||||
except KeyError:
|
||||
_LOGGER.exception(
|
||||
"Unable to determine stations. "
|
||||
"Check your settings and/or the availability of opendata.ch")
|
||||
|
||||
return None
|
||||
|
||||
dev = []
|
||||
data = PublicTransportData(journey)
|
||||
dev.append(SwissPublicTransportSensor(data, journey))
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SwissPublicTransportSensor(Entity):
|
||||
""" Implements an Swiss public transport sensor. """
|
||||
|
||||
def __init__(self, data, journey):
|
||||
self.data = data
|
||||
self._name = '{}-{}'.format(journey[2], journey[3])
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
""" Gets the latest data from opendata.ch and updates the states. """
|
||||
times = self.data.update()
|
||||
try:
|
||||
self._state = ', '.join(times)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class PublicTransportData(object):
|
||||
""" Class for handling the data retrieval. """
|
||||
|
||||
def __init__(self, journey):
|
||||
self.start = journey[0]
|
||||
self.destination = journey[1]
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from opendata.ch. """
|
||||
|
||||
response = get(
|
||||
_RESOURCE +
|
||||
'connections?' +
|
||||
'from=' + self.start + '&' +
|
||||
'to=' + self.destination + '&' +
|
||||
'fields[]=connections/from/departureTimestamp/&' +
|
||||
'fields[]=connections/')
|
||||
|
||||
connections = response.json()['connections'][:2]
|
||||
|
||||
try:
|
||||
return [
|
||||
dt_util.datetime_to_short_time_str(
|
||||
dt_util.as_local(dt_util.utc_from_timestamp(
|
||||
item['from']['departureTimestamp']))
|
||||
)
|
||||
for item in connections
|
||||
]
|
||||
except KeyError:
|
||||
return ['n/a']
|
|
@ -0,0 +1,81 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
homeassistant.components.switch.command_switch
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Allows to configure custom shell commands to turn a switch on/off.
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
|
||||
import subprocess
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return switches controlled by shell commands. """
|
||||
|
||||
switches = config.get('switches', {})
|
||||
devices = []
|
||||
|
||||
for dev_name, properties in switches.items():
|
||||
devices.append(
|
||||
CommandSwitch(
|
||||
dev_name,
|
||||
properties.get('oncmd', 'true'),
|
||||
properties.get('offcmd', 'true')))
|
||||
|
||||
add_devices_callback(devices)
|
||||
|
||||
|
||||
class CommandSwitch(ToggleEntity):
|
||||
""" Represents a switch that can be togggled using shell commands """
|
||||
def __init__(self, name, command_on, command_off):
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = STATE_OFF
|
||||
self._command_on = command_on
|
||||
self._command_off = command_off
|
||||
|
||||
@staticmethod
|
||||
def _switch(command):
|
||||
""" Execute the actual commands """
|
||||
_LOGGER.info('Running command: %s', command)
|
||||
|
||||
success = (subprocess.call(command, shell=True) == 0)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error('Command failed: %s', command)
|
||||
|
||||
return success
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the switch """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the switch. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state == STATE_ON
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if CommandSwitch._switch(self._command_on):
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if CommandSwitch._switch(self._command_off):
|
||||
self._state = STATE_OFF
|
|
@ -0,0 +1,139 @@
|
|||
"""
|
||||
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
|
||||
|
||||
Configuration:
|
||||
|
||||
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
|
||||
username: YOUR_USERNAME
|
||||
password: YOUR_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
This is the IP address of your Hikvision camera. Example: 192.168.1.32
|
||||
|
||||
username
|
||||
*Required
|
||||
Your Hikvision camera username
|
||||
|
||||
password
|
||||
*Required
|
||||
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
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
import logging
|
||||
|
||||
try:
|
||||
import hikvision.api
|
||||
from hikvision.error import HikvisionError, MissingParamError
|
||||
except ImportError:
|
||||
hikvision.api = None
|
||||
|
||||
_LOGGING = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup Hikvision Camera config. """
|
||||
|
||||
host = config.get(CONF_HOST, None)
|
||||
port = config.get('port', "80")
|
||||
name = config.get('name', "Hikvision Camera Motion Detection")
|
||||
username = config.get(CONF_USERNAME, "admin")
|
||||
password = config.get(CONF_PASSWORD, "12345")
|
||||
|
||||
if hikvision.api is None:
|
||||
_LOGGING.error((
|
||||
"Failed to import hikvision. Did you maybe not install the "
|
||||
"'hikvision' dependency?"))
|
||||
|
||||
return False
|
||||
|
||||
try:
|
||||
hikvision_cam = hikvision.api.CreateDevice(
|
||||
host, port=port, username=username,
|
||||
password=password, is_https=False)
|
||||
except MissingParamError as param_err:
|
||||
_LOGGING.error("Missing required param: %s", param_err)
|
||||
return False
|
||||
except HikvisionError as conn_err:
|
||||
_LOGGING.error("Unable to connect: %s", conn_err)
|
||||
return False
|
||||
|
||||
add_devices_callback([
|
||||
HikvisionMotionSwitch(name, hikvision_cam)
|
||||
])
|
||||
|
||||
|
||||
class HikvisionMotionSwitch(ToggleEntity):
|
||||
|
||||
""" Provides a switch to toggle on/off motion detection. """
|
||||
|
||||
def __init__(self, name, hikvision_cam):
|
||||
self._name = name
|
||||
self._hikvision_cam = hikvision_cam
|
||||
self._state = STATE_OFF
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Poll for status regularly. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device if any. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state == STATE_ON
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
|
||||
_LOGGING.info("Turning on Motion Detection ")
|
||||
self._hikvision_cam.enable_motion_detection()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
|
||||
_LOGGING.info("Turning off Motion Detection ")
|
||||
self._hikvision_cam.disable_motion_detection()
|
||||
|
||||
def update(self):
|
||||
""" Update Motion Detection state """
|
||||
enabled = self._hikvision_cam.is_motion_detection_enabled()
|
||||
_LOGGING.info('enabled: %s', enabled)
|
||||
|
||||
self._state = STATE_ON if enabled else STATE_OFF
|
|
@ -80,11 +80,11 @@ class NestThermostat(ThermostatDevice):
|
|||
low, high = target
|
||||
|
||||
if self.current_temperature < low:
|
||||
target = low
|
||||
temp = low
|
||||
elif self.current_temperature > high:
|
||||
target = high
|
||||
temp = high
|
||||
else:
|
||||
target = low + high
|
||||
temp = (low + high)/2
|
||||
else:
|
||||
temp = target
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ python-nest>=2.3.1
|
|||
# Z-Wave (*.zwave)
|
||||
pydispatcher>=2.0.5
|
||||
|
||||
# ISY994 bindings (*.isy994
|
||||
# ISY994 bindings (*.isy994)
|
||||
PyISY>=1.0.2
|
||||
|
||||
# PSutil (sensor.systemmonitor)
|
||||
|
@ -61,3 +61,6 @@ blockchain>=1.1.2
|
|||
|
||||
# MPD Bindings (media_player.mpd)
|
||||
python-mpd2>=0.5.4
|
||||
|
||||
# Hikvision (switch.hikvisioncam)
|
||||
hikvision>=0.4
|
||||
|
|
|
@ -14,7 +14,8 @@ import homeassistant as ha
|
|||
import homeassistant.loader as loader
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM)
|
||||
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM,
|
||||
DEVICE_DEFAULT_NAME)
|
||||
import homeassistant.components.device_tracker as device_tracker
|
||||
|
||||
from helpers import get_test_home_assistant
|
||||
|
@ -96,7 +97,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||
# To ensure all the three expected lines are there, we sort the file
|
||||
with open(self.known_dev_path) as fil:
|
||||
self.assertEqual(
|
||||
['DEV1,unknown device,0,\n', 'DEV2,dev2,0,\n',
|
||||
['DEV1,{},0,\n'.format(DEVICE_DEFAULT_NAME), 'DEV2,dev2,0,\n',
|
||||
'device,name,track,picture\n'],
|
||||
sorted(fil))
|
||||
|
||||
|
|
Loading…
Reference in New Issue