Merge pull request #100 from balloob/dev

Update master with latest changes
pull/110/merge
Paulus Schoutsen 2015-04-24 21:02:14 -07:00
commit 31a22d4c6a
35 changed files with 1605 additions and 336 deletions

View File

@ -28,6 +28,12 @@ omit =
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/sensor/transmission.py
homeassistant/components/isy994.py
homeassistant/components/light/isy994.py
homeassistant/components/switch/isy994.py
homeassistant/components/sensor/isy994.py
[report]

View File

@ -27,9 +27,23 @@ A state can have several attributes that will help the frontend in displaying yo
- `friendly_name`: this name will be used as the name of the device
- `entity_picture`: this picture will be shown instead of the domain icon
- `unit_of_measurement`: this will be appended to the state in the interface
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
## Proper Visibility Handling ##
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py) Class. If this is done, visibility will be handled for you.
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
```python
self.hidden = True
```
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
## Working on the frontend
The frontend is composed of Polymer web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the http-component in your config.

View File

@ -907,7 +907,8 @@ class Config(object):
_LOGGER.info('Auto detecting location and temperature unit')
try:
info = requests.get('https://freegeoip.net/json/').json()
info = requests.get(
'https://freegeoip.net/json/', timeout=5).json()
except requests.RequestException:
return

View File

@ -85,6 +85,7 @@ def ensure_config_path(config_dir):
conf.write("frontend:\n\n")
conf.write("discovery:\n\n")
conf.write("history:\n\n")
conf.write("logbook:\n\n")
except IOError:
print(('Fatal Error: No configuration file found and unable '
'to write a default one to {}').format(config_path))

View File

@ -20,10 +20,11 @@ import homeassistant
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_VISIBILITY,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
@ -207,6 +208,9 @@ def process_ha_core_config(hass, config):
if key in config:
setattr(hass.config, attr, config[key])
for entity_id, hidden in config.get(CONF_VISIBILITY, {}).items():
Entity.overwrite_hidden(entity_id, hidden == 'hide')
if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT]

View File

@ -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='180x180'
href='/apple-icon-180x180.png'>
<link rel='apple-touch-icon' sizes='192x192'
href='/static/favicon-192x192.png'>
<meta name='theme-color' content='#03a9f4'>
</head>
<body fullbleed>

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "1e004712440afc642a44ad927559587e"
VERSION = "93774c7a1643c7e3f9cbbb1554b36683"

File diff suppressed because one or more lines are too long

View File

@ -2,26 +2,54 @@
<link rel="import" href="../bower_components/google-apis/google-jsapi.html">
<polymer-element name="state-timeline" attributes="stateHistory">
<polymer-element name="state-timeline" attributes="stateHistory isLoadingData">
<template>
<style>
:host {
display: block;
}
#loadingbox {
text-align: center;
}
.loadingmessage {
margin-top: 10px;
}
.singlelinechart {
min-height:140px;
}
</style>
<div style='width: 100%; height: auto;' hidden?="{{!isLoading}}" >
<div layout horizontal center id="splash">
<div layout vertical center flex>
<div id="loadingbox">
<paper-spinner active="true"></paper-spinner><br />
<div class="loadingmessage">{{spinnerMessage}}</div>
</div>
</div>
</div>
</div>
<google-jsapi on-api-load="{{googleApiLoaded}}"></google-jsapi>
<div id="timeline" style='width: 100%; height: auto;'></div>
<div id="timeline" style='width: 100%; height: auto;' class="{{ {singlelinechart: isSingleDevice && hasLineChart } | tokenList}}" hidden?="{{isLoadingData}}"></div>
<div id="line_graphs" style='width: 100%; height: auto;' hidden?="{{isLoadingData}}"></div>
</template>
<script>
Polymer({
apiLoaded: false,
stateHistory: null,
isLoading: true,
isLoadingData: false,
spinnerMessage: "Loading history data...",
isSingleDevice: false,
hasLineChart: false,
googleApiLoaded: function() {
google.load("visualization", "1", {
packages: ["timeline"],
packages: ["timeline", "corechart"],
callback: function() {
this.apiLoaded = true;
this.drawChart();
@ -33,10 +61,17 @@
this.drawChart();
},
isLoadingDataChanged: function() {
if(this.isLoadingData) {
isLoading = true;
}
},
drawChart: function() {
if (!this.apiLoaded || !this.stateHistory) {
return;
}
this.isLoading = true;
var container = this.$.timeline;
var chart = new google.visualization.Timeline(container);
@ -48,6 +83,7 @@
dataTable.addColumn({ type: 'date', id: 'End' });
var addRow = function(entityDisplay, stateStr, start, end) {
stateStr = stateStr.replace(/_/g, ' ');
dataTable.addRow([entityDisplay, stateStr, start, end]);
};
@ -55,21 +91,39 @@
return;
}
// people can pass in history of 1 entityId or a collection.
this.hasLineChart = false;
this.isSingleDevice = false;
// people can pass in history of 1 entityId or a collection.
var stateHistory;
if (_.isArray(this.stateHistory[0])) {
stateHistory = this.stateHistory;
} else {
stateHistory = [this.stateHistory];
this.isSingleDevice = true;
}
var lineChartDevices = {};
var numTimelines = 0;
// stateHistory is a list of lists of sorted state objects
stateHistory.forEach(function(stateInfo) {
if(stateInfo.length === 0) return;
var entityDisplay = stateInfo[0].entityDisplay;
var newLastChanged, prevState = null, prevLastChanged = null;
//get the latest update to get the graph type from the component attributes
var attributes = stateInfo[stateInfo.length - 1].attributes;
//if the device has a unit of meaurment it will be added as a line graph further down
if(attributes.unit_of_measurement) {
if(!lineChartDevices[attributes.unit_of_measurement]){
lineChartDevices[attributes.unit_of_measurement] = [];
}
lineChartDevices[attributes.unit_of_measurement].push(stateInfo);
this.hasLineChart = true;
return;
}
stateInfo.forEach(function(state) {
if (prevState !== null && state.state !== prevState) {
@ -86,10 +140,11 @@
});
addRow(entityDisplay, prevState, prevLastChanged, new Date());
numTimelines++;
}.bind(this));
chart.draw(dataTable, {
height: 55 + stateHistory.length * 42,
height: 55 + numTimelines * 42,
// interactive properties require CSS, the JS api puts it on the document
// instead of inside our Shadow DOM.
@ -103,6 +158,162 @@
format: 'H:mm'
},
});
/**************************************************
The following code gererates line line graphs for devices with continuous
values(which are devices that have a unit_of_measurment values defined).
On each graph the devices are grouped by their unit of measurement, eg. all
sensors measuring MB will be a separate line on single graph. The google
chart API takes data as a 2 dimensional array in the format:
DateTime, device1, device2, device3
2015-04-01, 1, 2, 0
2015-04-01, 0, 1, 0
2015-04-01, 2, 1, 1
NOTE: the first column is a javascript date objects.
The first thing we do is build up the data with rows for each time of a state
change and initialise the values to 0. THen we loop through each device and
fill in its data.
**************************************************/
while (this.$.line_graphs.firstChild) {
this.$.line_graphs.removeChild(this.$.line_graphs.firstChild);
}
for (var key in lineChartDevices) {
var deviceStates = lineChartDevices[key];
if(this.isSingleDevice) {
container = this.$.timeline;
}
else {
container = document.createElement("DIV");
this.$.line_graphs.appendChild(container);
}
var chart = new google.visualization.LineChart(container);
var dataTable = new google.visualization.DataTable();
dataTable.addColumn({ type: 'datetime', id: 'Time' });
var options = {
legend: { position: 'top' },
titlePosition: 'none',
vAxes: {
// Adds units to the left hand side of the graph
0: {title: key}
},
hAxis: {
format: 'H:mm'
},
lineWidth: 1,
chartArea:{left:'60',width:"95%"},
explorer: {
actions: ['dragToZoom', 'rightClickToReset', 'dragToPan'],
keepInBounds: true,
axis: 'horizontal',
maxZoomIn: 0.1
}
};
if(this.isSingleDevice) {
options.legend.position = 'none';
options.vAxes[0].title = null;
options.chartArea.left = 40;
options.chartArea.height = '80%';
options.chartArea.top = 5;
options.enableInteractivity = false;
}
// Get a unique list of times of state changes for all the device
// for a particular unit of measureent.
var times = _.pluck(_.flatten(deviceStates), "lastChangedAsDate");
times = _.uniq(times, function(e) {
return e.getTime();
});
times = _.sortBy(times, function(o) { return o; });
var data = [];
var empty = new Array(deviceStates.length);
for(var i = 0; i < empty.length; i++) {
empty[i] = 0;
}
var timeIndex = 1;
var endDate = new Date();
var prevDate = times[0];
for(var i = 0; i < times.length; i++) {
var currentDate = new Date(prevDate);
// because we only have state changes we add an extra point at the same time
// that holds the previous state which makes the line display correctly
var beforePoint = new Date(times[i]);
data.push([beforePoint].concat(empty));
data.push([times[i]].concat(empty));
prevDate = times[i];
timeIndex++;
}
data.push([endDate].concat(empty));
var deviceCount = 0;
deviceStates.forEach(function(device) {
var attributes = device[device.length - 1].attributes;
dataTable.addColumn('number', attributes.friendly_name);
var currentState = 0;
var previousState = 0;
var lastIndex = 0;
var count = 0;
var prevTime = data[0][0];
device.forEach(function(state) {
currentState = state.state;
var start = state.lastChangedAsDate;
if(state.state == 'None') {
currentState = previousState;
}
for(var i = lastIndex; i < data.length; i++) {
data[i][1 + deviceCount] = parseFloat(previousState);
// this is where data gets filled in for each time for the particular device
// because for each time two entires were create we fill the first one with the
// previous value and the second one with the new value
if(prevTime.getTime() == data[i][0].getTime() && data[i][0].getTime() == start.getTime()) {
data[i][1 + deviceCount] = parseFloat(currentState);
lastIndex = i;
prevTime = data[i][0];
break;
}
prevTime = data[i][0];
}
previousState = currentState;
count++;
}.bind(this));
//fill in the rest of the Array
for(var i = lastIndex; i < data.length; i++) {
data[i][1 + deviceCount] = parseFloat(previousState);
}
deviceCount++;
}.bind(this));
dataTable.addRows(data);
chart.draw(dataTable, options);
}
this.isLoading = (!this.isLoadingData) ? false : true;
},
});

View File

@ -11,7 +11,7 @@
<div>
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
</state-card-content>
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingHistoryData}}"></state-timeline>
<more-info-content
stateObj="{{stateObj}}"
dialogOpen="{{dialogOpen}}"></more-info-content>
@ -30,6 +30,7 @@ Polymer(Polymer.mixin({
stateHistory: null,
hasHistoryComponent: false,
dialogOpen: false,
isLoadingHistoryData: false,
observe: {
'stateObj.attributes': 'reposition'
@ -67,7 +68,7 @@ Polymer(Polymer.mixin({
} else {
newHistory = null;
}
this.isLoadingHistoryData = false;
if (newHistory !== this.stateHistory) {
this.stateHistory = newHistory;
}
@ -87,6 +88,7 @@ Polymer(Polymer.mixin({
this.stateHistoryStoreChanged();
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
this.isLoadingHistoryData = true;
stateHistoryActions.fetch(entityId);
}
},

@ -1 +1 @@
Subproject commit 282004e3e27134a3de1b9c0e6c264ce811f3e510
Subproject commit 56f896efa573aaa9554812a3c41b78278bce2064

View File

@ -26,7 +26,7 @@
</span>
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingData}}"></state-timeline>
</div>
</partial-base>
</template>
@ -36,6 +36,7 @@
Polymer(Polymer.mixin({
stateHistory: null,
isLoadingData: false,
attached: function() {
this.listenToStores(true);
@ -47,13 +48,18 @@
stateHistoryStoreChanged: function(stateHistoryStore) {
if (stateHistoryStore.isStale()) {
this.isLoadingData = true;
stateHistoryActions.fetchAll();
}
else {
this.isLoadingData = false;
}
this.stateHistory = stateHistoryStore.all;
},
handleRefreshClick: function() {
this.isLoadingData = true;
stateHistoryActions.fetchAll();
},
}, storeListenerMixIn));

View File

@ -143,8 +143,9 @@
return !(state.domain in uiConstants.STATE_FILTERS);
});
}
this.states = states.toArray();
this.states = states.toArray().filter(
function (el) {return !el.attributes.hidden});
},
handleRefreshClick: function() {

View File

@ -7,9 +7,10 @@ Provides functionality to group devices that can be turned on or off.
import homeassistant as ha
from homeassistant.helpers import generate_entity_id
from homeassistant.helpers.entity import Entity
import homeassistant.util as util
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
DOMAIN = "group"
@ -110,35 +111,43 @@ def setup(hass, config):
return True
class Group(object):
class Group(Entity):
""" Tracks a group of entity ids. """
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, name, entity_ids=None, user_defined=True):
self.hass = hass
self.name = name
self._name = name
self._state = STATE_UNKNOWN
self.user_defined = user_defined
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
self.tracking = []
self.group_on, self.group_off = None, None
self.group_on = None
self.group_off = None
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
else:
self.force_update()
self.update_ha_state(True)
@property
def should_poll(self):
return False
@property
def name(self):
return self._name
@property
def state(self):
""" Return the current state from the group. """
return self.hass.states.get(self.entity_id)
return self._state
@property
def state_attr(self):
""" State attributes of this group. """
def state_attributes(self):
return {
ATTR_ENTITY_ID: self.tracking,
ATTR_AUTO: not self.user_defined,
ATTR_FRIENDLY_NAME: self.name
}
def update_tracked_entity_ids(self, entity_ids):
@ -147,71 +156,69 @@ class Group(object):
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
self.force_update()
self.update_ha_state(True)
self.start()
def force_update(self):
""" Query all the tracked states and update group state. """
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._update_group_state(state.entity_id, None, state)
# If parsing the entitys did not result in a state, set UNKNOWN
if self.state is None:
self.hass.states.set(
self.entity_id, STATE_UNKNOWN, self.state_attr)
def start(self):
""" Starts the tracking. """
self.hass.states.track_change(self.tracking, self._update_group_state)
self.hass.states.track_change(
self.tracking, self._state_changed_listener)
def stop(self):
""" Unregisters the group from Home Assistant. """
self.hass.states.remove(self.entity_id)
self.hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, self._update_group_state)
ha.EVENT_STATE_CHANGED, self._state_changed_listener)
def _update_group_state(self, entity_id, old_state, new_state):
""" Updates the group state based on a state change by
a tracked entity. """
def update(self):
""" Query all the tracked states and determine current group state. """
self._state = STATE_UNKNOWN
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._process_tracked_state(state)
def _state_changed_listener(self, entity_id, old_state, new_state):
""" Listener to receive state changes of tracked entities. """
self._process_tracked_state(new_state)
self.update_ha_state()
def _process_tracked_state(self, tr_state):
""" Updates group state based on a new state of a tracked entity. """
# We have not determined type of group yet
if self.group_on is None:
self.group_on, self.group_off = _get_group_on_off(new_state.state)
self.group_on, self.group_off = _get_group_on_off(tr_state.state)
if self.group_on is not None:
# New state of the group is going to be based on the first
# state that we can recognize
self.hass.states.set(
self.entity_id, new_state.state, self.state_attr)
self._state = tr_state.state
return
# There is already a group state
cur_gr_state = self.hass.states.get(self.entity_id).state
cur_gr_state = self._state
group_on, group_off = self.group_on, self.group_off
# if cur_gr_state = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research
# if cur_gr_state = OFF and tr_state = ON: set ON
# if cur_gr_state = ON and tr_state = OFF: research
# else: ignore
if cur_gr_state == group_off and new_state.state == group_on:
if cur_gr_state == group_off and tr_state.state == group_on:
self._state = group_on
self.hass.states.set(
self.entity_id, group_on, self.state_attr)
elif cur_gr_state == group_on and tr_state.state == group_off:
elif (cur_gr_state == group_on and
new_state.state == group_off):
# Check if any of the other states is still on
# Set to off if no other states are on
if not any(self.hass.states.is_state(ent_id, group_on)
for ent_id in self.tracking if entity_id != ent_id):
self.hass.states.set(
self.entity_id, group_off, self.state_attr)
for ent_id in self.tracking
if tr_state.entity_id != ent_id):
self._state = group_off
def setup_group(hass, name, entity_ids, user_defined=True):

View File

@ -50,8 +50,10 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
result = defaultdict(list)
entity_ids = [entity_id] if entity_id is not None else None
# Get the states at the start time
for state in get_states(start_time):
for state in get_states(start_time, entity_ids):
state.last_changed = start_time
result[state.entity_id].append(state)
@ -98,6 +100,7 @@ def get_state(point_in_time, entity_id, run=None):
return states[0] if states else None
# pylint: disable=unused-argument
def setup(hass, config):
""" Setup history hooks. """
hass.http.register_path(
@ -113,6 +116,7 @@ def setup(hass, config):
return True
# pylint: disable=unused-argument
# pylint: disable=invalid-name
def _api_last_5_states(handler, path_match, data):
""" Return the last 5 states for an entity id as JSON. """

View File

@ -0,0 +1,209 @@
"""
Connects to an ISY-994 controller and loads relevant components to control its
devices. Also contains the base classes for ISY Sensors, Lights, and Switches.
"""
# system imports
import logging
from urllib.parse import urlparse
# addon library imports
import PyISY
# homeassistant imports
from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, EVENT_PLATFORM_DISCOVERED,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
# homeassistant constants
DOMAIN = "isy994"
DEPENDENCIES = []
DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches"
DISCOVER_SENSORS = "isy994.sensors"
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
# setup logger
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""
Setup isy994 component.
This will automatically import associated lights, switches, and sensors.
"""
# pylint: disable=global-statement
# check for required values in configuration file
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return False
# pull and parse standard configuration
user = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
host = urlparse(config[DOMAIN][CONF_HOST])
addr = host.geturl()
if host.scheme == 'http':
addr = addr.replace('http://', '')
https = False
elif host.scheme == 'https':
addr = addr.replace('https://', '')
https = True
else:
_LOGGER.error('isy994 host value in configuration file is invalid.')
return False
port = host.port
addr = addr.replace(':{}'.format(port), '')
# pull and parse optional configuration
global SENSOR_STRING
global HIDDEN_STRING
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING))
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
# connect to ISY controller
global ISY
ISY = PyISY.ISY(addr, port, user, password, use_https=https, log=_LOGGER)
if not ISY.connected:
return False
# Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('light', DISCOVER_LIGHTS),
('switch', DISCOVER_SWITCHES))):
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery,
ATTR_DISCOVERED: {}})
ISY.auto_update = True
return True
class ISYDeviceABC(ToggleEntity):
""" Abstract Class for an ISY device within home assistant. """
_attrs = {}
_onattrs = []
_states = []
_dtype = None
_domain = None
_name = None
def __init__(self, node):
# setup properties
self.node = node
self.hidden = HIDDEN_STRING in self.raw_name
# track changes
self._change_handler = self.node.status. \
subscribe('changed', self.on_update)
def __del__(self):
""" cleanup subscriptions because it is the right thing to do. """
self._change_handler.unsubscribe()
@property
def domain(self):
""" Returns the domain of the entity. """
return self._domain
@property
def dtype(self):
""" Returns the data type of the entity (binary or analog). """
if self._dtype in ['analog', 'binary']:
return self._dtype
return 'binary' if self.unit_of_measurement is None else 'analog'
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property
def value(self):
""" returns the unclean value from the controller """
# pylint: disable=protected-access
return self.node.status._val
@property
def state_attributes(self):
""" Returns the state attributes for the node. """
attr = {ATTR_FRIENDLY_NAME: self.name}
for name, prop in self._attrs.items():
attr[name] = getattr(self, prop)
return attr
@property
def unique_id(self):
""" Returns the id of this isy sensor """
# pylint: disable=protected-access
return self.node._id
@property
def raw_name(self):
""" Returns the unclean node name. """
return str(self._name) \
if self._name is not None else str(self.node.name)
@property
def name(self):
""" Returns the cleaned name of the node. """
return self.raw_name.replace(HIDDEN_STRING, '').strip() \
.replace('_', ' ')
def update(self):
""" Update state of the sensor. """
# ISY objects are automatically updated by the ISY's event stream
pass
def on_update(self, event):
""" Handles the update received event. """
self.update_ha_state()
@property
def is_on(self):
""" Returns boolean response if the node is on. """
return bool(self.value)
@property
def is_open(self):
""" Returns boolean respons if the node is open. On = Open. """
return self.is_on
@property
def state(self):
""" Returns the state of the node. """
if len(self._states) > 0:
return self._states[0] if self.is_on else self._states[1]
return self.value
def turn_on(self, **kwargs):
""" turns the device on """
if self.domain is not 'sensor':
attrs = [kwargs.get(name) for name in self._onattrs]
self.node.on(*attrs)
else:
_LOGGER.error('ISY cannot turn on sensors.')
def turn_off(self, **kwargs):
""" turns the device off """
if self.domain is not 'sensor':
self.node.off()
else:
_LOGGER.error('ISY cannot turn off sensors.')
@property
def unit_of_measurement(self):
""" Returns the defined units of measurement or None. """
try:
return self.node.units
except AttributeError:
return None

View File

@ -57,7 +57,7 @@ from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.util as util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import group, discovery, wink
from homeassistant.components import group, discovery, wink, isy994
DOMAIN = "light"
@ -92,6 +92,7 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LIGHTS: 'wink',
isy994.DISCOVER_LIGHTS: 'isy994',
discovery.services.PHILIPS_HUE: 'hue',
}

View File

@ -0,0 +1,38 @@
""" Support for ISY994 lights. """
# system imports
import logging
# homeassistant imports
from homeassistant.components.isy994 import (ISYDeviceABC, ISY, SENSOR_STRING,
HIDDEN_STRING)
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import STATE_ON, STATE_OFF
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the isy994 platform. """
logger = logging.getLogger(__name__)
devs = []
# verify connection
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
return False
# import dimmable nodes
for (path, node) in ISY.nodes:
if node.dimmable and SENSOR_STRING not in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYLightDevice(node))
add_devices(devs)
class ISYLightDevice(ISYDeviceABC):
""" represents as isy light within home assistant. """
_domain = 'light'
_dtype = 'analog'
_attrs = {ATTR_BRIGHTNESS: 'value'}
_onattrs = [ATTR_BRIGHTNESS]
_states = [STATE_ON, STATE_OFF]

View File

@ -0,0 +1,100 @@
"""
components.modbus
~~~~~~~~~~~~~~~~~~~~~~~~~
Modbus component, using pymodbus (python3 branch)
typical declaration in configuration.yaml
#Modbus TCP
modbus:
type: tcp
host: 127.0.0.1
port: 2020
#Modbus RTU
modbus:
type: serial
method: rtu
port: /dev/ttyUSB0
baudrate: 9600
stopbits: 1
bytesize: 8
parity: N
"""
import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
# The domain of your component. Should be equal to the name of your component
DOMAIN = "modbus"
# List of component names (string) your component depends upon
DEPENDENCIES = []
# Type of network
MEDIUM = "type"
# if MEDIUM == "serial"
METHOD = "method"
SERIAL_PORT = "port"
BAUDRATE = "baudrate"
STOPBITS = "stopbits"
BYTESIZE = "bytesize"
PARITY = "parity"
# if MEDIUM == "tcp" or "udp"
HOST = "host"
IP_PORT = "port"
_LOGGER = logging.getLogger(__name__)
NETWORK = None
TYPE = None
def setup(hass, config):
""" Setup Modbus component. """
# Modbus connection type
# pylint: disable=global-statement, import-error
global TYPE
TYPE = config[DOMAIN][MEDIUM]
# Connect to Modbus network
# pylint: disable=global-statement, import-error
global NETWORK
if TYPE == "serial":
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
NETWORK = ModbusClient(method=config[DOMAIN][METHOD],
port=config[DOMAIN][SERIAL_PORT],
baudrate=config[DOMAIN][BAUDRATE],
stopbits=config[DOMAIN][STOPBITS],
bytesize=config[DOMAIN][BYTESIZE],
parity=config[DOMAIN][PARITY])
elif TYPE == "tcp":
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
NETWORK = ModbusClient(host=config[DOMAIN][HOST],
port=config[DOMAIN][IP_PORT])
elif TYPE == "udp":
from pymodbus.client.sync import ModbusUdpClient as ModbusClient
NETWORK = ModbusClient(host=config[DOMAIN][HOST],
port=config[DOMAIN][IP_PORT])
else:
return False
def stop_modbus(event):
""" Stop Modbus service"""
NETWORK.close()
def start_modbus(event):
""" Start Modbus service"""
NETWORK.connect()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
# Tells the bootstrapper that the component was succesfully initialized
return True

View File

@ -6,7 +6,7 @@ Component to interface with various sensors that can be monitored.
import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import wink, zwave
from homeassistant.components import wink, zwave, isy994
DOMAIN = 'sensor'
DEPENDENCIES = []
@ -18,6 +18,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
DISCOVERY_PLATFORMS = {
wink.DISCOVER_SENSORS: 'wink',
zwave.DISCOVER_SENSORS: 'zwave',
isy994.DISCOVER_SENSORS: 'isy994'
}

View File

@ -0,0 +1,90 @@
""" Support for ISY994 sensors. """
# system imports
import logging
# homeassistant imports
from homeassistant.components.isy994 import (ISY, ISYDeviceABC, SENSOR_STRING,
HIDDEN_STRING)
from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_HOME,
STATE_NOT_HOME, STATE_ON, STATE_OFF)
DEFAULT_HIDDEN_WEATHER = ['Temperature_High', 'Temperature_Low', 'Feels_Like',
'Temperature_Average', 'Pressure', 'Dew_Point',
'Gust_Speed', 'Evapotranspiration',
'Irrigation_Requirement', 'Water_Deficit_Yesterday',
'Elevation', 'Average_Temperature_Tomorrow',
'High_Temperature_Tomorrow',
'Low_Temperature_Tomorrow', 'Humidity_Tomorrow',
'Wind_Speed_Tomorrow', 'Gust_Speed_Tomorrow',
'Rain_Tomorrow', 'Snow_Tomorrow',
'Forecast_Average_Temperature',
'Forecast_High_Temperature',
'Forecast_Low_Temperature', 'Forecast_Humidity',
'Forecast_Rain', 'Forecast_Snow']
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the isy994 platform. """
# pylint: disable=protected-access
logger = logging.getLogger(__name__)
devs = []
# verify connection
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
return False
# import weather
if ISY.climate is not None:
for prop in ISY.climate._id2name:
if prop is not None:
prefix = HIDDEN_STRING \
if prop in DEFAULT_HIDDEN_WEATHER else ''
node = WeatherPseudoNode('ISY.weather.' + prop, prefix + prop,
getattr(ISY.climate, prop),
getattr(ISY.climate, prop + '_units'))
devs.append(ISYSensorDevice(node))
# import sensor nodes
for (path, node) in ISY.nodes:
if SENSOR_STRING in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYSensorDevice(node, [STATE_ON, STATE_OFF]))
# import sensor programs
for (folder_name, states) in (
('HA.locations', [STATE_HOME, STATE_NOT_HOME]),
('HA.sensors', [STATE_OPEN, STATE_CLOSED]),
('HA.states', [STATE_ON, STATE_OFF])):
try:
folder = ISY.programs['My Programs'][folder_name]
except KeyError:
# folder does not exist
pass
else:
for _, _, node_id in folder.children:
node = folder[node_id].leaf
devs.append(ISYSensorDevice(node, states))
add_devices(devs)
class WeatherPseudoNode(object):
""" This class allows weather variable to act as regular nodes. """
# pylint: disable=too-few-public-methods
def __init__(self, device_id, name, status, units=None):
self._id = device_id
self.name = name
self.status = status
self.units = units
class ISYSensorDevice(ISYDeviceABC):
""" represents a isy sensor within home assistant. """
_domain = 'sensor'
def __init__(self, node, states=None):
super().__init__(node)
self._states = states or []

View File

@ -0,0 +1,136 @@
"""
Support for Modbus sensors.
Configuration:
To use the Modbus sensors you will need to add something like the following to
your config/configuration.yaml
sensor:
platform: modbus
slave: 1
registers:
16:
name: My integer sensor
unit: C
24:
bits:
0:
name: My boolean sensor
2:
name: My other boolean sensor
VARIABLES:
- "slave" = slave number (ignored and can be omitted if not serial Modbus)
- "unit" = unit to attach to value (optional, ignored for boolean sensors)
- "registers" contains a list of relevant registers to read from
it can contain a "bits" section, listing relevant bits
- each named register will create an integer sensor
- each named bit will create a boolean sensor
"""
import logging
import homeassistant.components.modbus as modbus
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
TEMP_CELCIUS, TEMP_FAHRENHEIT,
STATE_ON, STATE_OFF)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Read config and create Modbus devices """
sensors = []
slave = config.get("slave", None)
if modbus.TYPE == "serial" and not slave:
_LOGGER.error("No slave number provided for serial Modbus")
return False
registers = config.get("registers")
for regnum, register in registers.items():
if register.get("name"):
sensors.append(ModbusSensor(register.get("name"),
slave,
regnum,
None,
register.get("unit")))
if register.get("bits"):
bits = register.get("bits")
for bitnum, bit in bits.items():
if bit.get("name"):
sensors.append(ModbusSensor(bit.get("name"),
slave,
regnum,
bitnum))
add_devices(sensors)
class ModbusSensor(Entity):
# pylint: disable=too-many-arguments
""" Represents a Modbus Sensor """
def __init__(self, name, slave, register, bit=None, unit=None):
self._name = name
self.slave = int(slave) if slave else 1
self.register = int(register)
self.bit = int(bit) if bit else None
self._value = None
self._unit = unit
def __str__(self):
return "%s: %s" % (self.name, self.state)
@property
def should_poll(self):
""" We should poll, because slaves are not allowed to
initiate communication on Modbus networks"""
return True
@property
def unique_id(self):
""" Returns a unique id. """
return "MODBUS-SENSOR-{}-{}-{}".format(self.slave,
self.register,
self.bit)
@property
def state(self):
""" Returns the state of the sensor. """
if self.bit:
return STATE_ON if self._value else STATE_OFF
else:
return self._value
@property
def name(self):
""" Get the name of the sensor. """
return self._name
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
if self._unit == "C":
return TEMP_CELCIUS
elif self._unit == "F":
return TEMP_FAHRENHEIT
else:
return self._unit
@property
def state_attributes(self):
attr = super().state_attributes
return attr
def update(self):
result = modbus.NETWORK.read_holding_registers(unit=self.slave,
address=self.register,
count=1)
val = 0
for i, res in enumerate(result.registers):
val += res * (2**(i*16))
if self.bit:
self._value = val & (0x0001 << self.bit)
else:
self._value = val

View File

@ -0,0 +1,186 @@
"""
homeassistant.components.sensor.transmission
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Monitors Transmission BitTorrent client API
Configuration:
To use the Transmission sensor you will need to add something like the
following to your config/configuration.yaml
sensor:
platform: transmission
name: Transmission
host: 192.168.1.26
port: 9091
username: YOUR_USERNAME
password: YOUR_PASSWORD
monitored_variables:
- type: 'current_status'
- type: 'download_speed'
- type: 'upload_speed'
VARIABLES:
host
*Required
This is the IP address of your Transmission Daemon
Example: 192.168.1.32
port
*Optional
The port your Transmission daemon uses, defaults to 9091
Example: 8080
username
*Required
Your Transmission username
password
*Required
Your Transmission password
name
*Optional
The name to use when displaying this Transmission instance
monitored_variables
*Required
An array specifying the variables to monitor.
These are the variables for the monitored_variables array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
list of all available variables
"""
from homeassistant.util import Throttle
from datetime import timedelta
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.entity import Entity
# pylint: disable=no-name-in-module, import-error
import transmissionrpc
from transmissionrpc.error import TransmissionError
import logging
SENSOR_TYPES = {
'current_status': ['Status', ''],
'download_speed': ['Down Speed', 'MB/s'],
'upload_speed': ['Up Speed', 'MB/s']
}
_LOGGER = logging.getLogger(__name__)
_THROTTLED_REFRESH = None
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the sensors """
host = config.get(CONF_HOST)
username = config.get(CONF_USERNAME, None)
password = config.get(CONF_PASSWORD, None)
port = config.get('port', 9091)
name = config.get("name", "Transmission")
if not host:
_LOGGER.error('Missing config variable %s', CONF_HOST)
return False
# import logging
# logging.getLogger('transmissionrpc').setLevel(logging.DEBUG)
transmission_api = transmissionrpc.Client(
host, port=port, user=username, password=password)
try:
transmission_api.session_stats()
except TransmissionError:
_LOGGER.exception("Connection to Transmission API failed.")
return False
# pylint: disable=global-statement
global _THROTTLED_REFRESH
_THROTTLED_REFRESH = Throttle(timedelta(seconds=1))(
transmission_api.session_stats)
dev = []
for variable in config['monitored_variables']:
if variable['type'] not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable['type'])
else:
dev.append(TransmissionSensor(
variable['type'], transmission_api, name))
add_devices(dev)
class TransmissionSensor(Entity):
""" A Transmission sensor """
def __init__(self, sensor_type, transmission_client, client_name):
self._name = SENSOR_TYPES[sensor_type][0]
self.transmission_client = transmission_client
self.type = sensor_type
self.client_name = client_name
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@property
def name(self):
return self.client_name + ' ' + self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
def refresh_transmission_data(self):
""" Calls the throttled Transmission refresh method. """
if _THROTTLED_REFRESH is not None:
try:
_THROTTLED_REFRESH()
except TransmissionError:
_LOGGER.exception(
self.name + " Connection to Transmission API failed."
)
def update(self):
""" Gets the latest from Transmission and updates the state. """
self.refresh_transmission_data()
if self.type == 'current_status':
if self.transmission_client.session:
upload = self.transmission_client.session.uploadSpeed
download = self.transmission_client.session.downloadSpeed
if upload > 0 and download > 0:
self._state = 'Up/Down'
elif upload > 0 and download == 0:
self._state = 'Seeding'
elif upload == 0 and download > 0:
self._state = 'Downloading'
else:
self._state = 'Idle'
else:
self._state = 'Unknown'
if self.transmission_client.session:
if self.type == 'download_speed':
mb_spd = float(self.transmission_client.session.downloadSpeed)
mb_spd = mb_spd / 1024 / 1024
self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1)
elif self.type == 'upload_speed':
mb_spd = float(self.transmission_client.session.uploadSpeed)
mb_spd = mb_spd / 1024 / 1024
self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1)

View File

@ -10,7 +10,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import group, discovery, wink
from homeassistant.components import group, discovery, wink, isy994
DOMAIN = 'switch'
DEPENDENCIES = []
@ -30,6 +30,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
DISCOVERY_PLATFORMS = {
discovery.services.BELKIN_WEMO: 'wemo',
wink.DISCOVER_SWITCHES: 'wink',
isy994.DISCOVER_SWITCHES: 'isy994',
}
_LOGGER = logging.getLogger(__name__)

View File

@ -1,4 +1,4 @@
""" Demo platform that has two fake switchces. """
""" Demo platform that has two fake switches. """
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
@ -30,7 +30,7 @@ class DemoSwitch(ToggleEntity):
@property
def state(self):
""" Returns the name of the device if any. """
""" Returns the state of the device if any. """
return self._state
@property

View File

@ -0,0 +1,82 @@
""" Support for ISY994 switch. """
# system imports
import logging
# homeassistant imports
from homeassistant.components.isy994 import (ISY, ISYDeviceABC, SENSOR_STRING,
HIDDEN_STRING)
from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED
# The frontend doesn't seem to fully support the open and closed states yet.
# Once it does, the HA.doors programs should report open and closed instead of
# off and on. It appears that on should be open and off should be closed.
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the isy994 platform. """
# pylint: disable=too-many-locals
logger = logging.getLogger(__name__)
devs = []
# verify connection
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
return False
# import not dimmable nodes and groups
for (path, node) in ISY.nodes:
if not node.dimmable and SENSOR_STRING not in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYSwitchDevice(node))
# import ISY doors programs
for folder_name, states in (('HA.doors', [STATE_ON, STATE_OFF]),
('HA.switches', [STATE_ON, STATE_OFF])):
try:
folder = ISY.programs['My Programs'][folder_name]
except KeyError:
# HA.doors folder does not exist
pass
else:
for dtype, name, node_id in folder.children:
if dtype is 'folder':
custom_switch = folder[node_id]
try:
actions = custom_switch['actions'].leaf
assert actions.dtype == 'program', 'Not a program'
node = custom_switch['status'].leaf
except (KeyError, AssertionError):
pass
else:
devs.append(ISYProgramDevice(name, node, actions,
states))
add_devices(devs)
class ISYSwitchDevice(ISYDeviceABC):
""" represents as isy light within home assistant. """
_domain = 'switch'
_dtype = 'binary'
_states = [STATE_ON, STATE_OFF]
class ISYProgramDevice(ISYSwitchDevice):
""" represents a door that can be manipulated within home assistant. """
_domain = 'switch'
_dtype = 'binary'
def __init__(self, name, node, actions, states):
super().__init__(node)
self._states = states
self._name = name
self.action_node = actions
def turn_on(self, **kwargs):
""" turns the device on/closes the device """
self.action_node.runThen()
def turn_off(self, **kwargs):
""" turns the device off/opens the device """
self.action_node.runElse()

View File

@ -0,0 +1,121 @@
"""
Support for Modbus switches.
Configuration:
To use the Modbus switches you will need to add something like the following to
your config/configuration.yaml
sensor:
platform: modbus
slave: 1
registers:
24:
bits:
0:
name: My switch
2:
name: My other switch
VARIABLES:
- "slave" = slave number (ignored and can be omitted if not serial Modbus)
- "registers" contains a list of relevant registers to read from
- it must contain a "bits" section, listing relevant bits
- each named bit will create a switch
"""
import logging
import homeassistant.components.modbus as modbus
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Read config and create Modbus devices """
switches = []
slave = config.get("slave", None)
if modbus.TYPE == "serial" and not slave:
_LOGGER.error("No slave number provided for serial Modbus")
return False
registers = config.get("registers")
for regnum, register in registers.items():
bits = register.get("bits")
for bitnum, bit in bits.items():
if bit.get("name"):
switches.append(ModbusSwitch(bit.get("name"),
slave,
regnum,
bitnum))
add_devices(switches)
class ModbusSwitch(ToggleEntity):
""" Represents a Modbus Switch """
def __init__(self, name, slave, register, bit):
self._name = name
self.slave = int(slave) if slave else 1
self.register = int(register)
self.bit = int(bit)
self._is_on = None
self.register_value = None
def __str__(self):
return "%s: %s" % (self.name, self.state)
@property
def should_poll(self):
""" We should poll, because slaves are not allowed to
initiate communication on Modbus networks"""
return True
@property
def unique_id(self):
""" Returns a unique id. """
return "MODBUS-SWITCH-{}-{}-{}".format(self.slave,
self.register,
self.bit)
@property
def is_on(self):
""" Returns True if switch is on. """
return self._is_on
@property
def name(self):
""" Get the name of the switch. """
return self._name
@property
def state_attributes(self):
attr = super().state_attributes
return attr
def turn_on(self, **kwargs):
if self.register_value is None:
self.update()
val = self.register_value | (0x0001 << self.bit)
modbus.NETWORK.write_register(unit=self.slave,
address=self.register,
value=val)
def turn_off(self, **kwargs):
if self.register_value is None:
self.update()
val = self.register_value & ~(0x0001 << self.bit)
modbus.NETWORK.write_register(unit=self.slave,
address=self.register,
value=val)
def update(self):
result = modbus.NETWORK.read_holding_registers(unit=self.slave,
address=self.register,
count=1)
val = 0
for i, res in enumerate(result.registers):
val += res * (2**(i*16))
self.register_value = val
self._is_on = (val & (0x0001 << self.bit) > 0)

View File

@ -11,6 +11,7 @@ CONF_LONGITUDE = "longitude"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_NAME = "name"
CONF_TIME_ZONE = "time_zone"
CONF_VISIBILITY = "visibility"
CONF_PLATFORM = "platform"
CONF_HOST = "host"
@ -86,6 +87,9 @@ ATTR_TRIPPED = "device_tripped"
# time the device was tripped
ATTR_LAST_TRIP_TIME = "last_tripped_time"
# For all entity's, this hold whether or not it should be hidden
ATTR_HIDDEN = "hidden"
# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"

View File

@ -8,16 +8,22 @@ Provides ABC for entities in HA.
from homeassistant import NoEntitySpecifiedError
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF,
DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT)
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN, STATE_ON,
STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT)
# Dict mapping entity_id to a boolean that overwrites the hidden property
_OVERWRITE_HIDDEN = {}
class Entity(object):
""" ABC for Home Assistant entities. """
# pylint: disable=no-self-use
hass = None
entity_id = None
_hidden = False
# SAFE TO OVERWRITE
# The properties and methods here are safe to overwrite when inherting this
# class. These may be used to customize the behavior of the entity.
@property
def should_poll(self):
@ -52,6 +58,20 @@ class Entity(object):
""" Unit of measurement of this entity, if any. """
return None
@property
def hidden(self):
""" Suggestion if the entity should be hidden from UIs. """
return self._hidden
@hidden.setter
def hidden(self, val):
""" Sets the suggestion for visibility. """
self._hidden = bool(val)
def update(self):
""" Retrieve latest state. """
pass
# DEPRECATION NOTICE:
# Device is moving from getters to properties.
# For now the new properties will call the old functions
@ -69,9 +89,13 @@ class Entity(object):
""" Returns optional state attributes. """
return None
def update(self):
""" Retrieve latest state. """
pass
# DO NOT OVERWRITE
# These properties and methods are either managed by Home Assistant or they
# are used to perform a very specific function. Overwriting these may
# produce undesirable effects in the entity's operation.
hass = None
entity_id = None
def update_ha_state(self, force_refresh=False):
"""
@ -97,6 +121,9 @@ class Entity(object):
if ATTR_UNIT_OF_MEASUREMENT not in attr and self.unit_of_measurement:
attr[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
if _OVERWRITE_HIDDEN.get(self.entity_id, self.hidden):
attr[ATTR_HIDDEN] = True
# Convert temperature if we detect one
if attr.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_CELCIUS,
TEMP_FAHRENHEIT):
@ -115,6 +142,17 @@ class Entity(object):
def __repr__(self):
return "<Entity {}: {}>".format(self.name, self.state)
@staticmethod
def overwrite_hidden(entity_id, hidden):
"""
Overwrite the hidden property of an entity.
Set hidden to None to remove any overwritten value in place.
"""
if hidden is None:
_OVERWRITE_HIDDEN.pop(entity_id, None)
else:
_OVERWRITE_HIDDEN[entity_id.lower()] = hidden
class ToggleEntity(Entity):
""" ABC for entities that can be turned on and off. """

View File

@ -34,8 +34,14 @@ python-nest>=2.1
# z-wave
pydispatcher>=2.0.5
# isy994
PyISY>=1.0.2
# sensor.systemmonitor
psutil>=2.2.1
#pushover notifications
python-pushover>=0.2
python-pushover>=0.2
# Transmission Torrent Client
transmissionrpc>=0.11

View File

@ -34,5 +34,5 @@ if [ $(command -v md5) ]; then
elif [ $(command -v md5sum) ]; then
echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> version.py
else
echo 'Could not find a MD5 utility'
echo 'Could not find an MD5 utility'
fi

97
scripts/get_entities.py Executable file
View File

@ -0,0 +1,97 @@
#! /usr/bin/python
"""
Query the Home Assistant API for available entities then print them and any
desired attributes to the screen.
"""
import sys
import getpass
import argparse
try:
from urllib2 import urlopen
PYTHON = 2
except ImportError:
from urllib.request import urlopen
PYTHON = 3
import json
def main(password, askpass, attrs, address, port):
""" fetch Home Assistant api json page and post process """
# ask for password
if askpass:
password = getpass.getpass('Home Assistant API Password: ')
# fetch API result
url = mk_url(address, port, password)
response = urlopen(url).read()
if PYTHON == 3:
response = response.decode('utf-8')
data = json.loads(response)
# parse data
output = {'entity_id': []}
output.update([(attr, []) for attr in attrs])
for item in data:
output['entity_id'].append(item['entity_id'])
for attr in attrs:
output[attr].append(item['attributes'].get(attr, ''))
# output data
print_table(output, ['entity_id'] + attrs)
def print_table(data, columns):
""" format and print a table of data from a dictionary """
# get column lengths
lengths = {}
for key, value in data.items():
lengths[key] = max([len(str(val)) for val in value] + [len(key)])
# print header
for item in columns:
itemup = item.upper()
sys.stdout.write(itemup + ' ' * (lengths[item] - len(item) + 4))
sys.stdout.write('\n')
# print body
for ind in range(len(data[columns[0]])):
for item in columns:
val = str(data[item][ind])
sys.stdout.write(val + ' ' * (lengths[item] - len(val) + 4))
sys.stdout.write("\n")
def mk_url(address, port, password):
""" construct the url call for the api states page """
url = ''
if address.startswith('http://'):
url += address
else:
url += 'http://' + address
url += ':' + port + '/api/states?'
if password is not None:
url += 'api_password=' + password
return url
if __name__ == "__main__":
all_options = {'password': None, 'askpass': False, 'attrs': [],
'address': 'localhost', 'port': '8123'}
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('attrs', metavar='ATTRIBUTE', type=str, nargs='*',
help='an attribute to read from the state')
parser.add_argument('--password', dest='password', default=None,
type=str, help='API password for the HA server')
parser.add_argument('--ask-password', dest='askpass', default=False,
action='store_const', const=True,
help='prompt for HA API password')
parser.add_argument('--addr', dest='address',
default='localhost', type=str,
help='address of the HA server')
parser.add_argument('--port', dest='port', default='8123',
type=str, help='port that HA is hosting on')
args = parser.parse_args()
main(args.password, args.askpass, args.attrs, args.address, args.port)

78
scripts/homeassistant-pi.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/sh
# To script is for running Home Assistant as a service and automatically starting it on boot.
# Assuming you have cloned the HA repo into /home/pi/Apps/home-assistant adjust this path if necessary
# This also assumes you installed HA on your raspberry pi using the instructions here:
# https://home-assistant.io/getting-started/
#
# To install to the following:
# sudo cp /home/pi/Apps/home-assistant/scripts/homeassistant-pi.sh /etc/init.d/homeassistant.sh
# sudo chmod +x /etc/init.d/homeassistant.sh
# sudo chown root:root /etc/init.d/homeassistant.sh
#
# If you want HA to start on boot also run the following:
# sudo update-rc.d homeassistant.sh defaults
# sudo update-rc.d homeassistant.sh enable
#
# You should now be able to start HA by running
# sudo /etc/init.d/homeassistant.sh start
### BEGIN INIT INFO
# Provides: myservice
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Put a short description of the service here
# Description: Put a long description of the service here
### END INIT INFO
# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=/home/pi/Apps/home-assistant
DAEMON="/home/pi/.pyenv/shims/python3 -m homeassistant"
DAEMON_NAME=homeassistant
# Add any command line options for your daemon here
DAEMON_OPTS=""
# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=pi
# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid
. /lib/lsb/init-functions
do_start () {
log_daemon_msg "Starting system $DAEMON_NAME daemon"
start-stop-daemon --start --background --chdir $DIR --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
log_end_msg $?
}
do_stop () {
log_daemon_msg "Stopping system $DAEMON_NAME daemon"
start-stop-daemon --stop --pidfile $PIDFILE --retry 10
log_end_msg $?
}
case "$1" in
start|stop)
do_${1}
;;
restart|reload|force-reload)
do_stop
do_start
;;
status)
status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
;;
*)
echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0

View File

@ -54,7 +54,7 @@ class TestComponentsGroup(unittest.TestCase):
self.hass, 'light_and_nothing',
['light.Bowl', 'non.existing'])
self.assertEqual(STATE_ON, grp.state.state)
self.assertEqual(STATE_ON, grp.state)
def test_setup_group_with_non_groupable_states(self):
self.hass.states.set('cast.living_room', "Plex")
@ -64,13 +64,13 @@ class TestComponentsGroup(unittest.TestCase):
self.hass, 'chromecasts',
['cast.living_room', 'cast.bedroom'])
self.assertEqual(STATE_UNKNOWN, grp.state.state)
self.assertEqual(STATE_UNKNOWN, grp.state)
def test_setup_empty_group(self):
""" Try to setup an empty group. """
grp = group.setup_group(self.hass, 'nothing', [])
self.assertEqual(STATE_UNKNOWN, grp.state.state)
self.assertEqual(STATE_UNKNOWN, grp.state)
def test_monitor_group(self):
""" Test if the group keeps track of states. """

View File

@ -0,0 +1,60 @@
"""
tests.test_helper_entity
~~~~~~~~~~~~~~~~~~~~~~~~
Tests the entity helper.
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
import homeassistant as ha
import homeassistant.helpers.entity as entity
from homeassistant.const import ATTR_HIDDEN
class TestHelpersEntity(unittest.TestCase):
""" Tests homeassistant.helpers.entity module. """
def setUp(self): # pylint: disable=invalid-name
""" Init needed objects. """
self.entity = entity.Entity()
self.entity.entity_id = 'test.overwrite_hidden_true'
self.hass = self.entity.hass = ha.HomeAssistant()
self.entity.update_ha_state()
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
entity.Entity.overwrite_hidden(self.entity.entity_id, None)
def test_default_hidden_not_in_attributes(self):
""" Test that the default hidden property is set to False. """
self.assertNotIn(
ATTR_HIDDEN,
self.hass.states.get(self.entity.entity_id).attributes)
def test_setting_hidden_to_true(self):
self.entity.hidden = True
self.entity.update_ha_state()
state = self.hass.states.get(self.entity.entity_id)
self.assertTrue(state.attributes.get(ATTR_HIDDEN))
def test_overwriting_hidden_property_to_true(self):
""" Test we can overwrite hidden property to True. """
entity.Entity.overwrite_hidden(self.entity.entity_id, True)
self.entity.update_ha_state()
state = self.hass.states.get(self.entity.entity_id)
self.assertTrue(state.attributes.get(ATTR_HIDDEN))
def test_overwriting_hidden_property_to_false(self):
""" Test we can overwrite hidden property to True. """
entity.Entity.overwrite_hidden(self.entity.entity_id, False)
self.entity.hidden = True
self.entity.update_ha_state()
self.assertNotIn(
ATTR_HIDDEN,
self.hass.states.get(self.entity.entity_id).attributes)