commit
31a22d4c6a
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
|
@ -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));
|
||||
|
|
|
@ -144,7 +144,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
this.states = states.toArray();
|
||||
this.states = states.toArray().filter(
|
||||
function (el) {return !el.attributes.hidden});
|
||||
},
|
||||
|
||||
handleRefreshClick: function() {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
|
@ -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
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 []
|
|
@ -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
|
|
@ -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)
|
|
@ -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__)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
||||
|
||||
# Transmission Torrent Client
|
||||
transmissionrpc>=0.11
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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. """
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue