@ -91,6 +91,7 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/switch/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/nx584.py
|
||||
@ -129,21 +130,25 @@ omit =
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
@ -151,6 +156,7 @@ omit =
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
@ -161,7 +167,6 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.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/hello_world.py
|
||||
!config/custom_components/mqtt_example.py
|
||||
!config/custom_components/react_panel
|
||||
|
||||
tests/config/deps
|
||||
tests/config/home-assistant.log
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
|
||||
# Hide sublime text stuff
|
||||
*.sublime-project
|
||||
|
@ -8,8 +8,13 @@ matrix:
|
||||
env: TOXENV=requirements
|
||||
- python: "3.5"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
allow_failures:
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
|
@ -20,7 +20,8 @@ RUN script/build_python_openzwave && \
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# 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 . .
|
||||
|
@ -67,7 +67,7 @@ Build home automation on top of your devices:
|
||||
- Turn on the lights when people get home after sunset
|
||||
- Turn on lights slowly during sunset to compensate for less light
|
||||
- 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
|
||||
like `OwnTracks <http://owntracks.org/>`__
|
||||
- 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 threading
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
from homeassistant.const import (
|
||||
__version__,
|
||||
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."""
|
||||
major, minor = sys.version_info[:2]
|
||||
req_major, req_minor = REQUIRED_PYTHON_VER
|
||||
@ -27,7 +29,7 @@ def validate_python():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_path(config_dir):
|
||||
def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
import homeassistant.config as config_util
|
||||
lib_dir = os.path.join(config_dir, 'deps')
|
||||
@ -56,7 +58,7 @@ def ensure_config_path(config_dir):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_file(config_dir):
|
||||
def ensure_config_file(config_dir: str) -> str:
|
||||
"""Ensure configuration file exists."""
|
||||
import homeassistant.config as config_util
|
||||
config_path = config_util.ensure_config_exists(config_dir)
|
||||
@ -68,7 +70,7 @@ def ensure_config_file(config_dir):
|
||||
return config_path
|
||||
|
||||
|
||||
def get_arguments():
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
import homeassistant.config as config_util
|
||||
parser = argparse.ArgumentParser(
|
||||
@ -125,12 +127,12 @@ def get_arguments():
|
||||
|
||||
arguments = parser.parse_args()
|
||||
if os.name != "posix" or arguments.debug or arguments.runner:
|
||||
arguments.daemon = False
|
||||
setattr(arguments, 'daemon', False)
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def daemonize():
|
||||
def daemonize() -> None:
|
||||
"""Move current process to daemon process."""
|
||||
# Create first fork
|
||||
pid = os.fork()
|
||||
@ -155,7 +157,7 @@ def daemonize():
|
||||
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 pid file
|
||||
try:
|
||||
@ -177,7 +179,7 @@ def check_pid(pid_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def write_pid(pid_file):
|
||||
def write_pid(pid_file: str) -> None:
|
||||
"""Create a PID File."""
|
||||
pid = os.getpid()
|
||||
try:
|
||||
@ -187,7 +189,7 @@ def write_pid(pid_file):
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
def cmdline():
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith('/__main__.py'):
|
||||
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']
|
||||
|
||||
|
||||
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."""
|
||||
from homeassistant import bootstrap
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||
args = cmdline() + ['--runner']
|
||||
nt_args = cmdline() + ['--runner']
|
||||
while True:
|
||||
try:
|
||||
subprocess.check_call(args)
|
||||
subprocess.check_call(nt_args)
|
||||
sys.exit(0)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
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)
|
||||
|
||||
if hass is None:
|
||||
return
|
||||
return None
|
||||
|
||||
if args.open_ui:
|
||||
def open_browser(event):
|
||||
@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args):
|
||||
return exit_code
|
||||
|
||||
|
||||
def try_to_restart():
|
||||
def try_to_restart() -> None:
|
||||
"""Attempt to clean up state and start a new homeassistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
# to clean up things that may have been left behind.
|
||||
@ -303,7 +306,7 @@ def try_to_restart():
|
||||
os.execv(args[0], args)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
|
@ -7,6 +7,9 @@ import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components as core_components
|
||||
@ -30,7 +33,8 @@ ATTR_COMPONENT = 'component'
|
||||
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."""
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
@ -53,7 +57,8 @@ def setup_component(hass, domain, config=None):
|
||||
return True
|
||||
|
||||
|
||||
def _handle_requirements(hass, component, name):
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component."""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
@ -67,9 +72,10 @@ def _handle_requirements(hass, component, name):
|
||||
return True
|
||||
|
||||
|
||||
def _setup_component(hass, domain, config):
|
||||
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant."""
|
||||
# pylint: disable=too-many-return-statements,too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
@ -147,9 +153,15 @@ def _setup_component(hass, domain, config):
|
||||
_CURRENT_SETUP.append(domain)
|
||||
|
||||
try:
|
||||
if not component.setup(hass, config):
|
||||
result = component.setup(hass, config)
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
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
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
return False
|
||||
@ -169,7 +181,8 @@ def _setup_component(hass, domain, config):
|
||||
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."""
|
||||
_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
|
||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
verbose=False, skip_pip=False,
|
||||
log_rotate_days=None):
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant]=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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
log_rotate_days=None):
|
||||
def from_config_file(config_path: str,
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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."""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
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)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass):
|
||||
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
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."""
|
||||
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 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 (
|
||||
BinarySensorDevice)
|
||||
from homeassistant.components.vera import (
|
||||
@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
self._state = False
|
||||
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
|
||||
def is_on(self):
|
||||
"""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.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
|
||||
SENSOR_TYPES = {
|
||||
"opened": "opening",
|
||||
"brightness": "light",
|
||||
"vibration": "vibration",
|
||||
"loudness": "sound"
|
||||
"loudness": "sound",
|
||||
"liquid_detected": "moisture"
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +75,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
return self.wink.vibration_boolean()
|
||||
elif self.capability == "brightness":
|
||||
return self.wink.brightness_boolean()
|
||||
elif self.capability == "liquid_detected":
|
||||
return self.wink.liquid_boolean()
|
||||
else:
|
||||
return self.wink.state()
|
||||
|
||||
|
@ -94,7 +94,8 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""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()
|
||||
|
||||
|
||||
|
@ -13,7 +13,8 @@ ATTR_URL = 'url'
|
||||
ATTR_URL_DEFAULT = 'https://www.google.com'
|
||||
|
||||
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/
|
||||
"""
|
||||
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.util import slugify
|
||||
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.8.3']
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
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):
|
||||
@ -23,63 +34,67 @@ def setup_scanner(hass, config, see):
|
||||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
|
||||
|
||||
# Get the username and password from the configuration.
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify a username and password')
|
||||
return False
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging into iCloud Account')
|
||||
# Attempt the login to iCloud
|
||||
api = PyiCloudService(username,
|
||||
password,
|
||||
verify=True)
|
||||
api = PyiCloudService(username, password, verify=True)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||
return False
|
||||
|
||||
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()
|
||||
_LOGGER.info("Authenticate against iCloud")
|
||||
|
||||
track_utc_time_change(hass, keep_alive, second=0)
|
||||
seen_devices = {}
|
||||
|
||||
def update_icloud(now):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
try:
|
||||
# The session timeouts if we are not using it so we
|
||||
# have to re-authenticate. This will send an email.
|
||||
api.authenticate()
|
||||
keep_alive(None)
|
||||
# Loop through every device registered with the iCloud account
|
||||
for device in api.devices:
|
||||
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()
|
||||
# If the device has a location add it. If not do nothing
|
||||
if location:
|
||||
see(
|
||||
dev_id=re.sub(r"(\s|\W|')",
|
||||
'',
|
||||
status['name']),
|
||||
dev_id=dev_id,
|
||||
host_name=status['name'],
|
||||
gps=(location['latitude'], location['longitude']),
|
||||
battery=status['batteryLevel']*100,
|
||||
gps_accuracy=location['horizontalAccuracy']
|
||||
)
|
||||
else:
|
||||
# No location found for the device so continue
|
||||
continue
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.info('No iCloud Devices found!')
|
||||
|
||||
track_utc_time_change(
|
||||
hass, update_icloud,
|
||||
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
|
||||
second=0
|
||||
)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
|
||||
|
||||
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
|
||||
# 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
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.6.7']
|
||||
REQUIREMENTS = ['netdisco==0.7.0']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
@ -30,6 +30,7 @@ SERVICE_HANDLERS = {
|
||||
'roku': ('media_player', 'roku'),
|
||||
'sonos': ('media_player', 'sonos'),
|
||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||
'directv': ('media_player', 'directv'),
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,7 +24,8 @@ ATTR_URL = "url"
|
||||
ATTR_SUBDIR = "subdir"
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
@ -1,37 +1,130 @@
|
||||
"""Handle the frontend for Home Assistant."""
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.components import api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from . import version, mdi_version
|
||||
from .version import FINGERPRINTS
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
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):
|
||||
"""Setup serving the frontend."""
|
||||
hass.wsgi.register_view(IndexView)
|
||||
hass.wsgi.register_view(BootstrapView)
|
||||
|
||||
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
if hass.wsgi.development:
|
||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||
else:
|
||||
sw_path = "service_worker.js"
|
||||
|
||||
hass.wsgi.register_static_path(
|
||||
"/service_worker.js",
|
||||
os.path.join(www_static_path, sw_path),
|
||||
0
|
||||
)
|
||||
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("/service_worker.js",
|
||||
os.path.join(STATIC_PATH, sw_path), 0)
|
||||
hass.wsgi.register_static_path("/robots.txt",
|
||||
os.path.join(STATIC_PATH, "robots.txt"))
|
||||
hass.wsgi.register_static_path("/static", STATIC_PATH)
|
||||
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
|
||||
|
||||
|
||||
@ -48,6 +141,7 @@ class BootstrapView(HomeAssistantView):
|
||||
'states': self.hass.states.all(),
|
||||
'events': api.events_json(self.hass),
|
||||
'services': api.services_json(self.hass),
|
||||
'panels': PANELS,
|
||||
})
|
||||
|
||||
|
||||
@ -57,16 +151,15 @@ class IndexView(HomeAssistantView):
|
||||
url = '/'
|
||||
name = "frontend:index"
|
||||
requires_auth = False
|
||||
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo', '/devTemplate',
|
||||
'/states', '/states/<entity:entity_id>']
|
||||
extra_urls = ['/states', '/states/<entity:entity_id>']
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass, extra_urls):
|
||||
"""Initialize the frontend view."""
|
||||
super().__init__(hass)
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
self.extra_urls = self.extra_urls + extra_urls
|
||||
self.templates = Environment(
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
@ -76,32 +169,32 @@ class IndexView(HomeAssistantView):
|
||||
def get(self, request, entity_id=None):
|
||||
"""Serve the index view."""
|
||||
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'
|
||||
map_url = ('/static/home-assistant-polymer/src/layouts/'
|
||||
'partial-map.html')
|
||||
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
|
||||
'dev-tools.html')
|
||||
else:
|
||||
core_url = '/static/core-{}.js'.format(version.CORE)
|
||||
ui_url = '/static/frontend-{}.html'.format(version.UI)
|
||||
map_url = '/static/partial-map-{}.html'.format(version.MAP)
|
||||
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
|
||||
core_url = '/static/core-{}.js'.format(
|
||||
FINGERPRINTS['core.js'])
|
||||
ui_url = '/static/frontend-{}.html'.format(
|
||||
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
|
||||
if self.hass.config.api.api_password is None:
|
||||
auth = 'true'
|
||||
else:
|
||||
auth = 'false'
|
||||
|
||||
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
|
||||
no_auth = 'false' if self.hass.config.api.api_password else 'true'
|
||||
|
||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||
template = self.templates.get_template('index.html')
|
||||
|
||||
# pylint is wrong
|
||||
# pylint: disable=no-member
|
||||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
|
||||
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
|
||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||
panel_url=panel_url)
|
||||
|
||||
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>
|
||||
|
||||
<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'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
|
||||
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
|
||||
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
|
||||
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
|
||||
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
|
||||
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
|
||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#ha-init-skeleton {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
@ -65,23 +72,23 @@
|
||||
.getElementById('ha-init-skeleton')
|
||||
.classList.add('error');
|
||||
};
|
||||
window.noAuth = {{ auth }};
|
||||
window.deferredLoading = {
|
||||
map: '{{ map_url }}',
|
||||
dev: '{{ dev_url }}',
|
||||
};
|
||||
window.noAuth = {{ no_auth }};
|
||||
window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
|
||||
</script>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<body>
|
||||
<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>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></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>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
@ -89,11 +96,11 @@
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var script = document.createElement('script')
|
||||
script.async = true
|
||||
script.onerror = initError;
|
||||
script.src = '/static/webcomponents-lite.min.js'
|
||||
document.head.appendChild(script)
|
||||
var e = document.createElement('script');
|
||||
e.async = true;
|
||||
e.onerror = initError;
|
||||
e.src = '/static/webcomponents-lite.min.js';
|
||||
document.head.appendChild(e);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
@ -1,5 +1,16 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
|
||||
UI = "805f8dda70419b26daabc8e8f625127f"
|
||||
MAP = "c922306de24140afd14f857f927bf8f0"
|
||||
DEV = "b7079ac3121b95b9856e5603a6d8a263"
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "bc78f21f5280217aa2c78dfc5848134f",
|
||||
"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",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/favicon-192x192.png",
|
||||
"src": "/static/icons/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-384x384.png",
|
||||
"src": "/static/icons/favicon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-512x512.png",
|
||||
"src": "/static/icons/favicon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-1024x1024.png",
|
||||
"src": "/static/icons/favicon-1024x1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"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.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):
|
||||
|
@ -44,7 +44,6 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
self._state = value.data
|
||||
dispatcher.connect(
|
||||
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."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self._state = value.data
|
||||
self.update_ha_state(True)
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
|
@ -12,8 +12,8 @@ import voluptuous as vol
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_UNKNOWN,
|
||||
ATTR_ASSUMED_STATE, )
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
|
||||
from homeassistant.helpers.entity import (
|
||||
Entity, generate_entity_id, split_entity_id)
|
||||
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
|
||||
_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):
|
||||
@ -304,8 +304,9 @@ class Group(Entity):
|
||||
if gr_on is None:
|
||||
return
|
||||
|
||||
if tr_state is None or (gr_state == gr_on and
|
||||
tr_state.state == gr_off):
|
||||
if tr_state is None or ((gr_state == gr_on and
|
||||
tr_state.state == gr_off) or
|
||||
tr_state.state not in (gr_on, gr_off)):
|
||||
if states is None:
|
||||
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
|
||||
https://home-assistant.io/components/history/
|
||||
"""
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, script
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'history'
|
||||
@ -19,9 +19,6 @@ DEPENDENCIES = ['recorder', 'http']
|
||||
SIGNIFICANT_DOMAINS = ('thermostat',)
|
||||
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):
|
||||
"""Return the last 5 states for entity_id."""
|
||||
@ -153,6 +150,7 @@ def setup(hass, config):
|
||||
"""Setup the history hooks."""
|
||||
hass.wsgi.register_view(Last5StatesView)
|
||||
hass.wsgi.register_view(HistoryPeriodView)
|
||||
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
||||
|
||||
return True
|
||||
|
||||
@ -173,14 +171,14 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
|
||||
url = '/api/history/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."""
|
||||
one_day = timedelta(days=1)
|
||||
|
||||
if date:
|
||||
start_time = dt_util.as_utc(dt_util.start_of_local_day(date))
|
||||
if datetime:
|
||||
start_time = dt_util.as_utc(datetime)
|
||||
else:
|
||||
start_time = dt_util.utcnow() - one_day
|
||||
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.9"]
|
||||
REQUIREMENTS = ["pyhomematic==0.1.10"]
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
|
@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
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_SERVER_HOST = "server_host"
|
||||
@ -216,9 +216,29 @@ def routing_map(hass):
|
||||
"""Convert date to url value."""
|
||||
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={
|
||||
'entity': EntityValidator,
|
||||
'date': DateValidator,
|
||||
'datetime': DateTimeValidator,
|
||||
})
|
||||
|
||||
|
||||
|
@ -98,9 +98,10 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""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_ha_state(True)
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
@ -135,7 +136,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
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)
|
||||
|
||||
@property
|
||||
@ -235,5 +236,5 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
value.data = int(swing_mode)
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
|
@ -33,6 +33,7 @@ CONF_PASSWORD = 'password'
|
||||
CONF_SSL = 'ssl'
|
||||
CONF_VERIFY_SSL = 'verify_ssl'
|
||||
CONF_BLACKLIST = 'blacklist'
|
||||
CONF_TAGS = 'tags'
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
@ -56,6 +57,7 @@ def setup(hass, config):
|
||||
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
||||
DEFAULT_VERIFY_SSL)
|
||||
blacklist = conf.get(CONF_BLACKLIST, [])
|
||||
tags = conf.get(CONF_TAGS, {})
|
||||
|
||||
try:
|
||||
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:
|
||||
influx.write_points(json_body)
|
||||
except exceptions.InfluxDBClientError:
|
||||
|
@ -34,7 +34,7 @@ SERVICE_SELECT_VALUE = 'select_value'
|
||||
|
||||
SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({
|
||||
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):
|
||||
"""Select new value."""
|
||||
num_value = int(value)
|
||||
num_value = float(value)
|
||||
if num_value < self._minimum or num_value > self._maximum:
|
||||
_LOGGER.warning('Invalid value: %s (range %s - %s)',
|
||||
num_value, self._minimum, self._maximum)
|
||||
|
@ -19,26 +19,19 @@ DOMAIN = 'joaoapps_join'
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_API_KEY): cv.string
|
||||
})
|
||||
}])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
"""Setup Join services."""
|
||||
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms,
|
||||
def register_device(hass, device_id, api_key, name):
|
||||
"""Method to register services for each join device listed."""
|
||||
from pyjoin import (ring_device, set_wallpaper, send_sms,
|
||||
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):
|
||||
"""Service to ring devices."""
|
||||
@ -69,7 +62,6 @@ def setup(hass, config):
|
||||
sms_text=service.data.get('message'),
|
||||
api_key=api_key)
|
||||
|
||||
name = name.lower().replace(" ", "_") + "_" if name else ""
|
||||
hass.services.register(DOMAIN, name + 'ring', ring_service)
|
||||
hass.services.register(DOMAIN, name + 'set_wallpaper',
|
||||
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_url', send_url_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
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = "knx"
|
||||
REQUIREMENTS = ['knxip==0.3.0']
|
||||
REQUIREMENTS = ['knxip==0.3.2']
|
||||
|
||||
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
|
||||
|
||||
@ -45,7 +45,12 @@ def setup(hass, config):
|
||||
|
||||
KNXTUNNEL = KNXIPTunnel(host, port)
|
||||
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:
|
||||
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
|
||||
KNXTUNNEL = None
|
||||
@ -74,7 +79,10 @@ class KNXConfig(object):
|
||||
|
||||
self.config = config
|
||||
self.should_poll = config.get("poll", True)
|
||||
self._address = parse_group_address(config.get("address"))
|
||||
if config.get("address"):
|
||||
self._address = parse_group_address(config.get("address"))
|
||||
else:
|
||||
self._address = None
|
||||
if self.config.get("state_address"):
|
||||
self._state_address = parse_group_address(
|
||||
self.config.get("state_address"))
|
||||
@ -198,7 +206,7 @@ class KNXGroupAddress(Entity):
|
||||
return False
|
||||
|
||||
|
||||
class KNXMultiAddressDevice(KNXGroupAddress):
|
||||
class KNXMultiAddressDevice(Entity):
|
||||
"""Representation of devices connected to a multiple KNX group address.
|
||||
|
||||
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
|
||||
|
||||
super().__init__(self, hass, config)
|
||||
|
||||
self.config = config
|
||||
self._config = config
|
||||
self._state = False
|
||||
self._data = None
|
||||
_LOGGER.debug("Initalizing KNX multi address device")
|
||||
|
||||
# parse required addresses
|
||||
for name in required:
|
||||
_LOGGER.info(name)
|
||||
paramname = name + "_address"
|
||||
addr = self._config.config.get(paramname)
|
||||
if addr is None:
|
||||
_LOGGER.exception("Required KNX group address %s missing",
|
||||
paramname)
|
||||
raise KNXException("Group address missing in configuration")
|
||||
raise KNXException("Group address for %s missing "
|
||||
"in configuration", paramname)
|
||||
addr = parse_group_address(addr)
|
||||
self.names[addr] = name
|
||||
|
||||
@ -244,23 +255,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
||||
_LOGGER.exception("Cannot parse group address %s", addr)
|
||||
self.names[addr] = name
|
||||
|
||||
def handle_frame(frame):
|
||||
"""Handle an incoming KNX frame.
|
||||
@property
|
||||
def name(self):
|
||||
"""The entity's display name."""
|
||||
return self._config.name
|
||||
|
||||
Handle an incoming frame and update our status if it contains
|
||||
information relating to this device.
|
||||
"""
|
||||
addr = frame.data[0]
|
||||
@property
|
||||
def config(self):
|
||||
"""The entity's configuration."""
|
||||
return self._config
|
||||
|
||||
if addr in self.names:
|
||||
self.values[addr] = frame.data[1]
|
||||
self.update_ha_state()
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the state of the polling, if needed."""
|
||||
return self._config.should_poll
|
||||
|
||||
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame)
|
||||
|
||||
def group_write_address(self, name, value):
|
||||
"""Write to the group address with the given name."""
|
||||
KNXTUNNEL.group_write(self.address, [value])
|
||||
@property
|
||||
def cache(self):
|
||||
"""The name given to the entity."""
|
||||
return self._config.config.get("cache", True)
|
||||
|
||||
def has_attribute(self, name):
|
||||
"""Check if the attribute with the given name is defined.
|
||||
@ -277,7 +290,7 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
||||
from knxip.core import KNXException
|
||||
|
||||
addr = None
|
||||
for attributename, attributeaddress in self.names.items():
|
||||
for attributeaddress, attributename in self.names.items():
|
||||
if attributename == name:
|
||||
addr = attributeaddress
|
||||
|
||||
@ -293,3 +306,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
|
||||
return False
|
||||
|
||||
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."""
|
||||
host = config.get(CONF_HOST, None)
|
||||
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():
|
||||
add_devices_callback([device])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class Hyperion(Light):
|
||||
"""Representation of a Hyperion remote."""
|
||||
|
||||
def __init__(self, name, host, port):
|
||||
def __init__(self, name, host, port, default_color):
|
||||
"""Initialize the light."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._name = name
|
||||
self._is_available = True
|
||||
self._rgb_color = [255, 255, 255]
|
||||
self._default_color = default_color
|
||||
self._rgb_color = [0, 0, 0]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -50,38 +50,43 @@ class Hyperion(Light):
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the device is online."""
|
||||
return self._is_available
|
||||
"""Return true if not black."""
|
||||
return self._rgb_color != [0, 0, 0]
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the lights on."""
|
||||
if self._is_available:
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
else:
|
||||
self._rgb_color = self._default_color
|
||||
|
||||
self.json_request({"command": "color", "priority": 128,
|
||||
"color": self._rgb_color})
|
||||
self.json_request({"command": "color", "priority": 128,
|
||||
"color": self._rgb_color})
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Disconnect the remote."""
|
||||
"""Disconnect all remotes."""
|
||||
self.json_request({"command": "clearall"})
|
||||
self._rgb_color = [0, 0, 0]
|
||||
|
||||
def update(self):
|
||||
"""Ping the remote."""
|
||||
# just see if the remote port is open
|
||||
self._is_available = self.json_request()
|
||||
"""Get the remote's active color."""
|
||||
response = self.json_request({"command": "serverinfo"})
|
||||
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):
|
||||
"""Get the hostname of the remote."""
|
||||
response = self.json_request({"command": "serverinfo"})
|
||||
if response:
|
||||
if self._name == self._host:
|
||||
self._name = response["info"]["hostname"]
|
||||
self._name = response["info"]["hostname"]
|
||||
return True
|
||||
|
||||
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."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
@ -92,11 +97,6 @@ class Hyperion(Light):
|
||||
sock.close()
|
||||
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"))
|
||||
try:
|
||||
buf = sock.recv(4096)
|
||||
|
@ -1,35 +1,21 @@
|
||||
"""
|
||||
Support for Qwikswitch Relays and Dimmers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.qwikswitch as qwikswitch
|
||||
from homeassistant.components.light import Light
|
||||
|
||||
DEPENDENCIES = ['qwikswitch']
|
||||
|
||||
|
||||
class QSLight(qwikswitch.QSToggleEntity, Light):
|
||||
"""Light based on a Qwikswitch relay/dimmer module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Store add_devices for the light components."""
|
||||
if discovery_info is None or 'qsusb_id' not in discovery_info:
|
||||
logging.getLogger(__name__).error(
|
||||
'Configure main Qwikswitch component')
|
||||
return False
|
||||
|
||||
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
|
||||
|
||||
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
|
||||
"""
|
||||
Support for Qwikswitch Relays and Dimmers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.qwikswitch as qwikswitch
|
||||
|
||||
DEPENDENCIES = ['qwikswitch']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Add lights from the main Qwikswitch component."""
|
||||
if discovery_info is None:
|
||||
logging.getLogger(__name__).error('Configure Qwikswitch Component.')
|
||||
return False
|
||||
|
||||
add_devices(qwikswitch.QSUSB['light'])
|
||||
return True
|
||||
|
@ -6,10 +6,8 @@ https://home-assistant.io/components/light.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||
from homeassistant.const import (
|
||||
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED,
|
||||
STATE_OFF, STATE_ON)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
@ -56,31 +54,6 @@ class VeraLight(VeraDevice, Light):
|
||||
self._state = STATE_OFF
|
||||
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
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.util import color as color_util
|
||||
from homeassistant.util.color import \
|
||||
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):
|
||||
|
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
|
||||
# pylint: disable=import-error
|
||||
from threading import Timer
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
ATTR_RGB_COLOR, DOMAIN, Light
|
||||
from homeassistant.components import zwave
|
||||
@ -107,25 +106,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
|
||||
def _value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id != value.value_id:
|
||||
return
|
||||
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
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
|
||||
def brightness(self):
|
||||
|
@ -8,7 +8,7 @@ import logging
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
|
||||
STATE_LOCKED, STATE_UNLOCKED)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
|
||||
@ -32,16 +32,6 @@ class VeraLock(VeraDevice, LockDevice):
|
||||
self._state = None
|
||||
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):
|
||||
"""Lock the device."""
|
||||
self.vera_device.lock()
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
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):
|
||||
|
@ -46,7 +46,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
|
||||
def _value_changed(self, value):
|
||||
"""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.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/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
|
||||
@ -14,6 +13,7 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, sun
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
@ -24,9 +24,7 @@ from homeassistant.helpers import template
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
|
||||
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
||||
DEPENDENCIES = ['recorder', 'frontend']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -75,6 +73,9 @@ def setup(hass, config):
|
||||
|
||||
hass.wsgi.register_view(LogbookView)
|
||||
|
||||
register_built_in_panel(hass, 'logbook', 'Logbook',
|
||||
'mdi:format-list-bulleted-type')
|
||||
|
||||
hass.services.register(DOMAIN, 'log', log_message,
|
||||
schema=LOG_MESSAGE_SCHEMA)
|
||||
return True
|
||||
@ -85,16 +86,11 @@ class LogbookView(HomeAssistantView):
|
||||
|
||||
url = '/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."""
|
||||
if date:
|
||||
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)
|
||||
start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day())
|
||||
end_day = start_day + timedelta(days=1)
|
||||
|
||||
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 logging
|
||||
import socket
|
||||
from os import path
|
||||
import socket
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
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_SELECT_SOURCE, MediaPlayerDevice)
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['SoCo==0.11.1']
|
||||
|
||||
@ -43,16 +46,28 @@ SUPPORT_SOURCE_LINEIN = 'Line-in'
|
||||
SUPPORT_SOURCE_TV = 'TV'
|
||||
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
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Sonos platform."""
|
||||
import soco
|
||||
global DEVICES
|
||||
|
||||
if discovery_info:
|
||||
player = soco.SoCo(discovery_info)
|
||||
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 False
|
||||
|
||||
@ -74,60 +89,72 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.warning('No Sonos speakers found.')
|
||||
return False
|
||||
|
||||
devices = [SonosDevice(hass, p) for p in players]
|
||||
add_devices(devices)
|
||||
DEVICES = [SonosDevice(hass, p) for p in players]
|
||||
add_devices(DEVICES)
|
||||
register_services(hass)
|
||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||
return True
|
||||
|
||||
def _apply_service(service, service_func, *service_func_args):
|
||||
"""Internal func for applying a service."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
service_func(device, *service_func_args)
|
||||
device.update_ha_state(True)
|
||||
|
||||
def group_players_service(service):
|
||||
"""Group media players, use player as coordinator."""
|
||||
_apply_service(service, SonosDevice.group_players)
|
||||
|
||||
def unjoin_service(service):
|
||||
"""Unjoin the player from a group."""
|
||||
_apply_service(service, SonosDevice.unjoin)
|
||||
|
||||
def snapshot_service(service):
|
||||
"""Take a snapshot."""
|
||||
_apply_service(service, SonosDevice.snapshot)
|
||||
|
||||
def restore_service(service):
|
||||
"""Restore a snapshot."""
|
||||
_apply_service(service, SonosDevice.restore)
|
||||
|
||||
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))
|
||||
_group_players_service,
|
||||
descriptions.get(SERVICE_GROUP_PLAYERS),
|
||||
schema=SONOS_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_UNJOIN,
|
||||
unjoin_service,
|
||||
descriptions.get(SERVICE_UNJOIN))
|
||||
_unjoin_service,
|
||||
descriptions.get(SERVICE_UNJOIN),
|
||||
schema=SONOS_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
|
||||
snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT))
|
||||
_snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT),
|
||||
schema=SONOS_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RESTORE,
|
||||
restore_service,
|
||||
descriptions.get(SERVICE_RESTORE))
|
||||
_restore_service,
|
||||
descriptions.get(SERVICE_RESTORE),
|
||||
schema=SONOS_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
def _apply_service(service, service_func, *service_func_args):
|
||||
"""Internal func for applying a service."""
|
||||
entity_ids = service.data.get('entity_id')
|
||||
|
||||
if entity_ids:
|
||||
_devices = [device for device in DEVICES
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
_devices = DEVICES
|
||||
|
||||
for device in _devices:
|
||||
service_func(device, *service_func_args)
|
||||
device.update_ha_state(True)
|
||||
|
||||
|
||||
def _group_players_service(service):
|
||||
"""Group media players, use player as coordinator."""
|
||||
_apply_service(service, SonosDevice.group_players)
|
||||
|
||||
|
||||
def _unjoin_service(service):
|
||||
"""Unjoin the player from a group."""
|
||||
_apply_service(service, SonosDevice.unjoin)
|
||||
|
||||
|
||||
def _snapshot_service(service):
|
||||
"""Take a snapshot."""
|
||||
_apply_service(service, SonosDevice.snapshot)
|
||||
|
||||
|
||||
def _restore_service(service):
|
||||
"""Restore a snapshot."""
|
||||
_apply_service(service, SonosDevice.restore)
|
||||
|
||||
|
||||
def only_if_coordinator(func):
|
||||
|
@ -22,7 +22,7 @@ def get_service(hass, config):
|
||||
"""Get the GNTP notification service."""
|
||||
if config.get('app_icon') is None:
|
||||
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()
|
||||
else:
|
||||
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)
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['sendgrid>=1.6.0,<1.7.0']
|
||||
REQUIREMENTS = ['sendgrid==3.0.7']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -24,27 +24,50 @@ def get_service(hass, config):
|
||||
api_key = config['api_key']
|
||||
sender = config['sender']
|
||||
recipient = config['recipient']
|
||||
|
||||
return SendgridNotificationService(api_key, sender, recipient)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
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):
|
||||
"""Initialize the service."""
|
||||
from sendgrid import SendGridAPIClient
|
||||
|
||||
self.api_key = api_key
|
||||
self.sender = sender
|
||||
self.recipient = recipient
|
||||
|
||||
from sendgrid import SendGridClient
|
||||
self._sg = SendGridClient(self.api_key)
|
||||
self._sg = SendGridAPIClient(apikey=self.api_key)
|
||||
|
||||
def send_message(self, message='', **kwargs):
|
||||
"""Send an email to a user via SendGrid."""
|
||||
subject = kwargs.get(ATTR_TITLE)
|
||||
|
||||
from sendgrid import Mail
|
||||
mail = Mail(from_email=self.sender, to=self.recipient,
|
||||
html=message, text=message, subject=subject)
|
||||
self._sg.send(mail)
|
||||
data = {
|
||||
"personalizations": [
|
||||
{
|
||||
"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.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['slacker==0.9.21']
|
||||
REQUIREMENTS = ['slacker==0.9.24']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -8,43 +8,69 @@ import io
|
||||
import logging
|
||||
import urllib
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
ATTR_TITLE, ATTR_DATA, BaseNotificationService)
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_NAME, ATTR_LOCATION,
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-telegram-bot==4.3.3']
|
||||
REQUIREMENTS = ['python-telegram-bot==5.0.0']
|
||||
|
||||
ATTR_PHOTO = "photo"
|
||||
ATTR_FILE = "file"
|
||||
ATTR_URL = "url"
|
||||
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):
|
||||
"""Get the Telegram notification service."""
|
||||
import telegram
|
||||
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_API_KEY, 'chat_id']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
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']
|
||||
_LOGGER.info("Telegram bot is '%s'.", username)
|
||||
except urllib.error.HTTPError:
|
||||
_LOGGER.error("Please check your access token.")
|
||||
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
|
||||
@ -64,7 +90,18 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
import telegram
|
||||
|
||||
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
|
||||
try:
|
||||
@ -74,41 +111,30 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
_LOGGER.exception("Error sending message.")
|
||||
return
|
||||
|
||||
def send_photo(self, data):
|
||||
"""Send a photo."""
|
||||
import telegram
|
||||
caption = data.pop(ATTR_CAPTION, None)
|
||||
|
||||
# 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:
|
||||
photo = load_data(**data)
|
||||
self.bot.sendPhoto(chat_id=self._chat_id,
|
||||
photo=photo, caption=caption)
|
||||
except telegram.error.TelegramError:
|
||||
_LOGGER.exception("Error sending photo.")
|
||||
return
|
||||
|
||||
try:
|
||||
for photo_data in photos:
|
||||
caption = photo_data.get(ATTR_CAPTION, None)
|
||||
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))
|
||||
|
||||
# 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,
|
||||
photo=file_id, caption=caption)
|
||||
|
||||
except (OSError, IOError, telegram.error.TelegramError,
|
||||
urllib.error.HTTPError):
|
||||
_LOGGER.exception("Error sending photo.")
|
||||
return
|
||||
# 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/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.discovery import load_platform
|
||||
import voluptuous as vol
|
||||
|
||||
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'
|
||||
'#pyqwikswitch==0.4']
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'qwikswitch'
|
||||
QSUSB = None
|
||||
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
|
||||
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):
|
||||
@ -42,6 +53,7 @@ class QSToggleEntity(object):
|
||||
self._value = qsitem[PQS_VALUE]
|
||||
self._qsusb = qsusb
|
||||
self._dim = qsitem[PQS_TYPE] == QSType.dimmer
|
||||
QSUSB[self._id] = self
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@ -87,51 +99,70 @@ class QSToggleEntity(object):
|
||||
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
|
||||
def setup(hass, config):
|
||||
"""Setup the QSUSB component."""
|
||||
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
|
||||
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
||||
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
|
||||
cmd_buttons = cmd_buttons.split(',')
|
||||
|
||||
try:
|
||||
url = config[DOMAIN].get('url', 'http://127.0.0.1:2020')
|
||||
dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1'))
|
||||
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
|
||||
url = config[DOMAIN]['url']
|
||||
dimmer_adjust = config[DOMAIN]['dimmer_adjust']
|
||||
|
||||
# Ensure qsusb terminates threads correctly
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda event: qsusb.stop())
|
||||
except ValueError as val_err:
|
||||
_LOGGER.error(str(val_err))
|
||||
return False
|
||||
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
|
||||
|
||||
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
|
||||
if QSUSB is None:
|
||||
def _stop(event):
|
||||
"""Stop the listener queue and clean up."""
|
||||
nonlocal qsusb
|
||||
qsusb.stop()
|
||||
qsusb = None
|
||||
global 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'):
|
||||
load_platform(hass, comp_name, 'qwikswitch',
|
||||
{'qsusb_id': id(qsusb)}, config)
|
||||
if len(QSUSB[comp_name]) > 0:
|
||||
load_platform(hass, comp_name, 'qwikswitch', {}, config)
|
||||
|
||||
def qs_callback(item):
|
||||
"""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 item.get(QS_CMD, '') in cmd_buttons:
|
||||
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
|
||||
@ -142,9 +173,13 @@ def setup(hass, config):
|
||||
if qsreply is False:
|
||||
return
|
||||
for item in qsreply:
|
||||
if item[QS_ID] in qsusb.ha_objects:
|
||||
qsusb.ha_objects[item[QS_ID]].update_value(
|
||||
if item[QS_ID] in QSUSB:
|
||||
QSUSB[item[QS_ID]].update_value(
|
||||
round(min(item[PQS_VALUE], 100) * 2.55))
|
||||
|
||||
qsusb.listen(callback=qs_callback, timeout=30)
|
||||
def _start(event):
|
||||
"""Start listening."""
|
||||
qsusb.listen(callback=qs_callback, timeout=30)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start)
|
||||
|
||||
return True
|
||||
|
@ -39,7 +39,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
||||
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)
|
||||
|
||||
@ -90,8 +91,12 @@ def run_information(point_in_time=None):
|
||||
def setup(hass, config):
|
||||
"""Setup the recorder."""
|
||||
# pylint: disable=global-statement
|
||||
# pylint: disable=too-many-locals
|
||||
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)
|
||||
|
||||
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:
|
||||
Session().rollback()
|
||||
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)
|
||||
|
||||
|
||||
@ -164,8 +169,6 @@ class Recorder(threading.Thread):
|
||||
from homeassistant.components.recorder.models import Events, States
|
||||
import sqlalchemy.exc
|
||||
|
||||
global _INSTANCE
|
||||
|
||||
while True:
|
||||
try:
|
||||
self._setup_connection()
|
||||
@ -176,8 +179,12 @@ class Recorder(threading.Thread):
|
||||
message="Error during connection setup: %s")
|
||||
|
||||
if self.purge_days is not None:
|
||||
track_point_in_utc_time(self.hass,
|
||||
lambda now: self._purge_old_data(),
|
||||
def purge_ticker(event):
|
||||
"""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))
|
||||
|
||||
while True:
|
||||
@ -186,42 +193,26 @@ class Recorder(threading.Thread):
|
||||
if event == self.quit_object:
|
||||
self._close_run()
|
||||
self._close_connection()
|
||||
# pylint: disable=global-statement
|
||||
global _INSTANCE
|
||||
_INSTANCE = None
|
||||
self.queue.task_done()
|
||||
return
|
||||
|
||||
elif event.event_type == EVENT_TIME_CHANGED:
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
self.queue.task_done()
|
||||
continue
|
||||
|
||||
session = Session()
|
||||
dbevent = Events.from_event(event)
|
||||
session.add(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)
|
||||
self._commit(dbevent)
|
||||
|
||||
if event.event_type != EVENT_STATE_CHANGED:
|
||||
self.queue.task_done()
|
||||
continue
|
||||
|
||||
session = Session()
|
||||
dbstate = States.from_event(event)
|
||||
|
||||
for _ in range(0, RETRIES):
|
||||
try:
|
||||
dbstate.event_id = dbevent.event_id
|
||||
session.add(dbstate)
|
||||
session.commit()
|
||||
break
|
||||
except sqlalchemy.exc.OperationalError as e:
|
||||
log_error(e, retry_wait=QUERY_RETRY_WAIT,
|
||||
rollback=True)
|
||||
dbstate.event_id = dbevent.event_id
|
||||
self._commit(dbstate)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
@ -268,6 +259,7 @@ class Recorder(threading.Thread):
|
||||
|
||||
def _close_connection(self):
|
||||
"""Close the connection."""
|
||||
# pylint: disable=global-statement
|
||||
global Session
|
||||
self.engine.dispose()
|
||||
self.engine = None
|
||||
@ -289,16 +281,12 @@ class Recorder(threading.Thread):
|
||||
start=self.recording_start,
|
||||
created=dt_util.utcnow()
|
||||
)
|
||||
session = Session()
|
||||
session.add(self._run)
|
||||
session.commit()
|
||||
self._commit(self._run)
|
||||
|
||||
def _close_run(self):
|
||||
"""Save end time for current run."""
|
||||
self._run.end = dt_util.utcnow()
|
||||
session = Session()
|
||||
session.add(self._run)
|
||||
session.commit()
|
||||
self._commit(self._run)
|
||||
self._run = None
|
||||
|
||||
def _purge_old_data(self):
|
||||
@ -312,17 +300,24 @@ class Recorder(threading.Thread):
|
||||
|
||||
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
|
||||
|
||||
_LOGGER.info("Purging events created before %s", purge_before)
|
||||
deleted_rows = Session().query(Events).filter(
|
||||
(Events.created < purge_before)).delete(synchronize_session=False)
|
||||
_LOGGER.debug("Deleted %s events", deleted_rows)
|
||||
def _purge_states(session):
|
||||
deleted_rows = session.query(States) \
|
||||
.filter((States.created < purge_before)) \
|
||||
.delete(synchronize_session=False)
|
||||
_LOGGER.debug("Deleted %s states", deleted_rows)
|
||||
|
||||
_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)
|
||||
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().commit()
|
||||
Session().expire_all()
|
||||
|
||||
# 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")
|
||||
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():
|
||||
"""Throw error if recorder not initialized."""
|
||||
|
@ -20,7 +20,7 @@ Base = declarative_base()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Events(Base):
|
||||
class Events(Base): # type: ignore
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Event history data."""
|
||||
|
||||
@ -55,7 +55,7 @@ class Events(Base):
|
||||
return None
|
||||
|
||||
|
||||
class States(Base):
|
||||
class States(Base): # type: ignore
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""State change history."""
|
||||
|
||||
@ -114,7 +114,7 @@ class States(Base):
|
||||
return None
|
||||
|
||||
|
||||
class RecorderRuns(Base):
|
||||
class RecorderRuns(Base): # type: ignore
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Representation of recorder run."""
|
||||
|
||||
|
@ -100,6 +100,7 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
|
||||
DEVICE_SCHEMA_SENSOR = vol.Schema({
|
||||
vol.Optional(ATTR_NAME, default=None): cv.string,
|
||||
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
|
||||
vol.Optional(ATTR_DATA_TYPE, default=[]):
|
||||
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.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):
|
||||
|
@ -49,14 +49,20 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state(True)
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""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):
|
||||
"""Move the roller shutter up."""
|
||||
|