Merge pull request #59 from balloob/dev

Update master with latest changes
This commit is contained in:
Paulus Schoutsen 2015-03-14 12:31:28 -07:00
commit ebd597b811
33 changed files with 819 additions and 188 deletions

View File

@ -24,6 +24,9 @@ omit =
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/light/vera.py
homeassistant/components/sensor/vera.py
homeassistant/components/switch/vera.py
[report]

3
.gitmodules vendored
View File

@ -13,3 +13,6 @@
[submodule "homeassistant/components/frontend/www_static/polymer/home-assistant-js"]
path = homeassistant/components/frontend/www_static/polymer/home-assistant-js
url = https://github.com/balloob/home-assistant-js.git
[submodule "homeassistant/external/vera"]
path = homeassistant/external/vera
url = https://github.com/jamespcole/home-assistant-vera-api.git

View File

@ -5,8 +5,6 @@ homeassistant.components.automation.event
Offers event listening automation rules.
"""
import logging
import json
from homeassistant.util import convert
CONF_EVENT_TYPE = "event_type"
CONF_EVENT_DATA = "event_data"
@ -22,7 +20,7 @@ def register(hass, config, action):
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
return False
event_data = convert(config.get(CONF_EVENT_DATA), json.loads, {})
event_data = config.get(CONF_EVENT_DATA, {})
def handle_event(event):
""" Listens for events and calls the action when data matches. """

View File

@ -0,0 +1,72 @@
"""
homeassistant.components.conversation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to have conversations with Home Assistant.
This is more a proof of concept.
"""
import logging
import re
import homeassistant
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
DOMAIN = "conversation"
DEPENDENCIES = []
SERVICE_PROCESS = "process"
ATTR_TEXT = "text"
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
def setup(hass, config):
""" Registers the process service. """
logger = logging.getLogger(__name__)
def process(service):
""" Parses text into commands for Home Assistant. """
if ATTR_TEXT not in service.data:
logger.error("Received process service call without a text")
return
text = service.data[ATTR_TEXT].lower()
match = REGEX_TURN_COMMAND.match(text)
if not match:
logger.error("Unable to process: %s", text)
return
name, command = match.groups()
entity_ids = [
state.entity_id for state in hass.states.all()
if state.attributes.get(ATTR_FRIENDLY_NAME, "").lower() == name]
if not entity_ids:
logger.error(
"Could not find entity id %s from text %s", name, text)
return
if command == 'on':
hass.services.call(
homeassistant.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
elif command == 'off':
hass.services.call(
homeassistant.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
else:
logger.error(
'Got unsupported command %s from text %s', command, text)
hass.services.register(DOMAIN, SERVICE_PROCESS, process)
return True

View File

@ -10,7 +10,7 @@ import homeassistant as ha
import homeassistant.bootstrap as bootstrap
import homeassistant.loader as loader
from homeassistant.const import (
CONF_PLATFORM, ATTR_ENTITY_PICTURE, STATE_ON,
CONF_PLATFORM, ATTR_ENTITY_PICTURE,
CONF_LATITUDE, CONF_LONGITUDE)
DOMAIN = "demo"
@ -52,9 +52,6 @@ def setup(hass, config):
group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0]])
group.setup_group(hass, 'bedroom', [lights[2], switches[1]])
# Setup process
hass.states.set("process.XBMC", STATE_ON)
# Setup device tracker
hass.states.set("device_tracker.Paulus", "home",
{ATTR_ENTITY_PICTURE:

View File

@ -1,6 +1,6 @@
""" Supports scanning using nmap. """
import logging
from datetime import timedelta
from datetime import timedelta, datetime
import threading
from collections import namedtuple
import subprocess
@ -11,7 +11,7 @@ from libnmap.parser import NmapParser, NmapParserException
from homeassistant.const import CONF_HOSTS
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.util import Throttle, convert
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
@ -19,6 +19,9 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
# interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
def get_scanner(hass, config):
""" Validates config and returns a Nmap scanner. """
@ -30,7 +33,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "name"])
Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
def _arp(ip_address):
@ -53,6 +56,8 @@ class NmapDeviceScanner(object):
self.lock = threading.Lock()
self.hosts = config[CONF_HOSTS]
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes)
self.success_init = True
self._update_info()
@ -77,6 +82,33 @@ class NmapDeviceScanner(object):
else:
return None
def _parse_results(self, stdout):
""" Parses results from an nmap scan.
Returns True if successful, False otherwise. """
try:
results = NmapParser.parse(stdout)
now = datetime.now()
self.last_results = []
for host in results.hosts:
if host.is_up():
if host.hostnames:
name = host.hostnames[0]
else:
name = host.ipv4
if host.mac:
mac = host.mac
else:
mac = _arp(host.ipv4)
if mac:
device = Device(mac, name, host.ipv4, now)
self.last_results.append(device)
_LOGGER.info("nmap scan successful")
return True
except NmapParserException as parse_exc:
_LOGGER.error("failed to parse nmap results: %s", parse_exc.msg)
self.last_results = []
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Scans the network for devices.
@ -87,35 +119,24 @@ class NmapDeviceScanner(object):
with self.lock:
_LOGGER.info("Scanning")
nmap = NmapProcess(targets=self.hosts, options="-F")
options = "-F"
exclude_targets = set()
if self.home_interval:
now = datetime.now()
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)
if len(exclude_targets) > 0:
target_list = [t.ip for t in exclude_targets]
options += " --exclude {}".format(",".join(target_list))
nmap = NmapProcess(targets=self.hosts, options=options)
nmap.run()
if nmap.rc == 0:
try:
results = NmapParser.parse(nmap.stdout)
self.last_results = []
for host in results.hosts:
if host.is_up():
if host.hostnames:
name = host.hostnames[0]
else:
name = host.ipv4
if host.mac:
mac = host.mac
else:
mac = _arp(host.ipv4)
if mac:
device = Device(mac, name)
self.last_results.append(device)
_LOGGER.info("nmap scan successful")
return True
except NmapParserException as parse_exc:
_LOGGER.error("failed to parse nmap results: %s",
parse_exc.msg)
self.last_results = []
return False
if self._parse_results(nmap.stdout):
self.last_results.extend(exclude_targets)
else:
self.last_results = []
_LOGGER.error(nmap.stderr)

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "1c265f0f07e6038c2cbb9b277e58b994"
VERSION = "08fb2ffccc72d7bfa0ad3478f2e8cfe7"

File diff suppressed because one or more lines are too long

View File

@ -1,72 +1,24 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-icons/social-icons.html">
<link rel="import" href="../bower_components/core-icons/image-icons.html">
<link rel="import" href="../bower_components/core-icons/hardware-icons.html">
<link rel="import" href="../resources/home-assistant-icons.html">
<polymer-element name="domain-icon"
attributes="domain state" constructor="DomainIcon">
<template>
<core-icon icon="{{icon(domain, state)}}"></core-icon>
<core-icon icon="{{icon}}"></core-icon>
</template>
<script>
Polymer({
icon: '',
icon: function(domain, state) {
switch(domain) {
case "homeassistant":
return "home";
case "group":
return "homeassistant-24:group";
case "device_tracker":
return "social:person";
case "switch":
return "image:flash-on";
case "media_player":
var icon = "hardware:cast";
if (state !== "idle") {
icon += "-connected";
}
return icon;
case "process":
return "hardware:memory";
case "sun":
return "image:wb-sunny";
case "light":
return "image:wb-incandescent";
case "simple_alarm":
return "social:notifications";
case "notify":
return "announcement";
case "thermostat":
return "homeassistant-100:thermostat";
case "sensor":
return "visibility";
case "configurator":
return "settings";
default:
return "bookmark-outline";
}
}
observe: {
'domain': 'updateIcon',
'state' : 'updateIcon',
},
updateIcon: function() {
this.icon = window.hass.uiUtil.domainIcon(this.domain, this.state);
},
});
</script>
</polymer-element>

View File

@ -22,9 +22,9 @@
</template>
<div>
<template repeat="{{state in states}}">
<template repeat="{{entityID in entityIDs}}">
<div class='eventContainer'>
<a on-click={{handleClick}}>{{state.entityId}}</a>
<a on-click={{handleClick}}>{{entityID}}</a>
</div>
</template>
@ -35,7 +35,7 @@
Polymer(Polymer.mixin({
cbEventClicked: null,
states: [],
entityIDs: [],
attached: function() {
this.listenToStores(true);
@ -46,7 +46,7 @@
},
stateStoreChanged: function(stateStore) {
this.states = stateStore.all();
this.entityIDs = stateStore.entityIDs.toArray();
},
handleClick: function(ev) {

View File

@ -48,7 +48,7 @@
},
eventStoreChanged: function(eventStore) {
this.events = eventStore.all();
this.events = eventStore.all;
},
handleClick: function(ev) {

View File

@ -11,7 +11,7 @@
Polymer(Polymer.mixin({
lastId: null,
ready: function() {
attached: function() {
this.listenToStores(true);
},
@ -22,7 +22,7 @@
notificationStoreChanged: function(notificationStore) {
if (notificationStore.hasNewNotifications(this.lastId)) {
var toast = this.$.toast;
var notification = notificationStore.getLastNotification();
var notification = notificationStore.lastNotification;
this.lastId = notification.id;
toast.text = notification.message;

View File

@ -7,35 +7,37 @@
{{ relativeTime }}
</template>
<script>
var UPDATE_INTERVAL = 60000; // 60 seconds
(function() {
var UPDATE_INTERVAL = 60000; // 60 seconds
var parseDateTime = window.hass.util.parseDateTime;
var parseDateTime = window.hass.util.parseDateTime;
Polymer({
relativeTime: "",
parsedDateTime: null,
Polymer({
relativeTime: "",
parsedDateTime: null,
created: function() {
this.updateRelative = this.updateRelative.bind(this);
},
created: function() {
this.updateRelative = this.updateRelative.bind(this);
},
attached: function() {
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
},
attached: function() {
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
},
detached: function() {
clearInterval(this._interval);
},
detached: function() {
clearInterval(this._interval);
},
datetimeChanged: function(oldVal, newVal) {
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
datetimeChanged: function(oldVal, newVal) {
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
this.updateRelative();
},
this.updateRelative();
},
updateRelative: function() {
this.relativeTime = this.parsedDateTime ? moment(this.parsedDateTime).fromNow() : "";
},
});
updateRelative: function() {
this.relativeTime = this.parsedDateTime ? moment(this.parsedDateTime).fromNow() : "";
},
});
})();
</script>
</polymer-element>

View File

@ -34,10 +34,12 @@
<div>
<core-menu selected="0">
<template repeat="{{serv in services}}">
<core-submenu icon="{{serv.domain | getIcon}}" label="{{serv.domain}}">
<template repeat="{{service in serv.services}}">
<a on-click={{serviceClicked}} data-domain={{serv.domain}}>{{service}}</a>
<template repeat="{{domain in domains}}">
<core-submenu icon="{{domain | getIcon}}" label="{{domain}}">
<template repeat="{{service in domain | getServices}}">
<a on-click={{serviceClicked}} data-domain={{domain}}>
{{service}}
</a>
</template>
</core-submenu>
</template>
@ -50,7 +52,8 @@
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
services: [],
domains: [],
services: null,
cbServiceClicked: null,
attached: function() {
@ -62,11 +65,16 @@
},
getIcon: function(domain) {
return (new DomainIcon()).icon(domain);
return hass.uiUtil.domainIcon(domain);
},
getServices: function(domain) {
return this.services.get(domain).toArray();
},
serviceStoreChanged: function(serviceStore) {
this.services = serviceStore.all();
this.services = serviceStore.all;
this.domains = this.services.keySeq().sort().toArray();
},
serviceClicked: function(ev) {

View File

@ -42,17 +42,15 @@
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming();
this.hasError = streamStore.hasError();
this.$.toggle.checked = this.isStreaming;
this.hasError = streamStore.hasError;
this.$.toggle.checked = this.isStreaming = streamStore.isStreaming;
},
toggleChanged: function(ev) {
if (this.isStreaming) {
streamActions.stop();
} else {
streamActions.start(authStore.getAuthToken());
streamActions.start(authStore.authToken);
}
},
}, storeListenerMixIn));

@ -1 +1 @@
Subproject commit 8ea3a9e858a8c39d4c3aa46b719362b33f4a358f
Subproject commit 642a83e437fed356db3e13d5a5b0c28d4b3fb713

View File

@ -44,8 +44,8 @@
// if auth was given, tell the backend
if(this.auth) {
uiActions.validateAuth(this.auth, false);
} else if (preferenceStore.hasAuthToken()) {
uiActions.validateAuth(preferenceStore.getAuthToken(), false);
} else if (preferenceStore.hasAuthToken) {
uiActions.validateAuth(preferenceStore.authToken, false);
}
},
@ -58,7 +58,7 @@
},
syncStoreChanged: function(syncStore) {
this.loaded = syncStore.initialLoadDone();
this.loaded = syncStore.initialLoadDone;
},
}, storeListenerMixIn));
</script>

View File

@ -172,10 +172,8 @@ Polymer(Polymer.mixin({
},
streamStoreChanged: function(streamStore) {
var state = streamStore.getState();
this.isStreaming = state === streamStore.STATE_CONNECTED;
this.hasStreamError = state === streamStore.STATE_ERROR;
this.isStreaming = streamStore.isStreaming;
this.hasStreamError = streamStore.hasError;
},
menuSelect: function(ev, detail, sender) {

View File

@ -63,7 +63,8 @@
<div horizontal center layout>
<core-label horizontal layout>
<paper-checkbox for checked={{rememberLogin}}></paper-checkbox><b>Remember</b>
<paper-checkbox for checked={{rememberLogin}}></paper-checkbox>
Remember
</core-label>
<paper-button on-click={{validatePassword}}>Log In</paper-button>
@ -106,12 +107,12 @@
},
authStoreChanged: function(authStore) {
this.isValidating = authStore.isValidating();
this.isLoggedIn = authStore.isLoggedIn();
this.isValidating = authStore.isValidating;
this.isLoggedIn = authStore.isLoggedIn;
this.spinnerMessage = this.isValidating ? this.MSG_VALIDATING : this.MSG_LOADING_DATA;
if (authStore.wasLastAttemptInvalid()) {
this.$.passwordDecorator.error = authStore.getLastAttemptMessage();
if (authStore.lastAttemptInvalid) {
this.$.passwordDecorator.error = authStore.lastAttemptMessage;
this.$.passwordDecorator.isInvalid = true;
}

View File

@ -50,7 +50,7 @@
stateHistoryActions.fetchAll();
}
this.stateHistory = stateHistoryStore.all();
this.stateHistory = stateHistoryStore.all;
},
handleRefreshClick: function() {

View File

@ -1,4 +1,5 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
@ -10,14 +11,47 @@
<template>
<core-style ref="ha-animations"></core-style>
<style>
.listening {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
padding: 16px;
background-color: rgba(255, 255, 255, 0.95);
line-height: 2em;
cursor: pointer;
}
.interimTranscript {
color: darkgrey;
}
.listening paper-spinner {
float: right;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>{{headerTitle}}</span>
<span header-buttons>
<paper-icon-button icon="refresh" class="{{isFetching && 'ha-spin'}}"
on-click="{{handleRefreshClick}}" hidden?="{{isStreaming}}"></paper-icon-button>
<paper-icon-button icon="{{isListening ? 'av:mic-off' : 'av:mic' }}" hidden?={{!canListen}}
on-click="{{handleListenClick}}"></paper-icon-button>
</span>
<div class='listening' hidden?="{{!isListening && !isTransmitting}}" on-click={{handleListenClick}}>
<core-icon icon="av:hearing"></core-icon> {{finalTranscript}}
<span class='interimTranscript'>{{interimTranscript}}</span>
<paper-spinner active?="{{isTransmitting}}"></paper-spinner>
</div>
<state-cards states="{{states}}">
<h3>Hi there!</h3>
<p>
@ -32,14 +66,29 @@
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var syncActions = window.hass.syncActions;
var voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var stateGroupFilter = function(state) { return state.domain === 'group'; };
Polymer(Polymer.mixin({
headerTitle: "States",
states: [],
isFetching: false,
isStreaming: false,
canListen: false,
voiceSupported: false,
hasConversationComponent: false,
isListening: false,
isTransmittingVoice: false,
interimTranscript: '',
finalTranscript: '',
ready: function() {
this.voiceSupported = voiceActions.isSupported();
},
attached: function() {
this.listenToStores(true);
},
@ -48,16 +97,29 @@
this.stopListeningToStores();
},
componentStoreChanged: function(componentStore) {
this.canListen = this.voiceSupported &&
componentStore.isLoaded('conversation');
},
stateStoreChanged: function() {
this.refreshStates();
},
syncStoreChanged: function(syncStore) {
this.isFetching = syncStore.isFetching();
this.isFetching = syncStore.isFetching;
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming();
this.isStreaming = streamStore.isStreaming;
},
voiceStoreChanged: function(voiceStore) {
this.isListening = voiceStore.isListening;
this.isTransmitting = voiceStore.isTransmitting;
this.finalTranscript = voiceStore.finalTranscript;
this.interimTranscript = voiceStore.interimTranscript.slice(
this.finalTranscript.length)
},
filterChanged: function() {
@ -75,20 +137,28 @@
},
refreshStates: function() {
if (this.filter == 'group') {
this.states = _.filter(stateStore.all(), function(state) {
return state.domain === 'group';
});
var states = stateStore.all;
if (this.filter === 'group') {
states = states.filter(stateGroupFilter);
} else {
this.states = _.filter(stateStore.all(), function(state) {
return state.domain !== 'group';
});
states = states.filterNot(stateGroupFilter);
}
this.states = states.toArray();
},
handleRefreshClick: function() {
syncActions.fetchAll();
},
handleListenClick: function() {
if (this.isListening) {
voiceActions.stop();
} else {
voiceActions.listen();
}
},
}, storeListenerMixIn));
</script>
</polymer>
</polymer>

View File

@ -84,7 +84,7 @@
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming();
this.isStreaming = streamStore.isStreaming;
},
submitClicked: function() {

View File

@ -41,11 +41,8 @@ Polymer(Polymer.mixin({
},
updateStates: function() {
if (this.stateObj && this.stateObj.attributes.entity_id) {
this.states = stateStore.gets(this.stateObj.attributes.entity_id);
} else {
this.states = [];
}
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
},
}, storeListenerMixIn));
</script>

View File

@ -1,6 +1,12 @@
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-icons/social-icons.html">
<link rel="import" href="../bower_components/core-icons/image-icons.html">
<link rel="import" href="../bower_components/core-icons/hardware-icons.html">
<link rel="import" href="../bower_components/core-icons/av-icons.html">
<core-iconset-svg id="homeassistant-100" iconSize="100">
<svg><defs>
<g id="thermostat">
@ -26,3 +32,57 @@
</defs></svg>
</core-iconset-svg>
<script>
window.hass.uiUtil.domainIcon = function(domain, state) {
switch(domain) {
case "homeassistant":
return "home";
case "group":
return "homeassistant-24:group";
case "device_tracker":
return "social:person";
case "switch":
return "image:flash-on";
case "media_player":
var icon = "hardware:cast";
if (state !== "idle") {
icon += "-connected";
}
return icon;
case "sun":
return "image:wb-sunny";
case "light":
return "image:wb-incandescent";
case "simple_alarm":
return "social:notifications";
case "notify":
return "announcement";
case "thermostat":
return "homeassistant-100:thermostat";
case "sensor":
return "visibility";
case "configurator":
return "settings";
case "conversation":
return "av:hearing";
default:
return "bookmark-outline";
}
}
</script>

View File

@ -63,10 +63,13 @@
validateAuth: function(authToken, rememberLogin) {
authActions.validate(authToken, {
useStreaming: preferenceStore.useStreaming(),
useStreaming: preferenceStore.useStreaming,
rememberLogin: rememberLogin,
});
},
};
// UI specific util methods
window.hass.uiUtil = {}
})();
</script>

View File

@ -111,7 +111,7 @@ def setup(hass, config=None):
if config is None or DOMAIN not in config:
config = {DOMAIN: {}}
api_password = str(config[DOMAIN].get(CONF_API_PASSWORD))
api_password = util.convert(config[DOMAIN].get(CONF_API_PASSWORD), str)
no_password_set = api_password is None

View File

@ -0,0 +1,94 @@
"""
Support for Vera lights.
Configuration:
This component is useful if you wish for switches connected to your Vera
controller to appear as lights in homeassistant. All switches will be added
as a light unless you exclude them in the config.
To use the Vera lights you will need to add something like the following to
your config/configuration.yaml
light:
platform: vera
vera_controller_url: http://YOUR_VERA_IP:3480/
device_data:
12:
name: My awesome switch
exclude: true
13:
name: Another switch
VARIABLES:
vera_controller_url
*Required
This is the base URL of your vera controller including the port number if not
running on 80
Example: http://192.168.1.21:3480/
device_data
*Optional
This contains an array additional device info for your Vera devices. It is not
required and if not specified all lights configured in your Vera controller
will be added with default values. You should use the id of your vera device
as the key for the device within device_data
These are the variables for the device_data array:
name
*Optional
This parameter allows you to override the name of your Vera device in the HA
interface, if not specified the value configured for the device in your Vera
will be used
exclude
*Optional
This parameter allows you to exclude the specified device from homeassistant,
it should be set to "true" if you want this device excluded
"""
import logging
from requests.exceptions import RequestException
from homeassistant.components.switch.vera import VeraSwitch
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.vera.vera as veraApi
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Vera lights. """
base_url = config.get('vera_controller_url')
if not base_url:
_LOGGER.error(
"The required parameter 'vera_controller_url'"
" was not found in config"
)
return False
device_data = config.get('device_data', {})
controller = veraApi.VeraController(base_url)
devices = []
try:
devices = controller.get_devices('Switch')
except RequestException:
# There was a network related error connecting to the vera controller
_LOGGER.exception("Error communicating with Vera API")
return False
lights = []
for device in devices:
extra_data = device_data.get(device.deviceId, {})
exclude = extra_data.get('exclude', False)
if exclude is not True:
lights.append(VeraSwitch(device, extra_data))
add_devices_callback(lights)

View File

@ -7,7 +7,7 @@ on the host machine.
Author: Markus Stenberg <fingon@iki.fi>
"""
import logging
import os
from homeassistant.const import STATE_ON, STATE_OFF
@ -27,6 +27,11 @@ def setup(hass, config):
in process list.
"""
# Deprecated as of 3/7/2015
logging.getLogger(__name__).warning(
"This component has been deprecated and will be removed in the future."
" Please use sensor.systemmonitor with the process type")
entities = {ENTITY_ID_FORMAT.format(util.slugify(pname)): pstring
for pname, pstring in config[DOMAIN].items()}

View File

@ -0,0 +1,162 @@
"""
Support for Vera sensors.
Configuration:
To use the Vera sensors you will need to add something like the following to
your config/configuration.yaml
sensor:
platform: vera
vera_controller_url: http://YOUR_VERA_IP:3480/
device_data:
12:
name: My awesome sensor
exclude: true
13:
name: Another sensor
VARIABLES:
vera_controller_url
*Required
This is the base URL of your vera controller including the port number if not
running on 80
Example: http://192.168.1.21:3480/
device_data
*Optional
This contains an array additional device info for your Vera devices. It is not
required and if not specified all sensors configured in your Vera controller
will be added with default values. You should use the id of your vera device
as the key for the device within device_data
These are the variables for the device_data array:
name
*Optional
This parameter allows you to override the name of your Vera device in the HA
interface, if not specified the value configured for the device in your Vera
will be used
exclude
*Optional
This parameter allows you to exclude the specified device from homeassistant,
it should be set to "true" if you want this device excluded
"""
import logging
import time
from requests.exceptions import RequestException
from homeassistant.helpers import Device
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME)
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.vera.vera as veraApi
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_devices(hass, config):
""" Find and return Vera Sensors. """
base_url = config.get('vera_controller_url')
if not base_url:
_LOGGER.error(
"The required parameter 'vera_controller_url'"
" was not found in config"
)
return False
device_data = config.get('device_data', {})
vera_controller = veraApi.VeraController(base_url)
categories = ['Temperature Sensor', 'Light Sensor', 'Sensor']
devices = []
try:
devices = vera_controller.get_devices(categories)
except RequestException:
# There was a network related error connecting to the vera controller
_LOGGER.exception("Error communicating with Vera API")
return False
vera_sensors = []
for device in devices:
extra_data = device_data.get(device.deviceId, {})
exclude = extra_data.get('exclude', False)
if exclude is not True:
vera_sensors.append(VeraSensor(device, extra_data))
return vera_sensors
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Performs setup for Vera controller devices """
add_devices(get_devices(hass, config))
class VeraSensor(Device):
""" Represents a Vera Sensor """
def __init__(self, vera_device, extra_data=None):
self.vera_device = vera_device
self.extra_data = extra_data
if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name')
else:
self._name = self.vera_device.name
self.current_value = ''
def __str__(self):
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state)
@property
def state(self):
return self.current_value
@property
def name(self):
""" Get the mame of the sensor. """
return self._name
@property
def state_attributes(self):
attr = super().state_attributes
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.refresh_value('Armed')
attr[ATTR_ARMED] = 'True' if armed == '1' else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.refresh_value('LastTrip')
trip_time_str = time.strftime(
"%Y-%m-%d %H:%M",
time.localtime(int(last_tripped))
)
attr[ATTR_LAST_TRIP_TIME] = trip_time_str
tripped = self.vera_device.refresh_value('Tripped')
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
def update(self):
if self.vera_device.category == "Temperature Sensor":
self.vera_device.refresh_value('CurrentTemperature')
current_temp = self.vera_device.get_value('CurrentTemperature')
vera_temp_units = self.vera_device.veraController.temperature_units
self.current_value = current_temp + '°' + vera_temp_units
elif self.vera_device.category == "Light Sensor":
self.vera_device.refresh_value('CurrentLevel')
self.current_value = self.vera_device.get_value('CurrentLevel')
elif self.vera_device.category == "Sensor":
tripped = self.vera_device.refresh_value('Tripped')
self.current_value = 'Tripped' if tripped == '1' else 'Not Tripped'
else:
self.current_value = 'Unknown'

View File

@ -0,0 +1,166 @@
"""
Support for Vera switches.
Configuration:
To use the Vera lights you will need to add something like the following to
your config/configuration.yaml
switch:
platform: vera
vera_controller_url: http://YOUR_VERA_IP:3480/
device_data:
12:
name: My awesome switch
exclude: true
13:
name: Another Switch
VARIABLES:
vera_controller_url
*Required
This is the base URL of your vera controller including the port number if not
running on 80
Example: http://192.168.1.21:3480/
device_data
*Optional
This contains an array additional device info for your Vera devices. It is not
required and if not specified all lights configured in your Vera controller
will be added with default values. You should use the id of your vera device
as the key for the device within device_data
These are the variables for the device_data array:
name
*Optional
This parameter allows you to override the name of your Vera device in the HA
interface, if not specified the value configured for the device in your Vera
will be used
exclude
*Optional
This parameter allows you to exclude the specified device from homeassistant,
it should be set to "true" if you want this device excluded
"""
import logging
import time
from requests.exceptions import RequestException
from homeassistant.helpers import ToggleDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME)
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.vera.vera as veraApi
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_devices(hass, config):
""" Find and return Vera switches. """
base_url = config.get('vera_controller_url')
if not base_url:
_LOGGER.error(
"The required parameter 'vera_controller_url'"
" was not found in config"
)
return False
device_data = config.get('device_data', {})
vera_controller = veraApi.VeraController(base_url)
devices = []
try:
devices = vera_controller.get_devices(['Switch', 'Armable Sensor'])
except RequestException:
# There was a network related error connecting to the vera controller
_LOGGER.exception("Error communicating with Vera API")
return False
vera_switches = []
for device in devices:
extra_data = device_data.get(device.deviceId, {})
exclude = extra_data.get('exclude', False)
if exclude is not True:
vera_switches.append(VeraSwitch(device, extra_data))
return vera_switches
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Find and return Vera lights. """
add_devices(get_devices(hass, config))
class VeraSwitch(ToggleDevice):
""" Represents a Vera Switch """
def __init__(self, vera_device, extra_data=None):
self.vera_device = vera_device
self.extra_data = extra_data
if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name')
else:
self._name = self.vera_device.name
self.is_on_status = False
# for debouncing status check after command is sent
self.last_command_send = 0
@property
def name(self):
""" Get the mame of the switch. """
return self._name
@property
def state_attributes(self):
attr = super().state_attributes
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.refresh_value('Armed')
attr[ATTR_ARMED] = 'True' if armed == '1' else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.refresh_value('LastTrip')
trip_time_str = time.strftime(
"%Y-%m-%d %H:%M",
time.localtime(int(last_tripped))
)
attr[ATTR_LAST_TRIP_TIME] = trip_time_str
tripped = self.vera_device.refresh_value('Tripped')
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
def turn_on(self, **kwargs):
self.last_command_send = time.time()
self.vera_device.switch_on()
self.is_on_status = True
def turn_off(self, **kwargs):
self.last_command_send = time.time()
self.vera_device.switch_off()
self.is_on_status = False
@property
def is_on(self):
""" True if device is on. """
return self.is_on_status
def update(self):
# We need to debounce the status call after turning switch on or off
# because the vera has some lag in updating the device status
if (self.last_command_send + 5) < time.time():
self.is_on_status = self.vera_device.is_switched_on()

View File

@ -73,6 +73,16 @@ ATTR_LOCATION = "location"
ATTR_BATTERY_LEVEL = "battery_level"
# For devices which support an armed state
ATTR_ARMED = "device_armed"
# For sensors that support 'tripping', eg. motion and door sensors
ATTR_TRIPPED = "device_tripped"
# For sensors that support 'tripping' this holds the most recent
# time the device was tripped
ATTR_LAST_TRIP_TIME = "last_tripped_time"
# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"

1
homeassistant/external/vera vendored Submodule

@ -0,0 +1 @@
Subproject commit fedbb5c3af1e5f36b7008d894e9fc1ecf3cc2ea8

View File

@ -98,14 +98,17 @@ def config_per_platform(config, domain, logger):
while config_key in config:
platform_config = config[config_key]
if not isinstance(platform_config, list):
platform_config = [platform_config]
platform_type = platform_config.get(CONF_PLATFORM)
for item in platform_config:
platform_type = item.get(CONF_PLATFORM)
if platform_type is None:
logger.warning('No platform specified for %s', config_key)
break
if platform_type is None:
logger.warning('No platform specified for %s', config_key)
continue
yield platform_type, platform_config
yield platform_type, item
found += 1
config_key = "{} {}".format(domain, found)