mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Merge branch 'dev' into refactor-media-player
This commit is contained in:
commit
4cc8b0bda5
@ -41,6 +41,7 @@ omit =
|
|||||||
homeassistant/components/sensor/mysensors.py
|
homeassistant/components/sensor/mysensors.py
|
||||||
homeassistant/components/sensor/openweathermap.py
|
homeassistant/components/sensor/openweathermap.py
|
||||||
homeassistant/components/sensor/sabnzbd.py
|
homeassistant/components/sensor/sabnzbd.py
|
||||||
|
homeassistant/components/sensor/swiss_public_transport.py
|
||||||
homeassistant/components/sensor/systemmonitor.py
|
homeassistant/components/sensor/systemmonitor.py
|
||||||
homeassistant/components/sensor/time_date.py
|
homeassistant/components/sensor/time_date.py
|
||||||
homeassistant/components/sensor/transmission.py
|
homeassistant/components/sensor/transmission.py
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Adding support for a new device
|
# 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:
|
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:
|
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 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
|
* 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 [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) and [Music Player Daemon](http://www.musicpd.org/)
|
||||||
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast)
|
* 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 services by monitoring `ps` output
|
* Track running system services and monitoring your system stats (Memory, disk usage, and more)
|
||||||
* Track and control [Tellstick devices and sensors](http://www.telldus.se/products/tellstick)
|
* 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 the lights when people get home after sun set
|
||||||
* Turn on lights slowly during sun set to compensate for light loss
|
* Turn on lights slowly during sun set to compensate for light loss
|
||||||
* Turn off all lights and devices when everybody leaves the house
|
* Turn off all lights and devices when everybody leaves the house
|
||||||
* Offers web interface to monitor and control Home Assistant
|
* 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
|
* 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)
|
* [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:
|
Home Assistant also includes functionality for controlling HTPCs:
|
||||||
|
|
||||||
|
@ -235,7 +235,12 @@ def process_ha_core_config(hass, config):
|
|||||||
|
|
||||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||||
|
|
||||||
|
customize = config.get(CONF_CUSTOMIZE)
|
||||||
|
|
||||||
|
if isinstance(customize, dict):
|
||||||
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
|
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())
|
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
|
||||||
|
|
||||||
if CONF_TEMPERATURE_UNIT in config:
|
if CONF_TEMPERATURE_UNIT in config:
|
||||||
|
@ -17,7 +17,7 @@ import homeassistant.util.dt as dt_util
|
|||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
|
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
|
||||||
CONF_PLATFORM)
|
CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||||
from homeassistant.components import group
|
from homeassistant.components import group
|
||||||
|
|
||||||
DOMAIN = "device_tracker"
|
DOMAIN = "device_tracker"
|
||||||
@ -169,6 +169,7 @@ class DeviceTracker(object):
|
|||||||
if not self.lock.acquire(False):
|
if not self.lock.acquire(False):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
found_devices = set(dev.upper() for dev in
|
found_devices = set(dev.upper() for dev in
|
||||||
self.device_scanner.scan_devices())
|
self.device_scanner.scan_devices())
|
||||||
|
|
||||||
@ -187,47 +188,8 @@ class DeviceTracker(object):
|
|||||||
if not self.track_new_devices:
|
if not self.track_new_devices:
|
||||||
self.untracked_devices.update(new_devices)
|
self.untracked_devices.update(new_devices)
|
||||||
|
|
||||||
# Write new devices to known devices file
|
self._update_known_devices_file(new_devices)
|
||||||
if not self.invalid_known_devices_file:
|
finally:
|
||||||
|
|
||||||
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.lock.release()
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
@ -309,6 +271,44 @@ class DeviceTracker(object):
|
|||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
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):
|
def _track_device(self, device, name):
|
||||||
"""
|
"""
|
||||||
Add a device to the list of tracked devices.
|
Add a device to the list of tracked devices.
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
<link rel='shortcut icon' href='/static/favicon.ico' />
|
<link rel='shortcut icon' href='/static/favicon.ico' />
|
||||||
<link rel='icon' type='image/png'
|
<link rel='icon' type='image/png'
|
||||||
href='/static/favicon-192x192.png' sizes='192x192'>
|
href='/static/favicon-192x192.png' sizes='192x192'>
|
||||||
<link rel='apple-touch-icon' sizes='192x192'
|
<link rel='apple-touch-icon' sizes='180x180'
|
||||||
href='/static/favicon-192x192.png'>
|
href='/static/favicon-apple-180x180.png'>
|
||||||
<meta name='theme-color' content='#03a9f4'>
|
<meta name='theme-color' content='#03a9f4'>
|
||||||
</head>
|
</head>
|
||||||
<body fullbleed>
|
<body fullbleed>
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
""" 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-a400: #ff3d00;
|
||||||
--paper-deep-orange-a700: #dd2c00;
|
--paper-deep-orange-a700: #dd2c00;
|
||||||
|
|
||||||
--paper-brown-50: #795548;
|
--paper-brown-50: #efebe9;
|
||||||
--paper-brown-100: #efebe9;
|
--paper-brown-100: #d7ccc8;
|
||||||
--paper-brown-200: #d7ccc8;
|
--paper-brown-200: #bcaaa4;
|
||||||
--paper-brown-300: #a1887f;
|
--paper-brown-300: #a1887f;
|
||||||
--paper-brown-400: #8d6e63;
|
--paper-brown-400: #8d6e63;
|
||||||
--paper-brown-500: #795548;
|
--paper-brown-500: #795548;
|
||||||
@ -12569,13 +12569,18 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||||||
_previousValidInput: {
|
_previousValidInput: {
|
||||||
type: String,
|
type: String,
|
||||||
value: ''
|
value: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
_patternAlreadyChecked: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
listeners: {
|
listeners: {
|
||||||
'input': '_onInput',
|
'input': '_onInput',
|
||||||
'keydown': '_onKeydown'
|
'keypress': '_onKeypress'
|
||||||
},
|
},
|
||||||
|
|
||||||
get _patternRegExp() {
|
get _patternRegExp() {
|
||||||
@ -12600,33 +12605,54 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||||||
|
|
||||||
_bindValueChanged: function() {
|
_bindValueChanged: function() {
|
||||||
if (this.value !== this.bindValue) {
|
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
|
// manually notify because we don't want to notify until after setting value
|
||||||
this.fire('bind-value-changed', {value: this.bindValue});
|
this.fire('bind-value-changed', {value: this.bindValue});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onInput: function() {
|
_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.bindValue = this.value;
|
||||||
|
this._previousValidInput = this.value;
|
||||||
|
this._patternAlreadyChecked = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
_isPrintable: function(keyCode) {
|
_isPrintable: function(event) {
|
||||||
var printable =
|
// What a control/printable character is varies wildly based on the browser.
|
||||||
(keyCode > 47 && keyCode < 58) || // number keys
|
// - most control characters (arrows, backspace) do not send a `keypress` event
|
||||||
keyCode == 32 || keyCode == 13 || // spacebar & return key
|
// in Chrome, but the *do* on Firefox
|
||||||
(keyCode > 64 && keyCode < 91) || // letter keys
|
// - in Firefox, when they do send a `keypress` event, control chars have
|
||||||
(keyCode > 95 && keyCode < 112) || // numpad keys
|
// a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
|
||||||
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
|
// - printable characters always send a keypress event.
|
||||||
(keyCode > 218 && keyCode < 223); // [\]' (in order)
|
// - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
|
||||||
return printable;
|
// 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
|
_onKeypress: function(event) {
|
||||||
_correctNumpadKeys: function(keyCode) {
|
|
||||||
return (keyCode > 95 && keyCode < 112) ? keyCode - 48 : keyCode;
|
|
||||||
},
|
|
||||||
|
|
||||||
_onKeydown: function(event) {
|
|
||||||
if (!this.preventInvalidInput && this.type !== 'number') {
|
if (!this.preventInvalidInput && this.type !== 'number') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -12634,12 +12660,33 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||||||
if (!regexp) {
|
if (!regexp) {
|
||||||
return;
|
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();
|
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,
|
* Returns true if `value` is valid. The validator provided in `validator` will be used first,
|
||||||
* then any constraints.
|
* then any constraints.
|
||||||
@ -13660,6 +13707,47 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|||||||
}
|
}
|
||||||
</style>
|
</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>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
"use strict";
|
"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">
|
<div class="horizontal justified layout">
|
||||||
<state-info state-obj="[[stateObj]]"></state-info>
|
<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>
|
</paper-toggle-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -22026,6 +22114,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||||||
this.forceStateChange();
|
this.forceStateChange();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleTapped: function(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
toggleChanged: function(ev) {
|
toggleChanged: function(ev) {
|
||||||
var newVal = ev.target.checked;
|
var newVal = ev.target.checked;
|
||||||
|
|
||||||
@ -22187,6 +22279,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||||||
.state {
|
.state {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-text {
|
.main-text {
|
||||||
@ -22236,55 +22329,6 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</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/">
|
<dom-module id="state-card" assetpath="cards/">
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
@ -22320,10 +22364,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
|||||||
},
|
},
|
||||||
|
|
||||||
listeners: {
|
listeners: {
|
||||||
'click': 'cardClicked',
|
'tap': 'cardTapped',
|
||||||
},
|
},
|
||||||
|
|
||||||
cardClicked: function() {
|
cardTapped: function() {
|
||||||
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
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) {
|
if (newVal) {
|
||||||
this.volumeSliderValue = newVal.attributes.media_volume * 100;
|
this.volumeSliderValue = newVal.attributes.media_volume * 100;
|
||||||
this.isMuted = newVal.attributes.media_is_volume_muted;
|
this.isMuted = newVal.attributes.media_is_volume_muted;
|
||||||
console.log(this.isMuted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debounce('more-info-volume-animation-finish', function() {
|
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-scene.html">
|
||||||
<link rel="import" href="state-card-media_player.html">
|
<link rel="import" href="state-card-media_player.html">
|
||||||
|
|
||||||
<dom-module id="state-card-content">
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</dom-module>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var uiUtil = window.hass.uiUtil;
|
var uiUtil = window.hass.uiUtil;
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
.state {
|
.state {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-text {
|
.main-text {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<paper-toggle-button class='self-center'
|
<paper-toggle-button class='self-center'
|
||||||
checked="[[toggleChecked]]"
|
checked="[[toggleChecked]]"
|
||||||
on-change="toggleChanged"
|
on-change="toggleChanged"
|
||||||
on-click="toggleClicked">
|
on-tap="toggleTapped">
|
||||||
</paper-toggle-button>
|
</paper-toggle-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -47,6 +47,10 @@
|
|||||||
this.forceStateChange();
|
this.forceStateChange();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleTapped: function(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
toggleChanged: function(ev) {
|
toggleChanged: function(ev) {
|
||||||
var newVal = ev.target.checked;
|
var newVal = ev.target.checked;
|
||||||
|
|
||||||
|
@ -37,10 +37,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
listeners: {
|
listeners: {
|
||||||
'click': 'cardClicked',
|
'tap': 'cardTapped',
|
||||||
},
|
},
|
||||||
|
|
||||||
cardClicked: function() {
|
cardTapped: function() {
|
||||||
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
132
homeassistant/components/sensor/swiss_public_transport.py
Normal file
132
homeassistant/components/sensor/swiss_public_transport.py
Normal file
@ -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']
|
81
homeassistant/components/switch/command_switch.py
Normal file
81
homeassistant/components/switch/command_switch.py
Normal file
@ -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
|
139
homeassistant/components/switch/hikvisioncam.py
Normal file
139
homeassistant/components/switch/hikvisioncam.py
Normal file
@ -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
|
low, high = target
|
||||||
|
|
||||||
if self.current_temperature < low:
|
if self.current_temperature < low:
|
||||||
target = low
|
temp = low
|
||||||
elif self.current_temperature > high:
|
elif self.current_temperature > high:
|
||||||
target = high
|
temp = high
|
||||||
else:
|
else:
|
||||||
target = low + high
|
temp = (low + high)/2
|
||||||
else:
|
else:
|
||||||
temp = target
|
temp = target
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ python-nest>=2.3.1
|
|||||||
# Z-Wave (*.zwave)
|
# Z-Wave (*.zwave)
|
||||||
pydispatcher>=2.0.5
|
pydispatcher>=2.0.5
|
||||||
|
|
||||||
# ISY994 bindings (*.isy994
|
# ISY994 bindings (*.isy994)
|
||||||
PyISY>=1.0.2
|
PyISY>=1.0.2
|
||||||
|
|
||||||
# PSutil (sensor.systemmonitor)
|
# PSutil (sensor.systemmonitor)
|
||||||
@ -61,3 +61,6 @@ blockchain>=1.1.2
|
|||||||
|
|
||||||
# MPD Bindings (media_player.mpd)
|
# MPD Bindings (media_player.mpd)
|
||||||
python-mpd2>=0.5.4
|
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.loader as loader
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import (
|
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
|
import homeassistant.components.device_tracker as device_tracker
|
||||||
|
|
||||||
from helpers import get_test_home_assistant
|
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
|
# To ensure all the three expected lines are there, we sort the file
|
||||||
with open(self.known_dev_path) as fil:
|
with open(self.known_dev_path) as fil:
|
||||||
self.assertEqual(
|
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'],
|
'device,name,track,picture\n'],
|
||||||
sorted(fil))
|
sorted(fil))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user