@ -91,6 +91,7 @@ omit =
|
|||||||
homeassistant/components/knx.py
|
homeassistant/components/knx.py
|
||||||
homeassistant/components/switch/knx.py
|
homeassistant/components/switch/knx.py
|
||||||
homeassistant/components/binary_sensor/knx.py
|
homeassistant/components/binary_sensor/knx.py
|
||||||
|
homeassistant/components/thermostat/knx.py
|
||||||
|
|
||||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.py
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
@ -129,21 +130,25 @@ omit =
|
|||||||
homeassistant/components/joaoapps_join.py
|
homeassistant/components/joaoapps_join.py
|
||||||
homeassistant/components/keyboard.py
|
homeassistant/components/keyboard.py
|
||||||
homeassistant/components/light/blinksticklight.py
|
homeassistant/components/light/blinksticklight.py
|
||||||
|
homeassistant/components/light/flux_led.py
|
||||||
homeassistant/components/light/hue.py
|
homeassistant/components/light/hue.py
|
||||||
homeassistant/components/light/hyperion.py
|
homeassistant/components/light/hyperion.py
|
||||||
homeassistant/components/light/lifx.py
|
homeassistant/components/light/lifx.py
|
||||||
homeassistant/components/light/limitlessled.py
|
homeassistant/components/light/limitlessled.py
|
||||||
homeassistant/components/light/osramlightify.py
|
homeassistant/components/light/osramlightify.py
|
||||||
|
homeassistant/components/light/x10.py
|
||||||
homeassistant/components/lirc.py
|
homeassistant/components/lirc.py
|
||||||
homeassistant/components/media_player/braviatv.py
|
homeassistant/components/media_player/braviatv.py
|
||||||
homeassistant/components/media_player/cast.py
|
homeassistant/components/media_player/cast.py
|
||||||
homeassistant/components/media_player/cmus.py
|
homeassistant/components/media_player/cmus.py
|
||||||
homeassistant/components/media_player/denon.py
|
homeassistant/components/media_player/denon.py
|
||||||
|
homeassistant/components/media_player/directv.py
|
||||||
homeassistant/components/media_player/firetv.py
|
homeassistant/components/media_player/firetv.py
|
||||||
homeassistant/components/media_player/gpmdp.py
|
homeassistant/components/media_player/gpmdp.py
|
||||||
homeassistant/components/media_player/itunes.py
|
homeassistant/components/media_player/itunes.py
|
||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/lg_netcast.py
|
homeassistant/components/media_player/lg_netcast.py
|
||||||
|
homeassistant/components/media_player/mpchc.py
|
||||||
homeassistant/components/media_player/mpd.py
|
homeassistant/components/media_player/mpd.py
|
||||||
homeassistant/components/media_player/onkyo.py
|
homeassistant/components/media_player/onkyo.py
|
||||||
homeassistant/components/media_player/panasonic_viera.py
|
homeassistant/components/media_player/panasonic_viera.py
|
||||||
@ -151,6 +156,7 @@ omit =
|
|||||||
homeassistant/components/media_player/pioneer.py
|
homeassistant/components/media_player/pioneer.py
|
||||||
homeassistant/components/media_player/plex.py
|
homeassistant/components/media_player/plex.py
|
||||||
homeassistant/components/media_player/roku.py
|
homeassistant/components/media_player/roku.py
|
||||||
|
homeassistant/components/media_player/russound_rnet.py
|
||||||
homeassistant/components/media_player/samsungtv.py
|
homeassistant/components/media_player/samsungtv.py
|
||||||
homeassistant/components/media_player/snapcast.py
|
homeassistant/components/media_player/snapcast.py
|
||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
@ -161,7 +167,6 @@ omit =
|
|||||||
homeassistant/components/notify/aws_sqs.py
|
homeassistant/components/notify/aws_sqs.py
|
||||||
homeassistant/components/notify/free_mobile.py
|
homeassistant/components/notify/free_mobile.py
|
||||||
homeassistant/components/notify/gntp.py
|
homeassistant/components/notify/gntp.py
|
||||||
homeassistant/components/notify/googlevoice.py
|
|
||||||
homeassistant/components/notify/instapush.py
|
homeassistant/components/notify/instapush.py
|
||||||
homeassistant/components/notify/joaoapps_join.py
|
homeassistant/components/notify/joaoapps_join.py
|
||||||
homeassistant/components/notify/message_bird.py
|
homeassistant/components/notify/message_bird.py
|
||||||
|
5
.gitignore
vendored
@ -7,9 +7,10 @@ config/custom_components/*
|
|||||||
!config/custom_components/example.py
|
!config/custom_components/example.py
|
||||||
!config/custom_components/hello_world.py
|
!config/custom_components/hello_world.py
|
||||||
!config/custom_components/mqtt_example.py
|
!config/custom_components/mqtt_example.py
|
||||||
|
!config/custom_components/react_panel
|
||||||
|
|
||||||
tests/config/deps
|
tests/testing_config/deps
|
||||||
tests/config/home-assistant.log
|
tests/testing_config/home-assistant.log
|
||||||
|
|
||||||
# Hide sublime text stuff
|
# Hide sublime text stuff
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
|
@ -8,8 +8,13 @@ matrix:
|
|||||||
env: TOXENV=requirements
|
env: TOXENV=requirements
|
||||||
- python: "3.5"
|
- python: "3.5"
|
||||||
env: TOXENV=lint
|
env: TOXENV=lint
|
||||||
|
- python: "3.5"
|
||||||
|
env: TOXENV=typing
|
||||||
- python: "3.5"
|
- python: "3.5"
|
||||||
env: TOXENV=py35
|
env: TOXENV=py35
|
||||||
|
allow_failures:
|
||||||
|
- python: "3.5"
|
||||||
|
env: TOXENV=typing
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.cache/pip
|
- $HOME/.cache/pip
|
||||||
|
@ -20,7 +20,8 @@ RUN script/build_python_openzwave && \
|
|||||||
|
|
||||||
COPY requirements_all.txt requirements_all.txt
|
COPY requirements_all.txt requirements_all.txt
|
||||||
# certifi breaks Debian based installs
|
# certifi breaks Debian based installs
|
||||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
|
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
|
||||||
|
pip3 install mysqlclient psycopg2
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
@ -67,7 +67,7 @@ Build home automation on top of your devices:
|
|||||||
- Turn on the lights when people get home after sunset
|
- Turn on the lights when people get home after sunset
|
||||||
- Turn on lights slowly during sunset to compensate for less light
|
- Turn on lights slowly during sunset to compensate for less light
|
||||||
- Turn off all lights and devices when everybody leaves the house
|
- Turn off all lights and devices when everybody leaves the house
|
||||||
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
|
- Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
|
||||||
and can interface with MQTT for easy integration with other projects
|
and can interface with MQTT for easy integration with other projects
|
||||||
like `OwnTracks <http://owntracks.org/>`__
|
like `OwnTracks <http://owntracks.org/>`__
|
||||||
- Allow sending notifications using
|
- Allow sending notifications using
|
||||||
|
30
config/custom_components/react_panel/__init__.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Custom panel example showing TodoMVC using React.
|
||||||
|
|
||||||
|
Will add a panel to control lights and switches using React. Allows configuring
|
||||||
|
the title via configuration.yaml:
|
||||||
|
|
||||||
|
react_panel:
|
||||||
|
title: 'home'
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from homeassistant.components.frontend import register_panel
|
||||||
|
|
||||||
|
DOMAIN = 'react_panel'
|
||||||
|
DEPENDENCIES = ['frontend']
|
||||||
|
|
||||||
|
PANEL_PATH = os.path.join(os.path.dirname(__file__), 'panel.html')
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Initialize custom panel."""
|
||||||
|
title = config.get(DOMAIN, {}).get('title')
|
||||||
|
|
||||||
|
config = None if title is None else {'title': title}
|
||||||
|
|
||||||
|
register_panel(hass, 'react', PANEL_PATH,
|
||||||
|
title='TodoMVC', icon='mdi:checkbox-marked-outline',
|
||||||
|
config=config)
|
||||||
|
return True
|
415
config/custom_components/react_panel/panel.html
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
<script src="https://fb.me/react-15.2.1.min.js"></script>
|
||||||
|
<script src="https://fb.me/react-dom-15.2.1.min.js"></script>
|
||||||
|
|
||||||
|
<!-- for development, replace with:
|
||||||
|
<script src="https://fb.me/react-15.2.1.js"></script>
|
||||||
|
<script src="https://fb.me/react-dom-15.2.1.js"></script>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
CSS taken from ReactJS TodoMVC example by Pete Hunt
|
||||||
|
http://todomvc.com/examples/react/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.todoapp input[type="checkbox"] {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp {
|
||||||
|
background: #fff;
|
||||||
|
margin: 130px 0 40px 0;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||||
|
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp h1 {
|
||||||
|
position: absolute;
|
||||||
|
top: -155px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 100px;
|
||||||
|
font-weight: 100;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(175, 47, 47, 0.15);
|
||||||
|
-webkit-text-rendering: optimizeLegibility;
|
||||||
|
-moz-text-rendering: optimizeLegibility;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .main {
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li .toggle {
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto 0;
|
||||||
|
border: none; /* Mobile Safari */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li .toggle:focus {
|
||||||
|
border-left: 3px solid rgba(175, 47, 47, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li .toggle:after {
|
||||||
|
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li .toggle:checked:after {
|
||||||
|
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li label {
|
||||||
|
white-space: pre-line;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 15px 60px 15px 15px;
|
||||||
|
margin-left: 45px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li.completed label {
|
||||||
|
color: #d9d9d9;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .footer {
|
||||||
|
color: #777;
|
||||||
|
padding: 10px 15px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .footer:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||||
|
0 8px 0 -3px #f6f6f6,
|
||||||
|
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 16px 0 -6px #f6f6f6,
|
||||||
|
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-count {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .toggle-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: rgba(175, 47, 47, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .filters {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .filters li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .filters li a {
|
||||||
|
color: inherit;
|
||||||
|
margin: 3px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .filters li a.selected,
|
||||||
|
.filters li a:hover {
|
||||||
|
border-color: rgba(175, 47, 47, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .filters li a.selected {
|
||||||
|
border-color: rgba(175, 47, 47, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hack to remove background from Mobile Safari.
|
||||||
|
Can't use it globally since it destroys checkboxes in Firefox
|
||||||
|
*/
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||||
|
.todoapp .toggle-all,
|
||||||
|
.todoapp .todo-list li .toggle {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .todo-list li .toggle {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .toggle-all {
|
||||||
|
-webkit-transform: rotate(90deg);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.todoapp .footer {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp .filters {
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<dom-module id='ha-panel-react'>
|
||||||
|
<template>
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.mount {
|
||||||
|
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.4em;
|
||||||
|
color: #4d4d4d;
|
||||||
|
min-width: 230px;
|
||||||
|
max-width: 550px;
|
||||||
|
margin: 0 auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-font-smoothing: antialiased;
|
||||||
|
font-smoothing: antialiased;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id='mount' class='mount'></div>
|
||||||
|
</template>
|
||||||
|
</dom-module>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Example uses ES6. Will only work in modern browsers
|
||||||
|
class TodoMVC extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
filter: 'all',
|
||||||
|
// load initial value of entities
|
||||||
|
entities: this.props.hass.reactor.evaluate(
|
||||||
|
this.props.hass.entityGetters.visibleEntityMap),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// register to entity updates
|
||||||
|
this._unwatchHass = this.props.hass.reactor.observe(
|
||||||
|
this.props.hass.entityGetters.visibleEntityMap,
|
||||||
|
entities => this.setState({entities}))
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// unregister to entity updates
|
||||||
|
this._unwatchHass();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePickFilter(filter, ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.setState({filter});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEntityToggle(entity, ev) {
|
||||||
|
this.props.hass.serviceActions.callService(
|
||||||
|
entity.domain, 'toggle', { entity_id: entity.entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleMenu(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
Polymer.Base.fire('open-menu', null, {node: ev.target});
|
||||||
|
}
|
||||||
|
|
||||||
|
entityRow(entity) {
|
||||||
|
const completed = entity.state === 'on';
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
'li', {
|
||||||
|
className: completed && 'completed',
|
||||||
|
key: entity.entityId,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
"div", { className: "view" },
|
||||||
|
React.createElement(
|
||||||
|
"input", {
|
||||||
|
checked: completed,
|
||||||
|
className: "toggle",
|
||||||
|
type: "checkbox",
|
||||||
|
onChange: ev => this.handleEntityToggle(entity, ev),
|
||||||
|
}),
|
||||||
|
React.createElement("label", null, entity.entityDisplay)));
|
||||||
|
}
|
||||||
|
|
||||||
|
filterRow(filter) {
|
||||||
|
return React.createElement(
|
||||||
|
"li", { key: filter },
|
||||||
|
React.createElement(
|
||||||
|
"a", {
|
||||||
|
href: "#",
|
||||||
|
className: this.state.filter === filter && "selected",
|
||||||
|
onClick: ev => this.handlePickFilter(filter, ev),
|
||||||
|
},
|
||||||
|
filter.substring(0, 1).toUpperCase() + filter.substring(1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { entities, filter } = this.state;
|
||||||
|
|
||||||
|
if (!entities) return null;
|
||||||
|
|
||||||
|
const filters = ['all', 'light', 'switch'];
|
||||||
|
|
||||||
|
const showEntities = filter === 'all' ?
|
||||||
|
entities.filter(ent => filters.includes(ent.domain)) :
|
||||||
|
entities.filter(ent => ent.domain == filter);
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
'div', { className: 'todoapp-wrapper' },
|
||||||
|
React.createElement(
|
||||||
|
"section", { className: "todoapp" },
|
||||||
|
React.createElement(
|
||||||
|
"div", null,
|
||||||
|
React.createElement(
|
||||||
|
"header", { className: "header" },
|
||||||
|
React.createElement("h1", null, this.props.title || "todos")
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
"section", { className: "main" },
|
||||||
|
React.createElement(
|
||||||
|
"ul", { className: "todo-list" },
|
||||||
|
showEntities.valueSeq().map(ent => this.entityRow(ent)))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
"footer", { className: "footer" },
|
||||||
|
React.createElement(
|
||||||
|
"span", { className: "todo-count" },
|
||||||
|
showEntities.filter(ent => ent.state === 'off').size + " items left"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
"ul", { className: "filters" },
|
||||||
|
filters.map(filter => this.filterRow(filter))
|
||||||
|
),
|
||||||
|
!this.props.showMenu && React.createElement(
|
||||||
|
"a", {
|
||||||
|
className: "toggle-menu",
|
||||||
|
href: '#',
|
||||||
|
onClick: ev => this.handleToggleMenu(ev),
|
||||||
|
},
|
||||||
|
"Show menu"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Polymer({
|
||||||
|
is: 'ha-panel-react',
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
// Home Assistant object
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If should render in narrow mode
|
||||||
|
narrow: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If sidebar is currently shown
|
||||||
|
showMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Home Assistant panel info
|
||||||
|
// panel.config contains config passed to register_panel serverside
|
||||||
|
panel: {
|
||||||
|
type: Object,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// This will make sure we forward changed properties to React
|
||||||
|
observers: [
|
||||||
|
'propsChanged(hass, narrow, showMenu, panel)',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Mount React when element attached
|
||||||
|
attached: function () {
|
||||||
|
this.mount(this.hass, this.narrow, this.showMenu, this.panel);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called when properties change
|
||||||
|
propsChanged: function (hass, narrow, showMenu, panel) {
|
||||||
|
this.mount(hass, narrow, showMenu, panel);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Render React. Debounce in case multiple properties change.
|
||||||
|
mount: function (hass, narrow, showMenu, panel) {
|
||||||
|
this.debounce('mount', function () {
|
||||||
|
ReactDOM.render(React.createElement(TodoMVC, {
|
||||||
|
hass: hass,
|
||||||
|
narrow: narrow,
|
||||||
|
showMenu: showMenu,
|
||||||
|
title: panel.config ? panel.config.title : null
|
||||||
|
}), this.$.mount);
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Unmount React node when panel no longer in use.
|
||||||
|
detached: function () {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.$.mount);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -8,6 +8,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
__version__,
|
__version__,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
@ -16,7 +18,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_python():
|
def validate_python() -> None:
|
||||||
"""Validate we're running the right Python version."""
|
"""Validate we're running the right Python version."""
|
||||||
major, minor = sys.version_info[:2]
|
major, minor = sys.version_info[:2]
|
||||||
req_major, req_minor = REQUIRED_PYTHON_VER
|
req_major, req_minor = REQUIRED_PYTHON_VER
|
||||||
@ -27,7 +29,7 @@ def validate_python():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def ensure_config_path(config_dir):
|
def ensure_config_path(config_dir: str) -> None:
|
||||||
"""Validate the configuration directory."""
|
"""Validate the configuration directory."""
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
lib_dir = os.path.join(config_dir, 'deps')
|
lib_dir = os.path.join(config_dir, 'deps')
|
||||||
@ -56,7 +58,7 @@ def ensure_config_path(config_dir):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def ensure_config_file(config_dir):
|
def ensure_config_file(config_dir: str) -> str:
|
||||||
"""Ensure configuration file exists."""
|
"""Ensure configuration file exists."""
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
config_path = config_util.ensure_config_exists(config_dir)
|
config_path = config_util.ensure_config_exists(config_dir)
|
||||||
@ -68,7 +70,7 @@ def ensure_config_file(config_dir):
|
|||||||
return config_path
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
def get_arguments():
|
def get_arguments() -> argparse.Namespace:
|
||||||
"""Get parsed passed in arguments."""
|
"""Get parsed passed in arguments."""
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@ -125,12 +127,12 @@ def get_arguments():
|
|||||||
|
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
if os.name != "posix" or arguments.debug or arguments.runner:
|
if os.name != "posix" or arguments.debug or arguments.runner:
|
||||||
arguments.daemon = False
|
setattr(arguments, 'daemon', False)
|
||||||
|
|
||||||
return arguments
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
def daemonize():
|
def daemonize() -> None:
|
||||||
"""Move current process to daemon process."""
|
"""Move current process to daemon process."""
|
||||||
# Create first fork
|
# Create first fork
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
@ -155,7 +157,7 @@ def daemonize():
|
|||||||
os.dup2(outfd.fileno(), sys.stderr.fileno())
|
os.dup2(outfd.fileno(), sys.stderr.fileno())
|
||||||
|
|
||||||
|
|
||||||
def check_pid(pid_file):
|
def check_pid(pid_file: str) -> None:
|
||||||
"""Check that HA is not already running."""
|
"""Check that HA is not already running."""
|
||||||
# Check pid file
|
# Check pid file
|
||||||
try:
|
try:
|
||||||
@ -177,7 +179,7 @@ def check_pid(pid_file):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def write_pid(pid_file):
|
def write_pid(pid_file: str) -> None:
|
||||||
"""Create a PID File."""
|
"""Create a PID File."""
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
@ -187,7 +189,7 @@ def write_pid(pid_file):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def closefds_osx(min_fd, max_fd):
|
def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||||
"""Make sure file descriptors get closed when we restart.
|
"""Make sure file descriptors get closed when we restart.
|
||||||
|
|
||||||
We cannot call close on guarded fds, and we cannot easily test which fds
|
We cannot call close on guarded fds, and we cannot easily test which fds
|
||||||
@ -205,7 +207,7 @@ def closefds_osx(min_fd, max_fd):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def cmdline():
|
def cmdline() -> List[str]:
|
||||||
"""Collect path and arguments to re-execute the current hass instance."""
|
"""Collect path and arguments to re-execute the current hass instance."""
|
||||||
if sys.argv[0].endswith('/__main__.py'):
|
if sys.argv[0].endswith('/__main__.py'):
|
||||||
modulepath = os.path.dirname(sys.argv[0])
|
modulepath = os.path.dirname(sys.argv[0])
|
||||||
@ -213,16 +215,17 @@ def cmdline():
|
|||||||
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
||||||
|
|
||||||
|
|
||||||
def setup_and_run_hass(config_dir, args):
|
def setup_and_run_hass(config_dir: str,
|
||||||
|
args: argparse.Namespace) -> Optional[int]:
|
||||||
"""Setup HASS and run."""
|
"""Setup HASS and run."""
|
||||||
from homeassistant import bootstrap
|
from homeassistant import bootstrap
|
||||||
|
|
||||||
# Run a simple daemon runner process on Windows to handle restarts
|
# Run a simple daemon runner process on Windows to handle restarts
|
||||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||||
args = cmdline() + ['--runner']
|
nt_args = cmdline() + ['--runner']
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(args)
|
subprocess.check_call(nt_args)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
if exc.returncode != RESTART_EXIT_CODE:
|
if exc.returncode != RESTART_EXIT_CODE:
|
||||||
@ -244,7 +247,7 @@ def setup_and_run_hass(config_dir, args):
|
|||||||
log_rotate_days=args.log_rotate_days)
|
log_rotate_days=args.log_rotate_days)
|
||||||
|
|
||||||
if hass is None:
|
if hass is None:
|
||||||
return
|
return None
|
||||||
|
|
||||||
if args.open_ui:
|
if args.open_ui:
|
||||||
def open_browser(event):
|
def open_browser(event):
|
||||||
@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args):
|
|||||||
return exit_code
|
return exit_code
|
||||||
|
|
||||||
|
|
||||||
def try_to_restart():
|
def try_to_restart() -> None:
|
||||||
"""Attempt to clean up state and start a new homeassistant instance."""
|
"""Attempt to clean up state and start a new homeassistant instance."""
|
||||||
# Things should be mostly shut down already at this point, now just try
|
# Things should be mostly shut down already at this point, now just try
|
||||||
# to clean up things that may have been left behind.
|
# to clean up things that may have been left behind.
|
||||||
@ -303,7 +306,7 @@ def try_to_restart():
|
|||||||
os.execv(args[0], args)
|
os.execv(args[0], args)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> int:
|
||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
validate_python()
|
validate_python()
|
||||||
|
|
||||||
|
@ -7,6 +7,9 @@ import sys
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any, Optional, Dict
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components as core_components
|
import homeassistant.components as core_components
|
||||||
@ -30,7 +33,8 @@ ATTR_COMPONENT = 'component'
|
|||||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||||
|
|
||||||
|
|
||||||
def setup_component(hass, domain, config=None):
|
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||||
|
config: Optional[Dict]=None) -> bool:
|
||||||
"""Setup a component and all its dependencies."""
|
"""Setup a component and all its dependencies."""
|
||||||
if domain in hass.config.components:
|
if domain in hass.config.components:
|
||||||
return True
|
return True
|
||||||
@ -53,7 +57,8 @@ def setup_component(hass, domain, config=None):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _handle_requirements(hass, component, name):
|
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||||
|
name: str) -> bool:
|
||||||
"""Install the requirements for a component."""
|
"""Install the requirements for a component."""
|
||||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||||
return True
|
return True
|
||||||
@ -67,9 +72,10 @@ def _handle_requirements(hass, component, name):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _setup_component(hass, domain, config):
|
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||||
"""Setup a component for Home Assistant."""
|
"""Setup a component for Home Assistant."""
|
||||||
# pylint: disable=too-many-return-statements,too-many-branches
|
# pylint: disable=too-many-return-statements,too-many-branches
|
||||||
|
# pylint: disable=too-many-statements
|
||||||
if domain in hass.config.components:
|
if domain in hass.config.components:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -147,9 +153,15 @@ def _setup_component(hass, domain, config):
|
|||||||
_CURRENT_SETUP.append(domain)
|
_CURRENT_SETUP.append(domain)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not component.setup(hass, config):
|
result = component.setup(hass, config)
|
||||||
|
if result is False:
|
||||||
_LOGGER.error('component %s failed to initialize', domain)
|
_LOGGER.error('component %s failed to initialize', domain)
|
||||||
return False
|
return False
|
||||||
|
elif result is not True:
|
||||||
|
_LOGGER.error('component %s did not return boolean if setup '
|
||||||
|
'was successful. Disabling component.', domain)
|
||||||
|
loader.set_component(domain, None)
|
||||||
|
return False
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception('Error during setup of component %s', domain)
|
_LOGGER.exception('Error during setup of component %s', domain)
|
||||||
return False
|
return False
|
||||||
@ -169,7 +181,8 @@ def _setup_component(hass, domain, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def prepare_setup_platform(hass, config, domain, platform_name):
|
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||||
|
platform_name: str) -> Optional[ModuleType]:
|
||||||
"""Load a platform and makes sure dependencies are setup."""
|
"""Load a platform and makes sure dependencies are setup."""
|
||||||
_ensure_loader_prepared(hass)
|
_ensure_loader_prepared(hass)
|
||||||
|
|
||||||
@ -202,9 +215,14 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
|||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
def from_config_dict(config: Dict[str, Any],
|
||||||
verbose=False, skip_pip=False,
|
hass: Optional[core.HomeAssistant]=None,
|
||||||
log_rotate_days=None):
|
config_dir: Optional[str]=None,
|
||||||
|
enable_log: bool=True,
|
||||||
|
verbose: bool=False,
|
||||||
|
skip_pip: bool=False,
|
||||||
|
log_rotate_days: Any=None) \
|
||||||
|
-> Optional[core.HomeAssistant]:
|
||||||
"""Try to configure Home Assistant from a config dict.
|
"""Try to configure Home Assistant from a config dict.
|
||||||
|
|
||||||
Dynamically loads required components and its dependencies.
|
Dynamically loads required components and its dependencies.
|
||||||
@ -266,8 +284,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
|||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
def from_config_file(config_path: str,
|
||||||
log_rotate_days=None):
|
hass: Optional[core.HomeAssistant]=None,
|
||||||
|
verbose: bool=False,
|
||||||
|
skip_pip: bool=True,
|
||||||
|
log_rotate_days: Any=None):
|
||||||
"""Read the configuration file and try to start all the functionality.
|
"""Read the configuration file and try to start all the functionality.
|
||||||
|
|
||||||
Will add functionality to 'hass' parameter if given,
|
Will add functionality to 'hass' parameter if given,
|
||||||
@ -292,7 +313,8 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
|||||||
skip_pip=skip_pip)
|
skip_pip=skip_pip)
|
||||||
|
|
||||||
|
|
||||||
def enable_logging(hass, verbose=False, log_rotate_days=None):
|
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||||
|
log_rotate_days=None) -> None:
|
||||||
"""Setup the logging."""
|
"""Setup the logging."""
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||||
@ -343,12 +365,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
|
|||||||
'Unable to setup error log %s (access denied)', err_log_path)
|
'Unable to setup error log %s (access denied)', err_log_path)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_loader_prepared(hass):
|
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||||
"""Ensure Home Assistant loader is prepared."""
|
"""Ensure Home Assistant loader is prepared."""
|
||||||
if not loader.PREPARED:
|
if not loader.PREPARED:
|
||||||
loader.prepare(hass)
|
loader.prepare(hass)
|
||||||
|
|
||||||
|
|
||||||
def _mount_local_lib_path(config_dir):
|
def _mount_local_lib_path(config_dir: str) -> None:
|
||||||
"""Add local library to Python Path."""
|
"""Add local library to Python Path."""
|
||||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||||
|
@ -6,9 +6,6 @@ https://home-assistant.io/components/binary_sensor.vera/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.util.dt as dt_util
|
|
||||||
from homeassistant.const import (
|
|
||||||
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED)
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice)
|
BinarySensorDevice)
|
||||||
from homeassistant.components.vera import (
|
from homeassistant.components.vera import (
|
||||||
@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
|||||||
self._state = False
|
self._state = False
|
||||||
VeraDevice.__init__(self, vera_device, controller)
|
VeraDevice.__init__(self, vera_device, controller)
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the state attributes."""
|
|
||||||
attr = {}
|
|
||||||
if self.vera_device.has_battery:
|
|
||||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
|
||||||
|
|
||||||
if self.vera_device.is_armable:
|
|
||||||
armed = self.vera_device.is_armed
|
|
||||||
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
|
||||||
|
|
||||||
if self.vera_device.is_trippable:
|
|
||||||
last_tripped = self.vera_device.last_trip
|
|
||||||
if last_tripped is not None:
|
|
||||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
|
||||||
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
|
|
||||||
else:
|
|
||||||
attr[ATTR_LAST_TRIP_TIME] = None
|
|
||||||
tripped = self.vera_device.is_tripped
|
|
||||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
|
||||||
|
|
||||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
|
||||||
return attr
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if sensor is on."""
|
"""Return true if sensor is on."""
|
||||||
|
@ -13,14 +13,15 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
|
||||||
|
|
||||||
# These are the available sensors mapped to binary_sensor class
|
# These are the available sensors mapped to binary_sensor class
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
"opened": "opening",
|
"opened": "opening",
|
||||||
"brightness": "light",
|
"brightness": "light",
|
||||||
"vibration": "vibration",
|
"vibration": "vibration",
|
||||||
"loudness": "sound"
|
"loudness": "sound",
|
||||||
|
"liquid_detected": "moisture"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,6 +75,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||||||
return self.wink.vibration_boolean()
|
return self.wink.vibration_boolean()
|
||||||
elif self.capability == "brightness":
|
elif self.capability == "brightness":
|
||||||
return self.wink.brightness_boolean()
|
return self.wink.brightness_boolean()
|
||||||
|
elif self.capability == "liquid_detected":
|
||||||
|
return self.wink.liquid_boolean()
|
||||||
else:
|
else:
|
||||||
return self.wink.state()
|
return self.wink.state()
|
||||||
|
|
||||||
|
@ -94,7 +94,8 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
|||||||
|
|
||||||
def value_changed(self, value):
|
def value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id:
|
if self._value.value_id == value.value_id or \
|
||||||
|
self._value.node == value.node:
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@ ATTR_URL = 'url'
|
|||||||
ATTR_URL_DEFAULT = 'https://www.google.com'
|
ATTR_URL_DEFAULT = 'https://www.google.com'
|
||||||
|
|
||||||
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
|
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url,
|
# pylint: disable=no-value-for-parameter
|
||||||
|
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,17 +5,28 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/device_tracker.icloud/
|
https://home-assistant.io/components/device_tracker.icloud/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
|
||||||
|
EVENT_HOMEASSISTANT_START)
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
|
||||||
|
PLATFORM_SCHEMA)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyicloud==0.8.3']
|
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||||
|
|
||||||
CONF_INTERVAL = 'interval'
|
CONF_INTERVAL = 'interval'
|
||||||
DEFAULT_INTERVAL = 8
|
KEEPALIVE_INTERVAL = 4
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_USERNAME): vol.Coerce(str),
|
||||||
|
vol.Required(CONF_PASSWORD): vol.Coerce(str),
|
||||||
|
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
|
||||||
|
vol.Range(min=1))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config, see):
|
||||||
@ -23,63 +34,67 @@ def setup_scanner(hass, config, see):
|
|||||||
from pyicloud import PyiCloudService
|
from pyicloud import PyiCloudService
|
||||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||||
|
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Get the username and password from the configuration.
|
username = config[CONF_USERNAME]
|
||||||
username = config.get(CONF_USERNAME)
|
password = config[CONF_PASSWORD]
|
||||||
password = config.get(CONF_PASSWORD)
|
|
||||||
|
|
||||||
if username is None or password is None:
|
|
||||||
_LOGGER.error('Must specify a username and password')
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info('Logging into iCloud Account')
|
_LOGGER.info('Logging into iCloud Account')
|
||||||
# Attempt the login to iCloud
|
# Attempt the login to iCloud
|
||||||
api = PyiCloudService(username,
|
api = PyiCloudService(username, password, verify=True)
|
||||||
password,
|
|
||||||
verify=True)
|
|
||||||
except PyiCloudFailedLoginException as error:
|
except PyiCloudFailedLoginException as error:
|
||||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def keep_alive(now):
|
def keep_alive(now):
|
||||||
"""Keep authenticating iCloud connection."""
|
"""Keep authenticating iCloud connection.
|
||||||
|
|
||||||
|
The session timeouts if we are not using it so we
|
||||||
|
have to re-authenticate & this will send an email.
|
||||||
|
"""
|
||||||
api.authenticate()
|
api.authenticate()
|
||||||
_LOGGER.info("Authenticate against iCloud")
|
_LOGGER.info("Authenticate against iCloud")
|
||||||
|
|
||||||
track_utc_time_change(hass, keep_alive, second=0)
|
seen_devices = {}
|
||||||
|
|
||||||
def update_icloud(now):
|
def update_icloud(now):
|
||||||
"""Authenticate against iCloud and scan for devices."""
|
"""Authenticate against iCloud and scan for devices."""
|
||||||
try:
|
try:
|
||||||
# The session timeouts if we are not using it so we
|
keep_alive(None)
|
||||||
# have to re-authenticate. This will send an email.
|
|
||||||
api.authenticate()
|
|
||||||
# Loop through every device registered with the iCloud account
|
# Loop through every device registered with the iCloud account
|
||||||
for device in api.devices:
|
for device in api.devices:
|
||||||
status = device.status()
|
status = device.status()
|
||||||
|
dev_id = slugify(status['name'].replace(' ', '', 99))
|
||||||
|
|
||||||
|
# An entity will not be created by see() when track=false in
|
||||||
|
# 'known_devices.yaml', but we need to see() it at least once
|
||||||
|
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
|
||||||
|
if entity is None and dev_id in seen_devices:
|
||||||
|
continue
|
||||||
|
seen_devices[dev_id] = True
|
||||||
|
|
||||||
location = device.location()
|
location = device.location()
|
||||||
# If the device has a location add it. If not do nothing
|
# If the device has a location add it. If not do nothing
|
||||||
if location:
|
if location:
|
||||||
see(
|
see(
|
||||||
dev_id=re.sub(r"(\s|\W|')",
|
dev_id=dev_id,
|
||||||
'',
|
|
||||||
status['name']),
|
|
||||||
host_name=status['name'],
|
host_name=status['name'],
|
||||||
gps=(location['latitude'], location['longitude']),
|
gps=(location['latitude'], location['longitude']),
|
||||||
battery=status['batteryLevel']*100,
|
battery=status['batteryLevel']*100,
|
||||||
gps_accuracy=location['horizontalAccuracy']
|
gps_accuracy=location['horizontalAccuracy']
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# No location found for the device so continue
|
|
||||||
continue
|
|
||||||
except PyiCloudNoDevicesException:
|
except PyiCloudNoDevicesException:
|
||||||
_LOGGER.info('No iCloud Devices found!')
|
_LOGGER.info('No iCloud Devices found!')
|
||||||
|
|
||||||
track_utc_time_change(
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
|
||||||
hass, update_icloud,
|
|
||||||
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
|
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
|
||||||
second=0
|
# Schedule keepalives between the updates
|
||||||
)
|
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
|
||||||
|
if x not in update_minutes)
|
||||||
|
|
||||||
|
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
|
||||||
|
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START
|
|||||||
from homeassistant.helpers.discovery import load_platform, discover
|
from homeassistant.helpers.discovery import load_platform, discover
|
||||||
|
|
||||||
DOMAIN = "discovery"
|
DOMAIN = "discovery"
|
||||||
REQUIREMENTS = ['netdisco==0.6.7']
|
REQUIREMENTS = ['netdisco==0.7.0']
|
||||||
|
|
||||||
SCAN_INTERVAL = 300 # seconds
|
SCAN_INTERVAL = 300 # seconds
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ SERVICE_HANDLERS = {
|
|||||||
'roku': ('media_player', 'roku'),
|
'roku': ('media_player', 'roku'),
|
||||||
'sonos': ('media_player', 'sonos'),
|
'sonos': ('media_player', 'sonos'),
|
||||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||||
|
'directv': ('media_player', 'directv'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@ ATTR_URL = "url"
|
|||||||
ATTR_SUBDIR = "subdir"
|
ATTR_SUBDIR = "subdir"
|
||||||
|
|
||||||
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
|
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_URL): vol.Url,
|
# pylint: disable=no-value-for-parameter
|
||||||
|
vol.Required(ATTR_URL): vol.Url(),
|
||||||
vol.Optional(ATTR_SUBDIR): cv.string,
|
vol.Optional(ATTR_SUBDIR): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,37 +1,130 @@
|
|||||||
"""Handle the frontend for Home Assistant."""
|
"""Handle the frontend for Home Assistant."""
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.components import api
|
from homeassistant.components import api
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from . import version, mdi_version
|
from .version import FINGERPRINTS
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api']
|
DEPENDENCIES = ['api']
|
||||||
|
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
||||||
|
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||||
|
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||||
|
PANELS = {}
|
||||||
|
|
||||||
|
# To keep track we don't register a component twice (gives a warning)
|
||||||
|
_REGISTERED_COMPONENTS = set()
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register_built_in_panel(hass, component_name, title=None, icon=None,
|
||||||
|
url_name=None, config=None):
|
||||||
|
"""Register a built-in panel."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
path = 'panels/ha-panel-{}.html'.format(component_name)
|
||||||
|
|
||||||
|
if hass.wsgi.development:
|
||||||
|
url = ('/static/home-assistant-polymer/panels/'
|
||||||
|
'{0}/ha-panel-{0}.html'.format(component_name))
|
||||||
|
else:
|
||||||
|
url = None # use default url generate mechanism
|
||||||
|
|
||||||
|
register_panel(hass, component_name, os.path.join(STATIC_PATH, path),
|
||||||
|
FINGERPRINTS[path], title, icon, url_name, url, config)
|
||||||
|
|
||||||
|
|
||||||
|
def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
|
||||||
|
url_name=None, url=None, config=None):
|
||||||
|
"""Register a panel for the frontend.
|
||||||
|
|
||||||
|
component_name: name of the web component
|
||||||
|
path: path to the HTML of the web component
|
||||||
|
md5: the md5 hash of the web component (for versioning, optional)
|
||||||
|
title: title to show in the sidebar (optional)
|
||||||
|
icon: icon to show next to title in sidebar (optional)
|
||||||
|
url_name: name to use in the url (defaults to component_name)
|
||||||
|
url: for the web component (for dev environment, optional)
|
||||||
|
config: config to be passed into the web component
|
||||||
|
|
||||||
|
Warning: this API will probably change. Use at own risk.
|
||||||
|
"""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
if url_name is None:
|
||||||
|
url_name = component_name
|
||||||
|
|
||||||
|
if url_name in PANELS:
|
||||||
|
_LOGGER.warning('Overwriting component %s', url_name)
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
_LOGGER.error('Panel %s component does not exist: %s',
|
||||||
|
component_name, path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if md5 is None:
|
||||||
|
with open(path) as fil:
|
||||||
|
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'url_name': url_name,
|
||||||
|
'component_name': component_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if title:
|
||||||
|
data['title'] = title
|
||||||
|
if icon:
|
||||||
|
data['icon'] = icon
|
||||||
|
if config is not None:
|
||||||
|
data['config'] = config
|
||||||
|
|
||||||
|
if url is not None:
|
||||||
|
data['url'] = url
|
||||||
|
else:
|
||||||
|
url = URL_PANEL_COMPONENT.format(component_name)
|
||||||
|
|
||||||
|
if url not in _REGISTERED_COMPONENTS:
|
||||||
|
hass.wsgi.register_static_path(url, path)
|
||||||
|
_REGISTERED_COMPONENTS.add(url)
|
||||||
|
|
||||||
|
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
||||||
|
data['url'] = fprinted_url
|
||||||
|
|
||||||
|
PANELS[url_name] = data
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup serving the frontend."""
|
"""Setup serving the frontend."""
|
||||||
hass.wsgi.register_view(IndexView)
|
|
||||||
hass.wsgi.register_view(BootstrapView)
|
hass.wsgi.register_view(BootstrapView)
|
||||||
|
|
||||||
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
|
|
||||||
if hass.wsgi.development:
|
if hass.wsgi.development:
|
||||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||||
else:
|
else:
|
||||||
sw_path = "service_worker.js"
|
sw_path = "service_worker.js"
|
||||||
|
|
||||||
hass.wsgi.register_static_path(
|
hass.wsgi.register_static_path("/service_worker.js",
|
||||||
"/service_worker.js",
|
os.path.join(STATIC_PATH, sw_path), 0)
|
||||||
os.path.join(www_static_path, sw_path),
|
hass.wsgi.register_static_path("/robots.txt",
|
||||||
0
|
os.path.join(STATIC_PATH, "robots.txt"))
|
||||||
)
|
hass.wsgi.register_static_path("/static", STATIC_PATH)
|
||||||
hass.wsgi.register_static_path(
|
|
||||||
"/robots.txt",
|
|
||||||
os.path.join(www_static_path, "robots.txt")
|
|
||||||
)
|
|
||||||
hass.wsgi.register_static_path("/static", www_static_path)
|
|
||||||
hass.wsgi.register_static_path("/local", hass.config.path('www'))
|
hass.wsgi.register_static_path("/local", hass.config.path('www'))
|
||||||
|
|
||||||
|
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||||
|
|
||||||
|
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||||
|
'dev-template'):
|
||||||
|
register_built_in_panel(hass, panel)
|
||||||
|
|
||||||
|
def register_frontend_index(event):
|
||||||
|
"""Register the frontend index urls.
|
||||||
|
|
||||||
|
Done when Home Assistant is started so that all panels are known.
|
||||||
|
"""
|
||||||
|
hass.wsgi.register_view(IndexView(
|
||||||
|
hass, ['/{}'.format(name) for name in PANELS]))
|
||||||
|
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -48,6 +141,7 @@ class BootstrapView(HomeAssistantView):
|
|||||||
'states': self.hass.states.all(),
|
'states': self.hass.states.all(),
|
||||||
'events': api.events_json(self.hass),
|
'events': api.events_json(self.hass),
|
||||||
'services': api.services_json(self.hass),
|
'services': api.services_json(self.hass),
|
||||||
|
'panels': PANELS,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -57,16 +151,15 @@ class IndexView(HomeAssistantView):
|
|||||||
url = '/'
|
url = '/'
|
||||||
name = "frontend:index"
|
name = "frontend:index"
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
|
extra_urls = ['/states', '/states/<entity:entity_id>']
|
||||||
'/devEvent', '/devInfo', '/devTemplate',
|
|
||||||
'/states', '/states/<entity:entity_id>']
|
|
||||||
|
|
||||||
def __init__(self, hass):
|
def __init__(self, hass, extra_urls):
|
||||||
"""Initialize the frontend view."""
|
"""Initialize the frontend view."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
|
|
||||||
from jinja2 import FileSystemLoader, Environment
|
from jinja2 import FileSystemLoader, Environment
|
||||||
|
|
||||||
|
self.extra_urls = self.extra_urls + extra_urls
|
||||||
self.templates = Environment(
|
self.templates = Environment(
|
||||||
loader=FileSystemLoader(
|
loader=FileSystemLoader(
|
||||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||||
@ -76,32 +169,32 @@ class IndexView(HomeAssistantView):
|
|||||||
def get(self, request, entity_id=None):
|
def get(self, request, entity_id=None):
|
||||||
"""Serve the index view."""
|
"""Serve the index view."""
|
||||||
if self.hass.wsgi.development:
|
if self.hass.wsgi.development:
|
||||||
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
|
core_url = '/static/home-assistant-polymer/build/core.js'
|
||||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||||
map_url = ('/static/home-assistant-polymer/src/layouts/'
|
|
||||||
'partial-map.html')
|
|
||||||
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
|
|
||||||
'dev-tools.html')
|
|
||||||
else:
|
else:
|
||||||
core_url = '/static/core-{}.js'.format(version.CORE)
|
core_url = '/static/core-{}.js'.format(
|
||||||
ui_url = '/static/frontend-{}.html'.format(version.UI)
|
FINGERPRINTS['core.js'])
|
||||||
map_url = '/static/partial-map-{}.html'.format(version.MAP)
|
ui_url = '/static/frontend-{}.html'.format(
|
||||||
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
|
FINGERPRINTS['frontend.html'])
|
||||||
|
|
||||||
|
if request.path == '/':
|
||||||
|
panel = 'states'
|
||||||
|
else:
|
||||||
|
panel = request.path.split('/')[1]
|
||||||
|
|
||||||
|
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
|
||||||
|
|
||||||
# auto login if no password was set
|
# auto login if no password was set
|
||||||
if self.hass.config.api.api_password is None:
|
no_auth = 'false' if self.hass.config.api.api_password else 'true'
|
||||||
auth = 'true'
|
|
||||||
else:
|
|
||||||
auth = 'false'
|
|
||||||
|
|
||||||
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
|
|
||||||
|
|
||||||
|
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||||
template = self.templates.get_template('index.html')
|
template = self.templates.get_template('index.html')
|
||||||
|
|
||||||
# pylint is wrong
|
# pylint is wrong
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
resp = template.render(
|
resp = template.render(
|
||||||
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
|
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||||
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
|
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||||
|
panel_url=panel_url)
|
||||||
|
|
||||||
return self.Response(resp, mimetype='text/html')
|
return self.Response(resp, mimetype='text/html')
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
|
||||||
VERSION = "758957b7ea989d6beca60e218ea7f7dd"
|
|
@ -5,19 +5,26 @@
|
|||||||
<title>Home Assistant</title>
|
<title>Home Assistant</title>
|
||||||
|
|
||||||
<link rel='manifest' href='/static/manifest.json'>
|
<link rel='manifest' href='/static/manifest.json'>
|
||||||
<link rel='icon' href='/static/favicon.ico'>
|
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||||
<link rel='apple-touch-icon' sizes='180x180'
|
<link rel='apple-touch-icon' sizes='180x180'
|
||||||
href='/static/favicon-apple-180x180.png'>
|
href='/static/icons/favicon-apple-180x180.png'>
|
||||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||||
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
|
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||||
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
|
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
|
||||||
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
|
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
|
||||||
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
|
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
|
||||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||||
<meta name='mobile-web-app-capable' content='yes'>
|
<meta name='mobile-web-app-capable' content='yes'>
|
||||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||||
<meta name='theme-color' content='#03a9f4'>
|
<meta name='theme-color' content='#03a9f4'>
|
||||||
<style>
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', 'Noto', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
#ha-init-skeleton {
|
#ha-init-skeleton {
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -65,23 +72,23 @@
|
|||||||
.getElementById('ha-init-skeleton')
|
.getElementById('ha-init-skeleton')
|
||||||
.classList.add('error');
|
.classList.add('error');
|
||||||
};
|
};
|
||||||
window.noAuth = {{ auth }};
|
window.noAuth = {{ no_auth }};
|
||||||
window.deferredLoading = {
|
window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
|
||||||
map: '{{ map_url }}',
|
|
||||||
dev: '{{ dev_url }}',
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body fullbleed>
|
<body>
|
||||||
<div id='ha-init-skeleton'>
|
<div id='ha-init-skeleton'>
|
||||||
<img src='/static/favicon-192x192.png' height='192'>
|
<img src='/static/icons/favicon-192x192.png' height='192'>
|
||||||
<paper-spinner active></paper-spinner>
|
<paper-spinner active></paper-spinner>
|
||||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||||
</div>
|
</div>
|
||||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
||||||
<script src='{{ core_url }}'></script>
|
<script src='{{ core_url }}'></script>
|
||||||
<link rel='import' href='{{ ui_url }}' onerror='initError()' async>
|
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||||
|
{% if panel_url %}
|
||||||
|
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||||
|
{% endif %}
|
||||||
<link rel='import' href='{{ icons_url }}' async>
|
<link rel='import' href='{{ icons_url }}' async>
|
||||||
<script>
|
<script>
|
||||||
var webComponentsSupported = (
|
var webComponentsSupported = (
|
||||||
@ -89,11 +96,11 @@
|
|||||||
'import' in document.createElement('link') &&
|
'import' in document.createElement('link') &&
|
||||||
'content' in document.createElement('template'));
|
'content' in document.createElement('template'));
|
||||||
if (!webComponentsSupported) {
|
if (!webComponentsSupported) {
|
||||||
var script = document.createElement('script')
|
var e = document.createElement('script');
|
||||||
script.async = true
|
e.async = true;
|
||||||
script.onerror = initError;
|
e.onerror = initError;
|
||||||
script.src = '/static/webcomponents-lite.min.js'
|
e.src = '/static/webcomponents-lite.min.js';
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(e);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||||
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
|
|
||||||
UI = "805f8dda70419b26daabc8e8f625127f"
|
FINGERPRINTS = {
|
||||||
MAP = "c922306de24140afd14f857f927bf8f0"
|
"core.js": "bc78f21f5280217aa2c78dfc5848134f",
|
||||||
DEV = "b7079ac3121b95b9856e5603a6d8a263"
|
"frontend.html": "6c52e8cb797bafa3124d936af5ce1fcc",
|
||||||
|
"mdi.html": "f6c6cc64c2ec38a80e91f801b41119b3",
|
||||||
|
"panels/ha-panel-dev-event.html": "20327fbd4fb0370aec9be4db26fd723f",
|
||||||
|
"panels/ha-panel-dev-info.html": "28e0a19ceb95aa714fd53228d9983a49",
|
||||||
|
"panels/ha-panel-dev-service.html": "85fd5b48600418bb5a6187539a623c38",
|
||||||
|
"panels/ha-panel-dev-state.html": "25d84d7b7aea779bb3bb3cd6c155f8d9",
|
||||||
|
"panels/ha-panel-dev-template.html": "d079abf61cff9690f828cafb0d29b7e7",
|
||||||
|
"panels/ha-panel-history.html": "7e051b5babf5653b689e0107ea608acb",
|
||||||
|
"panels/ha-panel-iframe.html": "7bdb564a8f37971d7b89b718935810a1",
|
||||||
|
"panels/ha-panel-logbook.html": "9b285357b0b2d82ee282e634f4e1cab2",
|
||||||
|
"panels/ha-panel-map.html": "dfe141a3fa5fd403be554def1dd039a9"
|
||||||
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 718384f22aa0a689190a4d3f41b5e9ed091c80a3
|
Subproject commit 697f9397de357cec9662626575fc01d6f921ef22
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
@ -7,22 +7,22 @@
|
|||||||
"background_color": "#FFFFFF",
|
"background_color": "#FFFFFF",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static/favicon-192x192.png",
|
"src": "/static/icons/favicon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/favicon-384x384.png",
|
"src": "/static/icons/favicon-384x384.png",
|
||||||
"sizes": "384x384",
|
"sizes": "384x384",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/favicon-512x512.png",
|
"src": "/static/icons/favicon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/favicon-1024x1024.png",
|
"src": "/static/icons/favicon-1024x1024.png",
|
||||||
"sizes": "1024x1024",
|
"sizes": "1024x1024",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style is="custom-style" include="iron-positioning"></style><style>.content{margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||||
|
clear: both;white-space:pre-wrap}</style><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">About</span><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a> — <a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
@ -0,0 +1 @@
|
|||||||
|
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-iframe"><template><style>iframe{border:0;width:100%;height:100%}</style><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">[[panel.title]]</span><iframe src="[[panel.config.url]]" sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"></iframe></partial-base></template></dom-module><script>Polymer({is:"ha-panel-iframe",properties:{panel:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean}}})</script></body></html>
|
@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice
|
|||||||
from homeassistant.components.wink import WinkDevice
|
from homeassistant.components.wink import WinkDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -44,7 +44,6 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
|||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||||
self._node = value.node
|
|
||||||
self._state = value.data
|
self._state = value.data
|
||||||
dispatcher.connect(
|
dispatcher.connect(
|
||||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||||
@ -53,7 +52,7 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
|||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id:
|
if self._value.value_id == value.value_id:
|
||||||
self._state = value.data
|
self._state = value.data
|
||||||
self.update_ha_state(True)
|
self.update_ha_state()
|
||||||
_LOGGER.debug("Value changed on network %s", value)
|
_LOGGER.debug("Value changed on network %s", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -12,8 +12,8 @@ import voluptuous as vol
|
|||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_UNKNOWN,
|
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||||
ATTR_ASSUMED_STATE, )
|
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
|
||||||
from homeassistant.helpers.entity import (
|
from homeassistant.helpers.entity import (
|
||||||
Entity, generate_entity_id, split_entity_id)
|
Entity, generate_entity_id, split_entity_id)
|
||||||
from homeassistant.helpers.event import track_state_change
|
from homeassistant.helpers.event import track_state_change
|
||||||
@ -64,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
# List of ON/OFF state tuples for groupable states
|
# List of ON/OFF state tuples for groupable states
|
||||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||||
(STATE_OPEN, STATE_CLOSED)]
|
(STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)]
|
||||||
|
|
||||||
|
|
||||||
def _get_group_on_off(state):
|
def _get_group_on_off(state):
|
||||||
@ -304,8 +304,9 @@ class Group(Entity):
|
|||||||
if gr_on is None:
|
if gr_on is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if tr_state is None or (gr_state == gr_on and
|
if tr_state is None or ((gr_state == gr_on and
|
||||||
tr_state.state == gr_off):
|
tr_state.state == gr_off) or
|
||||||
|
tr_state.state not in (gr_on, gr_off)):
|
||||||
if states is None:
|
if states is None:
|
||||||
states = self._tracking_states
|
states = self._tracking_states
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ Provide pre-made queries on top of the recorder component.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/history/
|
https://home-assistant.io/components/history/
|
||||||
"""
|
"""
|
||||||
import re
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components import recorder, script
|
from homeassistant.components import recorder, script
|
||||||
|
from homeassistant.components.frontend import register_built_in_panel
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'history'
|
DOMAIN = 'history'
|
||||||
@ -19,9 +19,6 @@ DEPENDENCIES = ['recorder', 'http']
|
|||||||
SIGNIFICANT_DOMAINS = ('thermostat',)
|
SIGNIFICANT_DOMAINS = ('thermostat',)
|
||||||
IGNORE_DOMAINS = ('zone', 'scene',)
|
IGNORE_DOMAINS = ('zone', 'scene',)
|
||||||
|
|
||||||
URL_HISTORY_PERIOD = re.compile(
|
|
||||||
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
|
||||||
|
|
||||||
|
|
||||||
def last_5_states(entity_id):
|
def last_5_states(entity_id):
|
||||||
"""Return the last 5 states for entity_id."""
|
"""Return the last 5 states for entity_id."""
|
||||||
@ -153,6 +150,7 @@ def setup(hass, config):
|
|||||||
"""Setup the history hooks."""
|
"""Setup the history hooks."""
|
||||||
hass.wsgi.register_view(Last5StatesView)
|
hass.wsgi.register_view(Last5StatesView)
|
||||||
hass.wsgi.register_view(HistoryPeriodView)
|
hass.wsgi.register_view(HistoryPeriodView)
|
||||||
|
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -173,14 +171,14 @@ class HistoryPeriodView(HomeAssistantView):
|
|||||||
|
|
||||||
url = '/api/history/period'
|
url = '/api/history/period'
|
||||||
name = 'api:history:view-period'
|
name = 'api:history:view-period'
|
||||||
extra_urls = ['/api/history/period/<date:date>']
|
extra_urls = ['/api/history/period/<datetime:datetime>']
|
||||||
|
|
||||||
def get(self, request, date=None):
|
def get(self, request, datetime=None):
|
||||||
"""Return history over a period of time."""
|
"""Return history over a period of time."""
|
||||||
one_day = timedelta(days=1)
|
one_day = timedelta(days=1)
|
||||||
|
|
||||||
if date:
|
if datetime:
|
||||||
start_time = dt_util.as_utc(dt_util.start_of_local_day(date))
|
start_time = dt_util.as_utc(datetime)
|
||||||
else:
|
else:
|
||||||
start_time = dt_util.utcnow() - one_day
|
start_time = dt_util.utcnow() - one_day
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
|||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
REQUIREMENTS = ["pyhomematic==0.1.9"]
|
REQUIREMENTS = ["pyhomematic==0.1.10"]
|
||||||
|
|
||||||
HOMEMATIC = None
|
HOMEMATIC = None
|
||||||
HOMEMATIC_LINK_DELAY = 0.5
|
HOMEMATIC_LINK_DELAY = 0.5
|
||||||
|
@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = "http"
|
DOMAIN = "http"
|
||||||
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")
|
REQUIREMENTS = ("cherrypy==6.1.1", "static3==0.7.0", "Werkzeug==0.11.10")
|
||||||
|
|
||||||
CONF_API_PASSWORD = "api_password"
|
CONF_API_PASSWORD = "api_password"
|
||||||
CONF_SERVER_HOST = "server_host"
|
CONF_SERVER_HOST = "server_host"
|
||||||
@ -216,9 +216,29 @@ def routing_map(hass):
|
|||||||
"""Convert date to url value."""
|
"""Convert date to url value."""
|
||||||
return value.isoformat()
|
return value.isoformat()
|
||||||
|
|
||||||
|
class DateTimeValidator(BaseConverter):
|
||||||
|
"""Validate datetimes in urls formatted per ISO 8601."""
|
||||||
|
|
||||||
|
regex = r'\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d' \
|
||||||
|
r'\.\d+([+-][0-2]\d:[0-5]\d|Z)'
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
"""Validate and convert date."""
|
||||||
|
parsed = dt_util.parse_datetime(value)
|
||||||
|
|
||||||
|
if parsed is None:
|
||||||
|
raise ValidationError()
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
def to_url(self, value):
|
||||||
|
"""Convert date to url value."""
|
||||||
|
return value.isoformat()
|
||||||
|
|
||||||
return Map(converters={
|
return Map(converters={
|
||||||
'entity': EntityValidator,
|
'entity': EntityValidator,
|
||||||
'date': DateValidator,
|
'date': DateValidator,
|
||||||
|
'datetime': DateTimeValidator,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,9 +98,10 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
|
|
||||||
def value_changed(self, value):
|
def value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id:
|
if self._value.value_id == value.value_id or \
|
||||||
|
self._value.node == value.node:
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
self.update_ha_state(True)
|
self.update_ha_state()
|
||||||
_LOGGER.debug("Value changed on network %s", value)
|
_LOGGER.debug("Value changed on network %s", value)
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
@ -135,7 +136,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||||
if value.command_class == 112 and value.index == 33:
|
if value.command_class == 112 and value.index == 33:
|
||||||
self._current_swing_mode = value.data
|
self._current_swing_mode = value.data
|
||||||
self._swing_list = [0, 1]
|
self._swing_list = list(value.data_items)
|
||||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -235,5 +236,5 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||||
if value.command_class == 112 and value.index == 33:
|
if value.command_class == 112 and value.index == 33:
|
||||||
value.data = int(swing_mode)
|
value.data = bytes(swing_mode, 'utf-8')
|
||||||
break
|
break
|
||||||
|
@ -33,6 +33,7 @@ CONF_PASSWORD = 'password'
|
|||||||
CONF_SSL = 'ssl'
|
CONF_SSL = 'ssl'
|
||||||
CONF_VERIFY_SSL = 'verify_ssl'
|
CONF_VERIFY_SSL = 'verify_ssl'
|
||||||
CONF_BLACKLIST = 'blacklist'
|
CONF_BLACKLIST = 'blacklist'
|
||||||
|
CONF_TAGS = 'tags'
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
@ -56,6 +57,7 @@ def setup(hass, config):
|
|||||||
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
||||||
DEFAULT_VERIFY_SSL)
|
DEFAULT_VERIFY_SSL)
|
||||||
blacklist = conf.get(CONF_BLACKLIST, [])
|
blacklist = conf.get(CONF_BLACKLIST, [])
|
||||||
|
tags = conf.get(CONF_TAGS, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
influx = InfluxDBClient(host=host, port=port, username=username,
|
influx = InfluxDBClient(host=host, port=port, username=username,
|
||||||
@ -99,6 +101,9 @@ def setup(hass, config):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
json_body[0]['tags'][tag] = tags[tag]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
influx.write_points(json_body)
|
influx.write_points(json_body)
|
||||||
except exceptions.InfluxDBClientError:
|
except exceptions.InfluxDBClientError:
|
||||||
|
@ -34,7 +34,7 @@ SERVICE_SELECT_VALUE = 'select_value'
|
|||||||
|
|
||||||
SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({
|
SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Required(ATTR_VALUE): vol.Coerce(int),
|
vol.Required(ATTR_VALUE): vol.Coerce(float),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ class InputSlider(Entity):
|
|||||||
|
|
||||||
def select_value(self, value):
|
def select_value(self, value):
|
||||||
"""Select new value."""
|
"""Select new value."""
|
||||||
num_value = int(value)
|
num_value = float(value)
|
||||||
if num_value < self._minimum or num_value > self._maximum:
|
if num_value < self._minimum or num_value > self._maximum:
|
||||||
_LOGGER.warning('Invalid value: %s (range %s - %s)',
|
_LOGGER.warning('Invalid value: %s (range %s - %s)',
|
||||||
num_value, self._minimum, self._maximum)
|
num_value, self._minimum, self._maximum)
|
||||||
|
@ -19,26 +19,19 @@ DOMAIN = 'joaoapps_join'
|
|||||||
CONF_DEVICE_ID = 'device_id'
|
CONF_DEVICE_ID = 'device_id'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.All(cv.ensure_list, [{
|
||||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_API_KEY): cv.string
|
vol.Optional(CONF_API_KEY): cv.string
|
||||||
})
|
}])
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
def setup(hass, config):
|
def register_device(hass, device_id, api_key, name):
|
||||||
"""Setup Join services."""
|
"""Method to register services for each join device listed."""
|
||||||
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms,
|
from pyjoin import (ring_device, set_wallpaper, send_sms,
|
||||||
send_file, send_url, send_notification)
|
send_file, send_url, send_notification)
|
||||||
device_id = config[DOMAIN].get(CONF_DEVICE_ID)
|
|
||||||
api_key = config[DOMAIN].get(CONF_API_KEY)
|
|
||||||
name = config[DOMAIN].get(CONF_NAME)
|
|
||||||
if api_key:
|
|
||||||
if not get_devices(api_key):
|
|
||||||
_LOGGER.error("Error connecting to Join, check API key")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ring_service(service):
|
def ring_service(service):
|
||||||
"""Service to ring devices."""
|
"""Service to ring devices."""
|
||||||
@ -69,7 +62,6 @@ def setup(hass, config):
|
|||||||
sms_text=service.data.get('message'),
|
sms_text=service.data.get('message'),
|
||||||
api_key=api_key)
|
api_key=api_key)
|
||||||
|
|
||||||
name = name.lower().replace(" ", "_") + "_" if name else ""
|
|
||||||
hass.services.register(DOMAIN, name + 'ring', ring_service)
|
hass.services.register(DOMAIN, name + 'ring', ring_service)
|
||||||
hass.services.register(DOMAIN, name + 'set_wallpaper',
|
hass.services.register(DOMAIN, name + 'set_wallpaper',
|
||||||
set_wallpaper_service)
|
set_wallpaper_service)
|
||||||
@ -77,4 +69,19 @@ def setup(hass, config):
|
|||||||
hass.services.register(DOMAIN, name + 'send_file', send_file_service)
|
hass.services.register(DOMAIN, name + 'send_file', send_file_service)
|
||||||
hass.services.register(DOMAIN, name + 'send_url', send_url_service)
|
hass.services.register(DOMAIN, name + 'send_url', send_url_service)
|
||||||
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
|
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Setup Join services."""
|
||||||
|
from pyjoin import get_devices
|
||||||
|
for device in config[DOMAIN]:
|
||||||
|
device_id = device.get(CONF_DEVICE_ID)
|
||||||
|
api_key = device.get(CONF_API_KEY)
|
||||||
|
name = device.get(CONF_NAME)
|
||||||
|
name = name.lower().replace(" ", "_") + "_" if name else ""
|
||||||
|
if api_key:
|
||||||
|
if not get_devices(api_key):
|
||||||
|
_LOGGER.error("Error connecting to Join, check API key")
|
||||||
|
return False
|
||||||
|
register_device(hass, device_id, api_key, name)
|
||||||
return True
|
return True
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
DOMAIN = "knx"
|
DOMAIN = "knx"
|
||||||
REQUIREMENTS = ['knxip==0.3.0']
|
REQUIREMENTS = ['knxip==0.3.2']
|
||||||
|
|
||||||
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
|
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
|
||||||
|
|
||||||
@ -45,7 +45,12 @@ def setup(hass, config):
|
|||||||
|
|
||||||
KNXTUNNEL = KNXIPTunnel(host, port)
|
KNXTUNNEL = KNXIPTunnel(host, port)
|
||||||
try:
|
try:
|
||||||
KNXTUNNEL.connect()
|
res = KNXTUNNEL.connect()
|
||||||
|
_LOGGER.debug("Res = %s", res)
|
||||||
|
if not res:
|
||||||
|
_LOGGER.exception("Could not connect to KNX/IP interface %s", host)
|
||||||
|
return False
|
||||||
|
|
||||||
except KNXException as ex:
|
except KNXException as ex:
|
||||||
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
|
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
|
||||||
KNXTUNNEL = None
|
KNXTUNNEL = None
|
||||||
@ -74,7 +79,10 @@ class KNXConfig(object):
|
|||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.should_poll = config.get("poll", True)
|
self.should_poll = config.get("poll", True)
|
||||||
|
if config.get("address"):
|
||||||
self._address = parse_group_address(config.get("address"))
|
self._address = parse_group_address(config.get("address"))
|
||||||
|
else:
|
||||||
|
self._address = None
|
||||||
if self.config.get("state_address"):
|
if self.config.get("state_address"):
|
||||||
self._state_address = parse_group_address(
|
self._state_address = parse_group_address(
|
||||||
self.config.get("state_address"))
|
self.config.get("state_address"))
|
||||||
@ -198,7 +206,7 @@ class KNXGroupAddress(Entity):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class KNXMultiAddressDevice(KNXGroupAddress):
|
class KNXMultiAddressDevice(Entity):
|
||||||
"""Representation of devices connected to a multiple KNX group address.
|
"""Representation of devices connected to a multiple KNX group address.
|
||||||
|
|
||||||
This is needed for devices like dimmers or shutter actuators as they have
|
This is needed for devices like dimmers or shutter actuators as they have
|
||||||
@ -218,18 +226,21 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
|||||||
"""
|
"""
|
||||||
from knxip.core import parse_group_address, KNXException
|
from knxip.core import parse_group_address, KNXException
|
||||||
|
|
||||||
super().__init__(self, hass, config)
|
self._config = config
|
||||||
|
self._state = False
|
||||||
self.config = config
|
self._data = None
|
||||||
|
_LOGGER.debug("Initalizing KNX multi address device")
|
||||||
|
|
||||||
# parse required addresses
|
# parse required addresses
|
||||||
for name in required:
|
for name in required:
|
||||||
|
_LOGGER.info(name)
|
||||||
paramname = name + "_address"
|
paramname = name + "_address"
|
||||||
addr = self._config.config.get(paramname)
|
addr = self._config.config.get(paramname)
|
||||||
if addr is None:
|
if addr is None:
|
||||||
_LOGGER.exception("Required KNX group address %s missing",
|
_LOGGER.exception("Required KNX group address %s missing",
|
||||||
paramname)
|
paramname)
|
||||||
raise KNXException("Group address missing in configuration")
|
raise KNXException("Group address for %s missing "
|
||||||
|
"in configuration", paramname)
|
||||||
addr = parse_group_address(addr)
|
addr = parse_group_address(addr)
|
||||||
self.names[addr] = name
|
self.names[addr] = name
|
||||||
|
|
||||||
@ -244,23 +255,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
|||||||
_LOGGER.exception("Cannot parse group address %s", addr)
|
_LOGGER.exception("Cannot parse group address %s", addr)
|
||||||
self.names[addr] = name
|
self.names[addr] = name
|
||||||
|
|
||||||
def handle_frame(frame):
|
@property
|
||||||
"""Handle an incoming KNX frame.
|
def name(self):
|
||||||
|
"""The entity's display name."""
|
||||||
|
return self._config.name
|
||||||
|
|
||||||
Handle an incoming frame and update our status if it contains
|
@property
|
||||||
information relating to this device.
|
def config(self):
|
||||||
"""
|
"""The entity's configuration."""
|
||||||
addr = frame.data[0]
|
return self._config
|
||||||
|
|
||||||
if addr in self.names:
|
@property
|
||||||
self.values[addr] = frame.data[1]
|
def should_poll(self):
|
||||||
self.update_ha_state()
|
"""Return the state of the polling, if needed."""
|
||||||
|
return self._config.should_poll
|
||||||
|
|
||||||
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame)
|
@property
|
||||||
|
def cache(self):
|
||||||
def group_write_address(self, name, value):
|
"""The name given to the entity."""
|
||||||
"""Write to the group address with the given name."""
|
return self._config.config.get("cache", True)
|
||||||
KNXTUNNEL.group_write(self.address, [value])
|
|
||||||
|
|
||||||
def has_attribute(self, name):
|
def has_attribute(self, name):
|
||||||
"""Check if the attribute with the given name is defined.
|
"""Check if the attribute with the given name is defined.
|
||||||
@ -277,7 +290,7 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
|||||||
from knxip.core import KNXException
|
from knxip.core import KNXException
|
||||||
|
|
||||||
addr = None
|
addr = None
|
||||||
for attributename, attributeaddress in self.names.items():
|
for attributeaddress, attributename in self.names.items():
|
||||||
if attributename == name:
|
if attributename == name:
|
||||||
addr = attributeaddress
|
addr = attributeaddress
|
||||||
|
|
||||||
@ -293,3 +306,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def set_value(self, name, value):
|
||||||
|
"""Set the value of a given named attribute."""
|
||||||
|
from knxip.core import KNXException
|
||||||
|
|
||||||
|
addr = None
|
||||||
|
for attributeaddress, attributename in self.names.items():
|
||||||
|
if attributename == name:
|
||||||
|
addr = attributeaddress
|
||||||
|
|
||||||
|
if addr is None:
|
||||||
|
_LOGGER.exception("Attribute %s undefined", name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
KNXTUNNEL.group_write(addr, value)
|
||||||
|
except KNXException:
|
||||||
|
_LOGGER.exception("Unable to write to KNX address: %s",
|
||||||
|
addr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
109
homeassistant/components/light/flux_led.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
Support for Flux lights.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.flux_led/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.light import Light
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip'
|
||||||
|
'#flux_led==0.3']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
DOMAIN = "flux_led"
|
||||||
|
ATTR_NAME = 'name'
|
||||||
|
|
||||||
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(ATTR_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
|
vol.Required('platform'): DOMAIN,
|
||||||
|
vol.Optional('devices', default={}): {cv.string: DEVICE_SCHEMA},
|
||||||
|
vol.Optional('automatic_add', default=False): cv.boolean,
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
"""Setup the Flux lights."""
|
||||||
|
import flux_led
|
||||||
|
lights = []
|
||||||
|
light_ips = []
|
||||||
|
for ipaddr, device_config in config["devices"].items():
|
||||||
|
device = {}
|
||||||
|
device['id'] = device_config[ATTR_NAME]
|
||||||
|
device['ipaddr'] = ipaddr
|
||||||
|
light = FluxLight(device)
|
||||||
|
if light.is_valid:
|
||||||
|
lights.append(light)
|
||||||
|
light_ips.append(ipaddr)
|
||||||
|
|
||||||
|
if not config['automatic_add']:
|
||||||
|
add_devices_callback(lights)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the bulbs on the LAN
|
||||||
|
scanner = flux_led.BulbScanner()
|
||||||
|
scanner.scan(timeout=20)
|
||||||
|
for device in scanner.getBulbInfo():
|
||||||
|
light = FluxLight(device)
|
||||||
|
ipaddr = device['ipaddr']
|
||||||
|
if light.is_valid and ipaddr not in light_ips:
|
||||||
|
lights.append(light)
|
||||||
|
light_ips.append(ipaddr)
|
||||||
|
|
||||||
|
add_devices_callback(lights)
|
||||||
|
|
||||||
|
|
||||||
|
class FluxLight(Light):
|
||||||
|
"""Representation of a Flux light."""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the light."""
|
||||||
|
import flux_led
|
||||||
|
|
||||||
|
self._name = device['id']
|
||||||
|
self._ipaddr = device['ipaddr']
|
||||||
|
self.is_valid = True
|
||||||
|
self._bulb = None
|
||||||
|
try:
|
||||||
|
self._bulb = flux_led.WifiLedBulb(self._ipaddr)
|
||||||
|
except socket.error:
|
||||||
|
self.is_valid = False
|
||||||
|
_LOGGER.error("Failed to connect to bulb %s, %s",
|
||||||
|
self._ipaddr, self._name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return the ID of this light."""
|
||||||
|
return "{}.{}".format(
|
||||||
|
self.__class__, self._ipaddr)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device if any."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if device is on."""
|
||||||
|
return self._bulb.isOn()
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the specified or all lights on."""
|
||||||
|
self._bulb.turnOn()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the specified or all lights off."""
|
||||||
|
self._bulb.turnOff()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Synchronize state with bulb."""
|
||||||
|
self._bulb.refreshState()
|
@ -19,24 +19,24 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
"""Setup a Hyperion server remote."""
|
"""Setup a Hyperion server remote."""
|
||||||
host = config.get(CONF_HOST, None)
|
host = config.get(CONF_HOST, None)
|
||||||
port = config.get("port", 19444)
|
port = config.get("port", 19444)
|
||||||
device = Hyperion(config.get('name', host), host, port)
|
default_color = config.get("default_color", [255, 255, 255])
|
||||||
|
device = Hyperion(config.get('name', host), host, port, default_color)
|
||||||
if device.setup():
|
if device.setup():
|
||||||
add_devices_callback([device])
|
add_devices_callback([device])
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Hyperion(Light):
|
class Hyperion(Light):
|
||||||
"""Representation of a Hyperion remote."""
|
"""Representation of a Hyperion remote."""
|
||||||
|
|
||||||
def __init__(self, name, host, port):
|
def __init__(self, name, host, port, default_color):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
self._name = name
|
self._name = name
|
||||||
self._is_available = True
|
self._default_color = default_color
|
||||||
self._rgb_color = [255, 255, 255]
|
self._rgb_color = [0, 0, 0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -50,38 +50,43 @@ class Hyperion(Light):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the device is online."""
|
"""Return true if not black."""
|
||||||
return self._is_available
|
return self._rgb_color != [0, 0, 0]
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the lights on."""
|
"""Turn the lights on."""
|
||||||
if self._is_available:
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||||
|
else:
|
||||||
|
self._rgb_color = self._default_color
|
||||||
|
|
||||||
self.json_request({"command": "color", "priority": 128,
|
self.json_request({"command": "color", "priority": 128,
|
||||||
"color": self._rgb_color})
|
"color": self._rgb_color})
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Disconnect the remote."""
|
"""Disconnect all remotes."""
|
||||||
self.json_request({"command": "clearall"})
|
self.json_request({"command": "clearall"})
|
||||||
|
self._rgb_color = [0, 0, 0]
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Ping the remote."""
|
"""Get the remote's active color."""
|
||||||
# just see if the remote port is open
|
response = self.json_request({"command": "serverinfo"})
|
||||||
self._is_available = self.json_request()
|
if response:
|
||||||
|
if response["info"]["activeLedColor"] == []:
|
||||||
|
self._rgb_color = [0, 0, 0]
|
||||||
|
else:
|
||||||
|
self._rgb_color =\
|
||||||
|
response["info"]["activeLedColor"][0]["RGB Value"]
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""Get the hostname of the remote."""
|
"""Get the hostname of the remote."""
|
||||||
response = self.json_request({"command": "serverinfo"})
|
response = self.json_request({"command": "serverinfo"})
|
||||||
if response:
|
if response:
|
||||||
if self._name == self._host:
|
|
||||||
self._name = response["info"]["hostname"]
|
self._name = response["info"]["hostname"]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def json_request(self, request=None, wait_for_response=False):
|
def json_request(self, request, wait_for_response=False):
|
||||||
"""Communicate with the JSON server."""
|
"""Communicate with the JSON server."""
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(5)
|
sock.settimeout(5)
|
||||||
@ -92,11 +97,6 @@ class Hyperion(Light):
|
|||||||
sock.close()
|
sock.close()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not request:
|
|
||||||
# No communication needed, simple presence detection returns True
|
|
||||||
sock.close()
|
|
||||||
return True
|
|
||||||
|
|
||||||
sock.send(bytearray(json.dumps(request) + "\n", "utf-8"))
|
sock.send(bytearray(json.dumps(request) + "\n", "utf-8"))
|
||||||
try:
|
try:
|
||||||
buf = sock.recv(4096)
|
buf = sock.recv(4096)
|
||||||
|
@ -6,30 +6,16 @@ https://home-assistant.io/components/light.qwikswitch/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.components.qwikswitch as qwikswitch
|
import homeassistant.components.qwikswitch as qwikswitch
|
||||||
from homeassistant.components.light import Light
|
|
||||||
|
|
||||||
DEPENDENCIES = ['qwikswitch']
|
DEPENDENCIES = ['qwikswitch']
|
||||||
|
|
||||||
|
|
||||||
class QSLight(qwikswitch.QSToggleEntity, Light):
|
|
||||||
"""Light based on a Qwikswitch relay/dimmer module."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Store add_devices for the light components."""
|
"""Add lights from the main Qwikswitch component."""
|
||||||
if discovery_info is None or 'qsusb_id' not in discovery_info:
|
if discovery_info is None:
|
||||||
logging.getLogger(__name__).error(
|
logging.getLogger(__name__).error('Configure Qwikswitch Component.')
|
||||||
'Configure main Qwikswitch component')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
|
add_devices(qwikswitch.QSUSB['light'])
|
||||||
|
return True
|
||||||
for item in qsusb.ha_devices:
|
|
||||||
if item['type'] not in ['dim', 'rel']:
|
|
||||||
continue
|
|
||||||
dev = QSLight(item, qsusb)
|
|
||||||
add_devices([dev])
|
|
||||||
qsusb.ha_objects[item['id']] = dev
|
|
||||||
|
@ -6,10 +6,8 @@ https://home-assistant.io/components/light.vera/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.util.dt as dt_util
|
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED,
|
|
||||||
STATE_OFF, STATE_ON)
|
STATE_OFF, STATE_ON)
|
||||||
from homeassistant.components.vera import (
|
from homeassistant.components.vera import (
|
||||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||||
@ -56,31 +54,6 @@ class VeraLight(VeraDevice, Light):
|
|||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the state attributes."""
|
|
||||||
attr = {}
|
|
||||||
|
|
||||||
if self.vera_device.has_battery:
|
|
||||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
|
||||||
|
|
||||||
if self.vera_device.is_armable:
|
|
||||||
armed = self.vera_device.is_armed
|
|
||||||
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
|
||||||
|
|
||||||
if self.vera_device.is_trippable:
|
|
||||||
last_tripped = self.vera_device.last_trip
|
|
||||||
if last_tripped is not None:
|
|
||||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
|
||||||
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
|
|
||||||
else:
|
|
||||||
attr[ATTR_LAST_TRIP_TIME] = None
|
|
||||||
tripped = self.vera_device.is_tripped
|
|
||||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
|
||||||
|
|
||||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
|
||||||
return attr
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.util import color as color_util
|
|||||||
from homeassistant.util.color import \
|
from homeassistant.util.color import \
|
||||||
color_temperature_mired_to_kelvin as mired_to_kelvin
|
color_temperature_mired_to_kelvin as mired_to_kelvin
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
80
homeassistant/components/light/x10.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
Support for X10 lights.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.x10/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from subprocess import check_output, CalledProcessError, STDOUT
|
||||||
|
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def x10_command(command):
|
||||||
|
"""Execute X10 command and check output."""
|
||||||
|
return check_output(["heyu"] + command.split(' '), stderr=STDOUT)
|
||||||
|
|
||||||
|
|
||||||
|
def get_status():
|
||||||
|
"""Get on/off status for all x10 units in default housecode."""
|
||||||
|
output = check_output("heyu info | grep monitored", shell=True)
|
||||||
|
return output.decode('utf-8').split(' ')[-1].strip('\n()')
|
||||||
|
|
||||||
|
|
||||||
|
def get_unit_status(code):
|
||||||
|
"""Get on/off status for given unit."""
|
||||||
|
unit = int(code[1])
|
||||||
|
return get_status()[16 - int(unit)] == '1'
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the x10 Light platform."""
|
||||||
|
try:
|
||||||
|
x10_command("info")
|
||||||
|
except CalledProcessError as err:
|
||||||
|
_LOGGER.error(err.output)
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices(X10Light(light) for light in config['lights'])
|
||||||
|
|
||||||
|
|
||||||
|
class X10Light(Light):
|
||||||
|
"""Representation of an X10 Light."""
|
||||||
|
|
||||||
|
def __init__(self, light):
|
||||||
|
"""Initialize an X10 Light."""
|
||||||
|
self._name = light['name']
|
||||||
|
self._id = light['id']
|
||||||
|
self._is_on = False
|
||||||
|
self._brightness = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the display name of this light."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Brightness of the light (an integer in the range 1-255)."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self._is_on
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Instruct the light to turn on."""
|
||||||
|
x10_command("on " + self._id)
|
||||||
|
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
|
||||||
|
self._is_on = True
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Instruct the light to turn off."""
|
||||||
|
x10_command("off " + self._id)
|
||||||
|
self._is_on = False
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Fetch new state data for this light."""
|
||||||
|
self._is_on = get_unit_status(self._id)
|
@ -8,7 +8,6 @@ import logging
|
|||||||
|
|
||||||
# Because we do not compile openzwave on CI
|
# Because we do not compile openzwave on CI
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from threading import Timer
|
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||||
ATTR_RGB_COLOR, DOMAIN, Light
|
ATTR_RGB_COLOR, DOMAIN, Light
|
||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
@ -107,24 +106,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
|
|
||||||
def _value_changed(self, value):
|
def _value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id != value.value_id:
|
if self._value.value_id == value.value_id or \
|
||||||
return
|
self._value.node == value.node:
|
||||||
|
|
||||||
if self._refreshing:
|
|
||||||
self._refreshing = False
|
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
else:
|
|
||||||
def _refresh_value():
|
|
||||||
"""Used timer callback for delayed value refresh."""
|
|
||||||
self._refreshing = True
|
|
||||||
self._value.refresh()
|
|
||||||
|
|
||||||
if self._timer is not None and self._timer.isAlive():
|
|
||||||
self._timer.cancel()
|
|
||||||
|
|
||||||
self._timer = Timer(2, _refresh_value)
|
|
||||||
self._timer.start()
|
|
||||||
|
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -8,7 +8,7 @@ import logging
|
|||||||
|
|
||||||
from homeassistant.components.lock import LockDevice
|
from homeassistant.components.lock import LockDevice
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
|
STATE_LOCKED, STATE_UNLOCKED)
|
||||||
from homeassistant.components.vera import (
|
from homeassistant.components.vera import (
|
||||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||||
|
|
||||||
@ -32,16 +32,6 @@ class VeraLock(VeraDevice, LockDevice):
|
|||||||
self._state = None
|
self._state = None
|
||||||
VeraDevice.__init__(self, vera_device, controller)
|
VeraDevice.__init__(self, vera_device, controller)
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the state attributes of the device."""
|
|
||||||
attr = {}
|
|
||||||
if self.vera_device.has_battery:
|
|
||||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
|
||||||
|
|
||||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
|
||||||
return attr
|
|
||||||
|
|
||||||
def lock(self, **kwargs):
|
def lock(self, **kwargs):
|
||||||
"""Lock the device."""
|
"""Lock the device."""
|
||||||
self.vera_device.lock()
|
self.vera_device.lock()
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.lock import LockDevice
|
|||||||
from homeassistant.components.wink import WinkDevice
|
from homeassistant.components.wink import WinkDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -46,7 +46,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||||||
|
|
||||||
def _value_changed(self, value):
|
def _value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id:
|
if self._value.value_id == value.value_id or \
|
||||||
|
self._value.node == value.node:
|
||||||
self._state = value.data
|
self._state = value.data
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/logbook/
|
https://home-assistant.io/components/logbook/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
@ -14,6 +13,7 @@ import voluptuous as vol
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components import recorder, sun
|
from homeassistant.components import recorder, sun
|
||||||
|
from homeassistant.components.frontend import register_built_in_panel
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||||
@ -24,9 +24,7 @@ from homeassistant.helpers import template
|
|||||||
from homeassistant.helpers.entity import split_entity_id
|
from homeassistant.helpers.entity import split_entity_id
|
||||||
|
|
||||||
DOMAIN = "logbook"
|
DOMAIN = "logbook"
|
||||||
DEPENDENCIES = ['recorder', 'http']
|
DEPENDENCIES = ['recorder', 'frontend']
|
||||||
|
|
||||||
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -75,6 +73,9 @@ def setup(hass, config):
|
|||||||
|
|
||||||
hass.wsgi.register_view(LogbookView)
|
hass.wsgi.register_view(LogbookView)
|
||||||
|
|
||||||
|
register_built_in_panel(hass, 'logbook', 'Logbook',
|
||||||
|
'mdi:format-list-bulleted-type')
|
||||||
|
|
||||||
hass.services.register(DOMAIN, 'log', log_message,
|
hass.services.register(DOMAIN, 'log', log_message,
|
||||||
schema=LOG_MESSAGE_SCHEMA)
|
schema=LOG_MESSAGE_SCHEMA)
|
||||||
return True
|
return True
|
||||||
@ -85,16 +86,11 @@ class LogbookView(HomeAssistantView):
|
|||||||
|
|
||||||
url = '/api/logbook'
|
url = '/api/logbook'
|
||||||
name = 'api:logbook'
|
name = 'api:logbook'
|
||||||
extra_urls = ['/api/logbook/<date:date>']
|
extra_urls = ['/api/logbook/<datetime:datetime>']
|
||||||
|
|
||||||
def get(self, request, date=None):
|
def get(self, request, datetime=None):
|
||||||
"""Retrieve logbook entries."""
|
"""Retrieve logbook entries."""
|
||||||
if date:
|
start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day())
|
||||||
start_day = dt_util.start_of_local_day(date)
|
|
||||||
else:
|
|
||||||
start_day = dt_util.start_of_local_day()
|
|
||||||
|
|
||||||
start_day = dt_util.as_utc(start_day)
|
|
||||||
end_day = start_day + timedelta(days=1)
|
end_day = start_day + timedelta(days=1)
|
||||||
|
|
||||||
events = recorder.get_model('Events')
|
events = recorder.get_model('Events')
|
||||||
|
172
homeassistant/components/media_player/directv.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
"""Support for the DirecTV recievers."""
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
|
||||||
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP,
|
||||||
|
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['directpy==0.1']
|
||||||
|
|
||||||
|
DEFAULT_PORT = 8080
|
||||||
|
|
||||||
|
SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||||
|
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
|
||||||
|
SUPPORT_PREVIOUS_TRACK
|
||||||
|
|
||||||
|
KNOWN_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the DirecTV platform."""
|
||||||
|
hosts = []
|
||||||
|
|
||||||
|
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||||
|
return
|
||||||
|
|
||||||
|
if discovery_info is not None:
|
||||||
|
hosts.append([
|
||||||
|
'DirecTV_' + discovery_info[1],
|
||||||
|
discovery_info[0],
|
||||||
|
DEFAULT_PORT
|
||||||
|
])
|
||||||
|
|
||||||
|
elif CONF_HOST in config:
|
||||||
|
hosts.append([
|
||||||
|
config.get(CONF_NAME, 'DirecTV Receiver'),
|
||||||
|
config[CONF_HOST], DEFAULT_PORT
|
||||||
|
])
|
||||||
|
|
||||||
|
dtvs = []
|
||||||
|
|
||||||
|
for host in hosts:
|
||||||
|
dtvs.append(DirecTvDevice(*host))
|
||||||
|
KNOWN_HOSTS.append(host)
|
||||||
|
|
||||||
|
add_devices(dtvs)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DirecTvDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a DirecTV reciever on the network."""
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
def __init__(self, name, host, port):
|
||||||
|
"""Initialize the device."""
|
||||||
|
from DirectPy import DIRECTV
|
||||||
|
self.dtv = DIRECTV(host, port)
|
||||||
|
self._name = name
|
||||||
|
self._is_standby = True
|
||||||
|
self._current = None
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Retrieve latest state."""
|
||||||
|
self._is_standby = self.dtv.get_standby()
|
||||||
|
if self._is_standby:
|
||||||
|
self._current = None
|
||||||
|
else:
|
||||||
|
self._current = self.dtv.get_tuned()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
# MediaPlayerDevice properties and methods
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
if self._is_standby:
|
||||||
|
return STATE_OFF
|
||||||
|
# haven't determined a way to see if the content is paused
|
||||||
|
else:
|
||||||
|
return STATE_PLAYING
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_id(self):
|
||||||
|
"""Content ID of current playing media."""
|
||||||
|
if self._is_standby:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self._current['programId']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_duration(self):
|
||||||
|
"""Duration of current playing media in seconds."""
|
||||||
|
if self._is_standby:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self._current['duration']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_title(self):
|
||||||
|
"""Title of current playing media."""
|
||||||
|
if self._is_standby:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self._current['title']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_series_title(self):
|
||||||
|
"""Title of current episode of TV show."""
|
||||||
|
if self._is_standby:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if 'episodeTitle' in self._current:
|
||||||
|
return self._current['episodeTitle']
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_DTV
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_type(self):
|
||||||
|
"""Content type of current playing media."""
|
||||||
|
if 'episodeTitle' in self._current:
|
||||||
|
return MEDIA_TYPE_TVSHOW
|
||||||
|
else:
|
||||||
|
return MEDIA_TYPE_VIDEO
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_channel(self):
|
||||||
|
"""Channel current playing media."""
|
||||||
|
if self._is_standby:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
chan = "{} ({})".format(self._current['callsign'],
|
||||||
|
self._current['major'])
|
||||||
|
return chan
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn on the reciever."""
|
||||||
|
self.dtv.key_press('poweron')
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off the reciever."""
|
||||||
|
self.dtv.key_press('poweroff')
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
"""Send play commmand."""
|
||||||
|
self.dtv.key_press('play')
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
"""Send pause commmand."""
|
||||||
|
self.dtv.key_press('pause')
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
"""Send stop commmand."""
|
||||||
|
self.dtv.key_press('stop')
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
"""Send rewind commmand."""
|
||||||
|
self.dtv.key_press('rew')
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
"""Send fast forward commmand."""
|
||||||
|
self.dtv.key_press('ffwd')
|
153
homeassistant/components/media_player/mpchc.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Support to interface with the MPC-HC Web API.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.mpchc/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK,
|
||||||
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, MediaPlayerDevice)
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SUPPORT_MPCHC = SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | SUPPORT_STOP | \
|
||||||
|
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_STEP
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the MPC-HC platform."""
|
||||||
|
name = config.get("name", "MPC-HC")
|
||||||
|
url = '{}:{}'.format(config.get('host'), config.get('port', '13579'))
|
||||||
|
|
||||||
|
if config.get('host') is None:
|
||||||
|
_LOGGER.error("Missing NPC-HC host address in config")
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices([MpcHcDevice(name, url)])
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
class MpcHcDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a MPC-HC server."""
|
||||||
|
|
||||||
|
def __init__(self, name, url):
|
||||||
|
"""Initialize the MPC-HC device."""
|
||||||
|
self._name = name
|
||||||
|
self._url = url
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest details."""
|
||||||
|
self._player_variables = dict()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get("{}/variables.html".format(self._url),
|
||||||
|
data=None, timeout=3)
|
||||||
|
|
||||||
|
mpchc_variables = re.findall(r'<p id="(.+?)">(.+?)</p>',
|
||||||
|
response.text)
|
||||||
|
|
||||||
|
self._player_variables = dict()
|
||||||
|
for var in mpchc_variables:
|
||||||
|
self._player_variables[var[0]] = var[1].lower()
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
_LOGGER.error("Could not connect to MPC-HC at: %s", self._url)
|
||||||
|
|
||||||
|
def _send_command(self, command_id):
|
||||||
|
"""Send a command to MPC-HC via its window message ID."""
|
||||||
|
try:
|
||||||
|
params = {"wm_command": command_id}
|
||||||
|
requests.get("{}/command.html".format(self._url),
|
||||||
|
params=params, timeout=3)
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
_LOGGER.error("Could not send command %d to MPC-HC at: %s",
|
||||||
|
command_id, self._url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
state = self._player_variables.get('statestring', None)
|
||||||
|
|
||||||
|
if state is None:
|
||||||
|
return STATE_OFF
|
||||||
|
if state == 'playing':
|
||||||
|
return STATE_PLAYING
|
||||||
|
elif state == 'paused':
|
||||||
|
return STATE_PAUSED
|
||||||
|
else:
|
||||||
|
return STATE_IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_title(self):
|
||||||
|
"""Title of current playing media."""
|
||||||
|
return self._player_variables.get('file', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
return int(self._player_variables.get('volumelevel', 0)) / 100.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Boolean if volume is currently muted."""
|
||||||
|
return self._player_variables.get('muted', '0') == '1'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_duration(self):
|
||||||
|
"""Duration of current playing media in seconds."""
|
||||||
|
duration = self._player_variables.get('durationstring',
|
||||||
|
"00:00:00").split(':')
|
||||||
|
return \
|
||||||
|
int(duration[0]) * 3600 + \
|
||||||
|
int(duration[1]) * 60 + \
|
||||||
|
int(duration[2])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_MPCHC
|
||||||
|
|
||||||
|
def volume_up(self):
|
||||||
|
"""Volume up the media player."""
|
||||||
|
self._send_command(907)
|
||||||
|
|
||||||
|
def volume_down(self):
|
||||||
|
"""Volume down media player."""
|
||||||
|
self._send_command(908)
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Mute the volume."""
|
||||||
|
self._send_command(909)
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
"""Send play command."""
|
||||||
|
self._send_command(887)
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
"""Send pause command."""
|
||||||
|
self._send_command(888)
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
"""Send stop command."""
|
||||||
|
self._send_command(890)
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
"""Send next track command."""
|
||||||
|
self._send_command(921)
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
"""Send previous track command."""
|
||||||
|
self._send_command(920)
|
118
homeassistant/components/media_player/russound_rnet.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Support for interfacing with Russound via RNET Protocol.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.russound_rnet/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
|
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
|
||||||
|
|
||||||
|
REQUIREMENTS = [
|
||||||
|
'https://github.com/laf/russound/archive/0.1.6.zip'
|
||||||
|
'#russound==0.1.6']
|
||||||
|
|
||||||
|
ZONES = 'zones'
|
||||||
|
SOURCES = 'sources'
|
||||||
|
|
||||||
|
SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||||
|
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Russound RNET platform."""
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
port = config.get(CONF_PORT)
|
||||||
|
keypad = config.get('keypad', '70')
|
||||||
|
|
||||||
|
if host is None or port is None:
|
||||||
|
_LOGGER.error('Invalid config. Expected %s and %s',
|
||||||
|
CONF_HOST, CONF_PORT)
|
||||||
|
return False
|
||||||
|
|
||||||
|
from russound import russound
|
||||||
|
|
||||||
|
russ = russound.Russound(host, port)
|
||||||
|
russ.connect(keypad)
|
||||||
|
|
||||||
|
sources = []
|
||||||
|
for source in config[SOURCES]:
|
||||||
|
sources.append(source['name'])
|
||||||
|
|
||||||
|
if russ.is_connected():
|
||||||
|
for zone_id, extra in config[ZONES].items():
|
||||||
|
add_devices([RussoundRNETDevice(hass, russ, sources, zone_id,
|
||||||
|
extra)])
|
||||||
|
else:
|
||||||
|
_LOGGER.error('Not connected to %s:%s', host, port)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method, too-many-public-methods,
|
||||||
|
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||||
|
class RussoundRNETDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a Russound RNET device."""
|
||||||
|
|
||||||
|
def __init__(self, hass, russ, sources, zone_id, extra):
|
||||||
|
"""Initialise the Russound RNET device."""
|
||||||
|
self._name = extra['name']
|
||||||
|
self._russ = russ
|
||||||
|
self._state = STATE_OFF
|
||||||
|
self._sources = sources
|
||||||
|
self._zone_id = zone_id
|
||||||
|
self._volume = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the zone."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_RUSSOUND
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
return self._volume
|
||||||
|
|
||||||
|
def set_volume_level(self, volume):
|
||||||
|
"""Set volume level, range 0..1."""
|
||||||
|
self._volume = volume * 100
|
||||||
|
self._russ.set_volume('1', self._zone_id, self._volume)
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn the media player on."""
|
||||||
|
self._russ.set_power('1', self._zone_id, '1')
|
||||||
|
self._state = STATE_ON
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off media player."""
|
||||||
|
self._russ.set_power('1', self._zone_id, '0')
|
||||||
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Send mute command."""
|
||||||
|
self._russ.toggle_mute('1', self._zone_id)
|
||||||
|
|
||||||
|
def select_source(self, source):
|
||||||
|
"""Set the input source."""
|
||||||
|
if source in self._sources:
|
||||||
|
index = self._sources.index(source)+1
|
||||||
|
self._russ.set_source('1', self._zone_id, index)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_list(self):
|
||||||
|
"""List of available input sources."""
|
||||||
|
return self._sources
|
@ -6,8 +6,9 @@ https://home-assistant.io/components/media_player.sonos/
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
from os import path
|
from os import path
|
||||||
|
import socket
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||||
@ -15,8 +16,10 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
||||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF,
|
||||||
|
ATTR_ENTITY_ID)
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['SoCo==0.11.1']
|
REQUIREMENTS = ['SoCo==0.11.1']
|
||||||
|
|
||||||
@ -43,16 +46,28 @@ SUPPORT_SOURCE_LINEIN = 'Line-in'
|
|||||||
SUPPORT_SOURCE_TV = 'TV'
|
SUPPORT_SOURCE_TV = 'TV'
|
||||||
SUPPORT_SOURCE_RADIO = 'Radio'
|
SUPPORT_SOURCE_RADIO = 'Radio'
|
||||||
|
|
||||||
|
SONOS_SCHEMA = vol.Schema({
|
||||||
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
# List of devices that have been registered
|
||||||
|
DEVICES = []
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument, too-many-locals
|
# pylint: disable=unused-argument, too-many-locals
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Sonos platform."""
|
"""Setup the Sonos platform."""
|
||||||
import soco
|
import soco
|
||||||
|
global DEVICES
|
||||||
|
|
||||||
if discovery_info:
|
if discovery_info:
|
||||||
player = soco.SoCo(discovery_info)
|
player = soco.SoCo(discovery_info)
|
||||||
if player.is_visible:
|
if player.is_visible:
|
||||||
add_devices([SonosDevice(hass, player)])
|
device = SonosDevice(hass, player)
|
||||||
|
add_devices([device])
|
||||||
|
if not DEVICES:
|
||||||
|
register_services(hass)
|
||||||
|
DEVICES.append(device)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -74,61 +89,73 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
_LOGGER.warning('No Sonos speakers found.')
|
_LOGGER.warning('No Sonos speakers found.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
devices = [SonosDevice(hass, p) for p in players]
|
DEVICES = [SonosDevice(hass, p) for p in players]
|
||||||
add_devices(devices)
|
add_devices(DEVICES)
|
||||||
|
register_services(hass)
|
||||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def register_services(hass):
|
||||||
|
"""Register all services for sonos devices."""
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
|
||||||
|
_group_players_service,
|
||||||
|
descriptions.get(SERVICE_GROUP_PLAYERS),
|
||||||
|
schema=SONOS_SCHEMA)
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_UNJOIN,
|
||||||
|
_unjoin_service,
|
||||||
|
descriptions.get(SERVICE_UNJOIN),
|
||||||
|
schema=SONOS_SCHEMA)
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
|
||||||
|
_snapshot_service,
|
||||||
|
descriptions.get(SERVICE_SNAPSHOT),
|
||||||
|
schema=SONOS_SCHEMA)
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_RESTORE,
|
||||||
|
_restore_service,
|
||||||
|
descriptions.get(SERVICE_RESTORE),
|
||||||
|
schema=SONOS_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
def _apply_service(service, service_func, *service_func_args):
|
def _apply_service(service, service_func, *service_func_args):
|
||||||
"""Internal func for applying a service."""
|
"""Internal func for applying a service."""
|
||||||
entity_id = service.data.get('entity_id')
|
entity_ids = service.data.get('entity_id')
|
||||||
|
|
||||||
if entity_id:
|
if entity_ids:
|
||||||
_devices = [device for device in devices
|
_devices = [device for device in DEVICES
|
||||||
if device.entity_id == entity_id]
|
if device.entity_id in entity_ids]
|
||||||
else:
|
else:
|
||||||
_devices = devices
|
_devices = DEVICES
|
||||||
|
|
||||||
for device in _devices:
|
for device in _devices:
|
||||||
service_func(device, *service_func_args)
|
service_func(device, *service_func_args)
|
||||||
device.update_ha_state(True)
|
device.update_ha_state(True)
|
||||||
|
|
||||||
def group_players_service(service):
|
|
||||||
|
def _group_players_service(service):
|
||||||
"""Group media players, use player as coordinator."""
|
"""Group media players, use player as coordinator."""
|
||||||
_apply_service(service, SonosDevice.group_players)
|
_apply_service(service, SonosDevice.group_players)
|
||||||
|
|
||||||
def unjoin_service(service):
|
|
||||||
|
def _unjoin_service(service):
|
||||||
"""Unjoin the player from a group."""
|
"""Unjoin the player from a group."""
|
||||||
_apply_service(service, SonosDevice.unjoin)
|
_apply_service(service, SonosDevice.unjoin)
|
||||||
|
|
||||||
def snapshot_service(service):
|
|
||||||
|
def _snapshot_service(service):
|
||||||
"""Take a snapshot."""
|
"""Take a snapshot."""
|
||||||
_apply_service(service, SonosDevice.snapshot)
|
_apply_service(service, SonosDevice.snapshot)
|
||||||
|
|
||||||
def restore_service(service):
|
|
||||||
|
def _restore_service(service):
|
||||||
"""Restore a snapshot."""
|
"""Restore a snapshot."""
|
||||||
_apply_service(service, SonosDevice.restore)
|
_apply_service(service, SonosDevice.restore)
|
||||||
|
|
||||||
descriptions = load_yaml_config_file(
|
|
||||||
path.join(path.dirname(__file__), 'services.yaml'))
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
|
|
||||||
group_players_service,
|
|
||||||
descriptions.get(SERVICE_GROUP_PLAYERS))
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_UNJOIN,
|
|
||||||
unjoin_service,
|
|
||||||
descriptions.get(SERVICE_UNJOIN))
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
|
|
||||||
snapshot_service,
|
|
||||||
descriptions.get(SERVICE_SNAPSHOT))
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_RESTORE,
|
|
||||||
restore_service,
|
|
||||||
descriptions.get(SERVICE_RESTORE))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def only_if_coordinator(func):
|
def only_if_coordinator(func):
|
||||||
"""Decorator for coordinator.
|
"""Decorator for coordinator.
|
||||||
|
@ -22,7 +22,7 @@ def get_service(hass, config):
|
|||||||
"""Get the GNTP notification service."""
|
"""Get the GNTP notification service."""
|
||||||
if config.get('app_icon') is None:
|
if config.get('app_icon') is None:
|
||||||
icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend",
|
icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend",
|
||||||
"www_static", "favicon-192x192.png")
|
"www_static", "icons", "favicon-192x192.png")
|
||||||
app_icon = open(icon_file, 'rb').read()
|
app_icon = open(icon_file, 'rb').read()
|
||||||
else:
|
else:
|
||||||
app_icon = config.get('app_icon')
|
app_icon = config.get('app_icon')
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
"""
|
|
||||||
Google Voice SMS platform for notify component.
|
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
|
||||||
https://home-assistant.io/components/notify.google_voice/
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.components.notify import (
|
|
||||||
ATTR_TARGET, DOMAIN, BaseNotificationService)
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
|
||||||
from homeassistant.helpers import validate_config
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
REQUIREMENTS = ['https://github.com/w1ll1am23/pygooglevoice-sms/archive/'
|
|
||||||
'7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#'
|
|
||||||
'pygooglevoice-sms==0.0.1']
|
|
||||||
|
|
||||||
|
|
||||||
def get_service(hass, config):
|
|
||||||
"""Get the Google Voice SMS notification service."""
|
|
||||||
if not validate_config({DOMAIN: config},
|
|
||||||
{DOMAIN: [CONF_USERNAME,
|
|
||||||
CONF_PASSWORD]},
|
|
||||||
_LOGGER):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return GoogleVoiceSMSNotificationService(config[CONF_USERNAME],
|
|
||||||
config[CONF_PASSWORD])
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class GoogleVoiceSMSNotificationService(BaseNotificationService):
|
|
||||||
"""Implement the notification service for the Google Voice SMS service."""
|
|
||||||
|
|
||||||
def __init__(self, username, password):
|
|
||||||
"""Initialize the service."""
|
|
||||||
from googlevoicesms import Voice
|
|
||||||
self.voice = Voice()
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
|
||||||
"""Send SMS to specified target user cell."""
|
|
||||||
targets = kwargs.get(ATTR_TARGET)
|
|
||||||
|
|
||||||
if not targets:
|
|
||||||
_LOGGER.info('At least 1 target is required')
|
|
||||||
return
|
|
||||||
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
self.voice.login(self.username, self.password)
|
|
||||||
|
|
||||||
for target in targets:
|
|
||||||
self.voice.send_sms(target, message)
|
|
||||||
|
|
||||||
self.voice.logout()
|
|
@ -10,7 +10,7 @@ from homeassistant.components.notify import (
|
|||||||
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
REQUIREMENTS = ['sendgrid>=1.6.0,<1.7.0']
|
REQUIREMENTS = ['sendgrid==3.0.7']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -24,27 +24,50 @@ def get_service(hass, config):
|
|||||||
api_key = config['api_key']
|
api_key = config['api_key']
|
||||||
sender = config['sender']
|
sender = config['sender']
|
||||||
recipient = config['recipient']
|
recipient = config['recipient']
|
||||||
|
|
||||||
return SendgridNotificationService(api_key, sender, recipient)
|
return SendgridNotificationService(api_key, sender, recipient)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class SendgridNotificationService(BaseNotificationService):
|
class SendgridNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for email via Sendgrid."""
|
"""Implementation the notification service for email via Sendgrid."""
|
||||||
|
|
||||||
def __init__(self, api_key, sender, recipient):
|
def __init__(self, api_key, sender, recipient):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
|
from sendgrid import SendGridAPIClient
|
||||||
|
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.recipient = recipient
|
self.recipient = recipient
|
||||||
|
|
||||||
from sendgrid import SendGridClient
|
self._sg = SendGridAPIClient(apikey=self.api_key)
|
||||||
self._sg = SendGridClient(self.api_key)
|
|
||||||
|
|
||||||
def send_message(self, message='', **kwargs):
|
def send_message(self, message='', **kwargs):
|
||||||
"""Send an email to a user via SendGrid."""
|
"""Send an email to a user via SendGrid."""
|
||||||
subject = kwargs.get(ATTR_TITLE)
|
subject = kwargs.get(ATTR_TITLE)
|
||||||
|
|
||||||
from sendgrid import Mail
|
data = {
|
||||||
mail = Mail(from_email=self.sender, to=self.recipient,
|
"personalizations": [
|
||||||
html=message, text=message, subject=subject)
|
{
|
||||||
self._sg.send(mail)
|
"to": [
|
||||||
|
{
|
||||||
|
"email": self.recipient
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subject": subject
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"from": {
|
||||||
|
"email": self.sender
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text/plain",
|
||||||
|
"value": message
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self._sg.client.mail.send.post(request_body=data)
|
||||||
|
if response.status_code is not 202:
|
||||||
|
_LOGGER.error('Unable to send notification with SendGrid')
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
|
|||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
REQUIREMENTS = ['slacker==0.9.21']
|
REQUIREMENTS = ['slacker==0.9.24']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,43 +8,69 @@ import io
|
|||||||
import logging
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
import requests
|
import requests
|
||||||
from requests.auth import HTTPBasicAuth
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
|
ATTR_TITLE, ATTR_DATA, BaseNotificationService)
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import (CONF_API_KEY, CONF_NAME, ATTR_LOCATION,
|
||||||
from homeassistant.helpers import validate_config
|
ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['python-telegram-bot==4.3.3']
|
REQUIREMENTS = ['python-telegram-bot==5.0.0']
|
||||||
|
|
||||||
ATTR_PHOTO = "photo"
|
ATTR_PHOTO = "photo"
|
||||||
ATTR_FILE = "file"
|
|
||||||
ATTR_URL = "url"
|
|
||||||
ATTR_CAPTION = "caption"
|
ATTR_CAPTION = "caption"
|
||||||
ATTR_USERNAME = "username"
|
|
||||||
ATTR_PASSWORD = "password"
|
CONF_CHAT_ID = 'chat_id'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_PLATFORM): "telegram",
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
|
vol.Required(CONF_CHAT_ID): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_service(hass, config):
|
def get_service(hass, config):
|
||||||
"""Get the Telegram notification service."""
|
"""Get the Telegram notification service."""
|
||||||
import telegram
|
import telegram
|
||||||
|
|
||||||
if not validate_config({DOMAIN: config},
|
|
||||||
{DOMAIN: [CONF_API_KEY, 'chat_id']},
|
|
||||||
_LOGGER):
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot = telegram.Bot(token=config[CONF_API_KEY])
|
chat_id = config.get(CONF_CHAT_ID)
|
||||||
|
api_key = config.get(CONF_API_KEY)
|
||||||
|
bot = telegram.Bot(token=api_key)
|
||||||
username = bot.getMe()['username']
|
username = bot.getMe()['username']
|
||||||
_LOGGER.info("Telegram bot is '%s'.", username)
|
_LOGGER.info("Telegram bot is '%s'.", username)
|
||||||
except urllib.error.HTTPError:
|
except urllib.error.HTTPError:
|
||||||
_LOGGER.error("Please check your access token.")
|
_LOGGER.error("Please check your access token.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return TelegramNotificationService(config[CONF_API_KEY], config['chat_id'])
|
return TelegramNotificationService(api_key, chat_id)
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(url=None, file=None, username=None, password=None):
|
||||||
|
"""Load photo/document into ByteIO/File container from a source."""
|
||||||
|
try:
|
||||||
|
if url is not None:
|
||||||
|
# load photo from url
|
||||||
|
if username is not None and password is not None:
|
||||||
|
req = requests.get(url, auth=(username, password), timeout=15)
|
||||||
|
else:
|
||||||
|
req = requests.get(url, timeout=15)
|
||||||
|
return io.BytesIO(req.content)
|
||||||
|
|
||||||
|
elif file is not None:
|
||||||
|
# load photo from file
|
||||||
|
return open(file, "rb")
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Can't load photo no photo found in params!")
|
||||||
|
|
||||||
|
except (OSError, IOError, requests.exceptions.RequestException):
|
||||||
|
_LOGGER.error("Can't load photo into ByteIO")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
@ -64,7 +90,18 @@ class TelegramNotificationService(BaseNotificationService):
|
|||||||
import telegram
|
import telegram
|
||||||
|
|
||||||
title = kwargs.get(ATTR_TITLE)
|
title = kwargs.get(ATTR_TITLE)
|
||||||
data = kwargs.get(ATTR_DATA, {})
|
data = kwargs.get(ATTR_DATA)
|
||||||
|
|
||||||
|
# exists data for send a photo/location
|
||||||
|
if data is not None and ATTR_PHOTO in data:
|
||||||
|
photos = data.get(ATTR_PHOTO, None)
|
||||||
|
photos = photos if isinstance(photos, list) else [photos]
|
||||||
|
|
||||||
|
for photo_data in photos:
|
||||||
|
self.send_photo(photo_data)
|
||||||
|
return
|
||||||
|
elif data is not None and ATTR_LOCATION in data:
|
||||||
|
return self.send_location(data.get(ATTR_LOCATION))
|
||||||
|
|
||||||
# send message
|
# send message
|
||||||
try:
|
try:
|
||||||
@ -74,41 +111,30 @@ class TelegramNotificationService(BaseNotificationService):
|
|||||||
_LOGGER.exception("Error sending message.")
|
_LOGGER.exception("Error sending message.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def send_photo(self, data):
|
||||||
|
"""Send a photo."""
|
||||||
|
import telegram
|
||||||
|
caption = data.pop(ATTR_CAPTION, None)
|
||||||
|
|
||||||
# send photo
|
# send photo
|
||||||
if ATTR_PHOTO in data:
|
|
||||||
# if not a list
|
|
||||||
if not isinstance(data[ATTR_PHOTO], list):
|
|
||||||
photos = [data[ATTR_PHOTO]]
|
|
||||||
else:
|
|
||||||
photos = data[ATTR_PHOTO]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for photo_data in photos:
|
photo = load_data(**data)
|
||||||
caption = photo_data.get(ATTR_CAPTION, None)
|
|
||||||
|
|
||||||
# file is a url
|
|
||||||
if ATTR_URL in photo_data:
|
|
||||||
# use http authenticate
|
|
||||||
if ATTR_USERNAME in photo_data and\
|
|
||||||
ATTR_PASSWORD in photo_data:
|
|
||||||
req = requests.get(
|
|
||||||
photo_data[ATTR_URL],
|
|
||||||
auth=HTTPBasicAuth(photo_data[ATTR_USERNAME],
|
|
||||||
photo_data[ATTR_PASSWORD])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
req = requests.get(photo_data[ATTR_URL])
|
|
||||||
file_id = io.BytesIO(req.content)
|
|
||||||
elif ATTR_FILE in photo_data:
|
|
||||||
file_id = open(photo_data[ATTR_FILE], "rb")
|
|
||||||
else:
|
|
||||||
_LOGGER.error("No url or path is set for photo!")
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.bot.sendPhoto(chat_id=self._chat_id,
|
self.bot.sendPhoto(chat_id=self._chat_id,
|
||||||
photo=file_id, caption=caption)
|
photo=photo, caption=caption)
|
||||||
|
except telegram.error.TelegramError:
|
||||||
except (OSError, IOError, telegram.error.TelegramError,
|
|
||||||
urllib.error.HTTPError):
|
|
||||||
_LOGGER.exception("Error sending photo.")
|
_LOGGER.exception("Error sending photo.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def send_location(self, gps):
|
||||||
|
"""Send a location."""
|
||||||
|
import telegram
|
||||||
|
latitude = float(gps.get(ATTR_LATITUDE, 0.0))
|
||||||
|
longitude = float(gps.get(ATTR_LONGITUDE, 0.0))
|
||||||
|
|
||||||
|
# send location
|
||||||
|
try:
|
||||||
|
self.bot.sendLocation(chat_id=self._chat_id,
|
||||||
|
latitude=latitude, longitude=longitude)
|
||||||
|
except telegram.error.TelegramError:
|
||||||
|
_LOGGER.exception("Error sending location.")
|
||||||
|
return
|
||||||
|
31
homeassistant/components/panel_iframe.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""Add an iframe panel to Home Assistant."""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.frontend import register_built_in_panel
|
||||||
|
|
||||||
|
DOMAIN = 'panel_iframe'
|
||||||
|
DEPENDENCIES = ['frontend']
|
||||||
|
|
||||||
|
CONF_TITLE = 'title'
|
||||||
|
CONF_ICON = 'icon'
|
||||||
|
CONF_URL = 'url'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
cv.slug: {
|
||||||
|
vol.Optional(CONF_TITLE): cv.string,
|
||||||
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
vol.Required(CONF_URL): vol.Url(),
|
||||||
|
}})}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Setup iframe frontend panels."""
|
||||||
|
for url_name, info in config[DOMAIN].items():
|
||||||
|
register_built_in_panel(
|
||||||
|
hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
|
||||||
|
url_name, {'url': info[CONF_URL]})
|
||||||
|
|
||||||
|
return True
|
@ -5,18 +5,29 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/qwikswitch/
|
https://home-assistant.io/components/qwikswitch/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
import voluptuous as vol
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
|
||||||
from homeassistant.components.discovery import load_platform
|
|
||||||
|
|
||||||
|
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||||
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
|
from homeassistant.helpers.discovery import load_platform
|
||||||
|
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||||
|
from homeassistant.components.switch import SwitchDevice
|
||||||
|
|
||||||
|
DOMAIN = 'qwikswitch'
|
||||||
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip'
|
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip'
|
||||||
'#pyqwikswitch==0.4']
|
'#pyqwikswitch==0.4']
|
||||||
DEPENDENCIES = []
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'qwikswitch'
|
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
|
||||||
QSUSB = None
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required('url', default='http://127.0.0.1:2020'): vol.Coerce(str),
|
||||||
|
vol.Optional('dimmer_adjust', default=1): CV_DIM_VALUE,
|
||||||
|
vol.Optional('button_events'): vol.Coerce(str)
|
||||||
|
})}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
QSUSB = {}
|
||||||
|
|
||||||
|
|
||||||
class QSToggleEntity(object):
|
class QSToggleEntity(object):
|
||||||
@ -42,6 +53,7 @@ class QSToggleEntity(object):
|
|||||||
self._value = qsitem[PQS_VALUE]
|
self._value = qsitem[PQS_VALUE]
|
||||||
self._qsusb = qsusb
|
self._qsusb = qsusb
|
||||||
self._dim = qsitem[PQS_TYPE] == QSType.dimmer
|
self._dim = qsitem[PQS_TYPE] == QSType.dimmer
|
||||||
|
QSUSB[self._id] = self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
@ -87,51 +99,70 @@ class QSToggleEntity(object):
|
|||||||
self.update_value(0)
|
self.update_value(0)
|
||||||
|
|
||||||
|
|
||||||
|
class QSSwitch(QSToggleEntity, SwitchDevice):
|
||||||
|
"""Switch based on a Qwikswitch relay module."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QSLight(QSToggleEntity, Light):
|
||||||
|
"""Light based on a Qwikswitch relay/dimmer module."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the QSUSB component."""
|
"""Setup the QSUSB component."""
|
||||||
from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD,
|
from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD,
|
||||||
QS_TYPE, PQS_VALUE, PQS_TYPE, QSType)
|
PQS_VALUE, PQS_TYPE, QSType)
|
||||||
|
|
||||||
# Override which cmd's in /&listen packets will fire events
|
# Override which cmd's in /&listen packets will fire events
|
||||||
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
||||||
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
|
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
|
||||||
cmd_buttons = cmd_buttons.split(',')
|
cmd_buttons = cmd_buttons.split(',')
|
||||||
|
|
||||||
try:
|
url = config[DOMAIN]['url']
|
||||||
url = config[DOMAIN].get('url', 'http://127.0.0.1:2020')
|
dimmer_adjust = config[DOMAIN]['dimmer_adjust']
|
||||||
dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1'))
|
|
||||||
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
|
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
|
||||||
|
|
||||||
# Ensure qsusb terminates threads correctly
|
def _stop(event):
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
"""Stop the listener queue and clean up."""
|
||||||
lambda event: qsusb.stop())
|
nonlocal qsusb
|
||||||
except ValueError as val_err:
|
qsusb.stop()
|
||||||
_LOGGER.error(str(val_err))
|
qsusb = None
|
||||||
return False
|
|
||||||
|
|
||||||
qsusb.ha_devices = qsusb.devices()
|
|
||||||
qsusb.ha_objects = {}
|
|
||||||
|
|
||||||
# Identify switches & remove ' Switch' postfix in name
|
|
||||||
for item in qsusb.ha_devices:
|
|
||||||
if item[PQS_TYPE] == QSType.relay and \
|
|
||||||
item[QS_NAME].lower().endswith(' switch'):
|
|
||||||
item[QS_TYPE] = 'switch'
|
|
||||||
item[QS_NAME] = item[QS_NAME][:-7]
|
|
||||||
|
|
||||||
global QSUSB
|
global QSUSB
|
||||||
if QSUSB is None:
|
|
||||||
QSUSB = {}
|
QSUSB = {}
|
||||||
QSUSB[id(qsusb)] = qsusb
|
_LOGGER.info("Waiting for long poll to QSUSB to time out")
|
||||||
|
|
||||||
# Load sub-components for qwikswitch
|
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop)
|
||||||
|
|
||||||
|
# Discover all devices in QSUSB
|
||||||
|
devices = qsusb.devices()
|
||||||
|
QSUSB['switch'] = []
|
||||||
|
QSUSB['light'] = []
|
||||||
|
for item in devices:
|
||||||
|
if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower()
|
||||||
|
.endswith(' switch')):
|
||||||
|
item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix
|
||||||
|
QSUSB['switch'].append(QSSwitch(item, qsusb))
|
||||||
|
elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]:
|
||||||
|
QSUSB['light'].append(QSLight(item, qsusb))
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Ignored unknown QSUSB device: %s", item)
|
||||||
|
|
||||||
|
# Load platforms
|
||||||
for comp_name in ('switch', 'light'):
|
for comp_name in ('switch', 'light'):
|
||||||
load_platform(hass, comp_name, 'qwikswitch',
|
if len(QSUSB[comp_name]) > 0:
|
||||||
{'qsusb_id': id(qsusb)}, config)
|
load_platform(hass, comp_name, 'qwikswitch', {}, config)
|
||||||
|
|
||||||
def qs_callback(item):
|
def qs_callback(item):
|
||||||
"""Typically a button press or update signal."""
|
"""Typically a button press or update signal."""
|
||||||
|
if qsusb is None: # Shutting down
|
||||||
|
_LOGGER.info("Done")
|
||||||
|
return
|
||||||
|
|
||||||
# If button pressed, fire a hass event
|
# If button pressed, fire a hass event
|
||||||
if item.get(QS_CMD, '') in cmd_buttons:
|
if item.get(QS_CMD, '') in cmd_buttons:
|
||||||
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
|
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
|
||||||
@ -142,9 +173,13 @@ def setup(hass, config):
|
|||||||
if qsreply is False:
|
if qsreply is False:
|
||||||
return
|
return
|
||||||
for item in qsreply:
|
for item in qsreply:
|
||||||
if item[QS_ID] in qsusb.ha_objects:
|
if item[QS_ID] in QSUSB:
|
||||||
qsusb.ha_objects[item[QS_ID]].update_value(
|
QSUSB[item[QS_ID]].update_value(
|
||||||
round(min(item[PQS_VALUE], 100) * 2.55))
|
round(min(item[PQS_VALUE], 100) * 2.55))
|
||||||
|
|
||||||
|
def _start(event):
|
||||||
|
"""Start listening."""
|
||||||
qsusb.listen(callback=qs_callback, timeout=30)
|
qsusb.listen(callback=qs_callback, timeout=30)
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -39,7 +39,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
||||||
vol.Range(min=1)),
|
vol.Range(min=1)),
|
||||||
vol.Optional(CONF_DB_URL): vol.Url(''),
|
# pylint: disable=no-value-for-parameter
|
||||||
|
vol.Optional(CONF_DB_URL): vol.Url(),
|
||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -90,8 +91,12 @@ def run_information(point_in_time=None):
|
|||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the recorder."""
|
"""Setup the recorder."""
|
||||||
# pylint: disable=global-statement
|
# pylint: disable=global-statement
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
global _INSTANCE
|
global _INSTANCE
|
||||||
|
|
||||||
|
if _INSTANCE is not None:
|
||||||
|
_LOGGER.error('Only a single instance allowed.')
|
||||||
|
return False
|
||||||
|
|
||||||
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
|
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
|
||||||
|
|
||||||
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
|
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
|
||||||
@ -129,7 +134,7 @@ def log_error(e, retry_wait=0, rollback=True,
|
|||||||
if rollback:
|
if rollback:
|
||||||
Session().rollback()
|
Session().rollback()
|
||||||
if retry_wait:
|
if retry_wait:
|
||||||
_LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT)
|
_LOGGER.info("Retrying in %s seconds", retry_wait)
|
||||||
time.sleep(retry_wait)
|
time.sleep(retry_wait)
|
||||||
|
|
||||||
|
|
||||||
@ -164,8 +169,6 @@ class Recorder(threading.Thread):
|
|||||||
from homeassistant.components.recorder.models import Events, States
|
from homeassistant.components.recorder.models import Events, States
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
|
|
||||||
global _INSTANCE
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
self._setup_connection()
|
self._setup_connection()
|
||||||
@ -176,8 +179,12 @@ class Recorder(threading.Thread):
|
|||||||
message="Error during connection setup: %s")
|
message="Error during connection setup: %s")
|
||||||
|
|
||||||
if self.purge_days is not None:
|
if self.purge_days is not None:
|
||||||
track_point_in_utc_time(self.hass,
|
def purge_ticker(event):
|
||||||
lambda now: self._purge_old_data(),
|
"""Rerun purge every second day."""
|
||||||
|
self._purge_old_data()
|
||||||
|
track_point_in_utc_time(self.hass, purge_ticker,
|
||||||
|
dt_util.utcnow() + timedelta(days=2))
|
||||||
|
track_point_in_utc_time(self.hass, purge_ticker,
|
||||||
dt_util.utcnow() + timedelta(minutes=5))
|
dt_util.utcnow() + timedelta(minutes=5))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@ -186,42 +193,26 @@ class Recorder(threading.Thread):
|
|||||||
if event == self.quit_object:
|
if event == self.quit_object:
|
||||||
self._close_run()
|
self._close_run()
|
||||||
self._close_connection()
|
self._close_connection()
|
||||||
|
# pylint: disable=global-statement
|
||||||
|
global _INSTANCE
|
||||||
_INSTANCE = None
|
_INSTANCE = None
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
return
|
return
|
||||||
|
|
||||||
elif event.event_type == EVENT_TIME_CHANGED:
|
if event.event_type == EVENT_TIME_CHANGED:
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
session = Session()
|
|
||||||
dbevent = Events.from_event(event)
|
dbevent = Events.from_event(event)
|
||||||
session.add(dbevent)
|
self._commit(dbevent)
|
||||||
|
|
||||||
for _ in range(0, RETRIES):
|
|
||||||
try:
|
|
||||||
session.commit()
|
|
||||||
break
|
|
||||||
except sqlalchemy.exc.OperationalError as e:
|
|
||||||
log_error(e, retry_wait=QUERY_RETRY_WAIT,
|
|
||||||
rollback=True)
|
|
||||||
|
|
||||||
if event.event_type != EVENT_STATE_CHANGED:
|
if event.event_type != EVENT_STATE_CHANGED:
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
session = Session()
|
|
||||||
dbstate = States.from_event(event)
|
dbstate = States.from_event(event)
|
||||||
|
|
||||||
for _ in range(0, RETRIES):
|
|
||||||
try:
|
|
||||||
dbstate.event_id = dbevent.event_id
|
dbstate.event_id = dbevent.event_id
|
||||||
session.add(dbstate)
|
self._commit(dbstate)
|
||||||
session.commit()
|
|
||||||
break
|
|
||||||
except sqlalchemy.exc.OperationalError as e:
|
|
||||||
log_error(e, retry_wait=QUERY_RETRY_WAIT,
|
|
||||||
rollback=True)
|
|
||||||
|
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
|
|
||||||
@ -268,6 +259,7 @@ class Recorder(threading.Thread):
|
|||||||
|
|
||||||
def _close_connection(self):
|
def _close_connection(self):
|
||||||
"""Close the connection."""
|
"""Close the connection."""
|
||||||
|
# pylint: disable=global-statement
|
||||||
global Session
|
global Session
|
||||||
self.engine.dispose()
|
self.engine.dispose()
|
||||||
self.engine = None
|
self.engine = None
|
||||||
@ -289,16 +281,12 @@ class Recorder(threading.Thread):
|
|||||||
start=self.recording_start,
|
start=self.recording_start,
|
||||||
created=dt_util.utcnow()
|
created=dt_util.utcnow()
|
||||||
)
|
)
|
||||||
session = Session()
|
self._commit(self._run)
|
||||||
session.add(self._run)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def _close_run(self):
|
def _close_run(self):
|
||||||
"""Save end time for current run."""
|
"""Save end time for current run."""
|
||||||
self._run.end = dt_util.utcnow()
|
self._run.end = dt_util.utcnow()
|
||||||
session = Session()
|
self._commit(self._run)
|
||||||
session.add(self._run)
|
|
||||||
session.commit()
|
|
||||||
self._run = None
|
self._run = None
|
||||||
|
|
||||||
def _purge_old_data(self):
|
def _purge_old_data(self):
|
||||||
@ -312,17 +300,24 @@ class Recorder(threading.Thread):
|
|||||||
|
|
||||||
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
|
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
|
||||||
|
|
||||||
_LOGGER.info("Purging events created before %s", purge_before)
|
def _purge_states(session):
|
||||||
deleted_rows = Session().query(Events).filter(
|
deleted_rows = session.query(States) \
|
||||||
(Events.created < purge_before)).delete(synchronize_session=False)
|
.filter((States.created < purge_before)) \
|
||||||
_LOGGER.debug("Deleted %s events", deleted_rows)
|
.delete(synchronize_session=False)
|
||||||
|
|
||||||
_LOGGER.info("Purging states created before %s", purge_before)
|
|
||||||
deleted_rows = Session().query(States).filter(
|
|
||||||
(States.created < purge_before)).delete(synchronize_session=False)
|
|
||||||
_LOGGER.debug("Deleted %s states", deleted_rows)
|
_LOGGER.debug("Deleted %s states", deleted_rows)
|
||||||
|
|
||||||
Session().commit()
|
if self._commit(_purge_states):
|
||||||
|
_LOGGER.info("Purged states created before %s", purge_before)
|
||||||
|
|
||||||
|
def _purge_events(session):
|
||||||
|
deleted_rows = session.query(Events) \
|
||||||
|
.filter((Events.created < purge_before)) \
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
_LOGGER.debug("Deleted %s events", deleted_rows)
|
||||||
|
|
||||||
|
if self._commit(_purge_events):
|
||||||
|
_LOGGER.info("Purged events created before %s", purge_before)
|
||||||
|
|
||||||
Session().expire_all()
|
Session().expire_all()
|
||||||
|
|
||||||
# Execute sqlite vacuum command to free up space on disk
|
# Execute sqlite vacuum command to free up space on disk
|
||||||
@ -330,6 +325,23 @@ class Recorder(threading.Thread):
|
|||||||
_LOGGER.info("Vacuuming SQLite to free space")
|
_LOGGER.info("Vacuuming SQLite to free space")
|
||||||
self.engine.execute("VACUUM")
|
self.engine.execute("VACUUM")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _commit(work):
|
||||||
|
"""Commit & retry work: Either a model or in a function."""
|
||||||
|
import sqlalchemy.exc
|
||||||
|
session = Session()
|
||||||
|
for _ in range(0, RETRIES):
|
||||||
|
try:
|
||||||
|
if callable(work):
|
||||||
|
work(session)
|
||||||
|
else:
|
||||||
|
session.add(work)
|
||||||
|
session.commit()
|
||||||
|
return True
|
||||||
|
except sqlalchemy.exc.OperationalError as e:
|
||||||
|
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _verify_instance():
|
def _verify_instance():
|
||||||
"""Throw error if recorder not initialized."""
|
"""Throw error if recorder not initialized."""
|
||||||
|
@ -20,7 +20,7 @@ Base = declarative_base()
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Events(Base):
|
class Events(Base): # type: ignore
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
"""Event history data."""
|
"""Event history data."""
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ class Events(Base):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class States(Base):
|
class States(Base): # type: ignore
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
"""State change history."""
|
"""State change history."""
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class States(Base):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class RecorderRuns(Base):
|
class RecorderRuns(Base): # type: ignore
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
"""Representation of recorder run."""
|
"""Representation of recorder run."""
|
||||||
|
|
||||||
|
@ -100,6 +100,7 @@ DEVICE_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
DEVICE_SCHEMA_SENSOR = vol.Schema({
|
DEVICE_SCHEMA_SENSOR = vol.Schema({
|
||||||
vol.Optional(ATTR_NAME, default=None): cv.string,
|
vol.Optional(ATTR_NAME, default=None): cv.string,
|
||||||
|
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
|
||||||
vol.Optional(ATTR_DATA_TYPE, default=[]):
|
vol.Optional(ATTR_DATA_TYPE, default=[]):
|
||||||
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
|
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
|
|||||||
from homeassistant.components.wink import WinkDevice
|
from homeassistant.components.wink import WinkDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -49,14 +49,20 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||||||
|
|
||||||
def value_changed(self, value):
|
def value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id:
|
if self._value.value_id == value.value_id or \
|
||||||
self.update_ha_state(True)
|
self._value.node == value.node:
|
||||||
|
self.update_ha_state()
|
||||||
_LOGGER.debug("Value changed on network %s", value)
|
_LOGGER.debug("Value changed on network %s", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_position(self):
|
def current_position(self):
|
||||||
"""Return the current position of Zwave roller shutter."""
|
"""Return the current position of Zwave roller shutter."""
|
||||||
return self._value.data
|
if self._value.data <= 5:
|
||||||
|
return 100
|
||||||
|
elif self._value.data >= 95:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return 100 - self._value.data
|
||||||
|
|
||||||
def move_up(self, **kwargs):
|
def move_up(self, **kwargs):
|
||||||
"""Move the roller shutter up."""
|
"""Move the roller shutter up."""
|
||||||
|