Merge branch 'dev' of https://github.com/balloob/home-assistant into limitlessled
|
@ -17,6 +17,9 @@ omit =
|
|||
homeassistant/components/*/tellstick.py
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/verisure.py
|
||||
homeassistant/components/*/verisure.py
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ def _setup_component(hass, domain, config):
|
|||
return True
|
||||
component = loader.get_component(domain)
|
||||
|
||||
missing_deps = [dep for dep in component.DEPENDENCIES
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
if dep not in hass.config.components]
|
||||
|
||||
if missing_deps:
|
||||
|
@ -106,7 +106,7 @@ def _setup_component(hass, domain, config):
|
|||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if group.DOMAIN not in component.DEPENDENCIES:
|
||||
if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(
|
||||
|
@ -133,14 +133,13 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
|||
return platform
|
||||
|
||||
# Load dependencies
|
||||
if hasattr(platform, 'DEPENDENCIES'):
|
||||
for component in platform.DEPENDENCIES:
|
||||
if not setup_component(hass, component, config):
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
return None
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
if not setup_component(hass, component, config):
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
return None
|
||||
|
||||
if not _handle_requirements(hass, platform, platform_path):
|
||||
return None
|
||||
|
|
|
@ -15,7 +15,6 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
|
|
@ -18,8 +18,6 @@ from homeassistant.const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
|
|
|
@ -18,10 +18,10 @@ from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
|||
from homeassistant.const import (
|
||||
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
|
||||
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG,
|
||||
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
|
||||
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
|
||||
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, CONTENT_TYPE_TEXT_PLAIN)
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
|
||||
DOMAIN = 'api'
|
||||
|
@ -36,10 +36,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||
def setup(hass, config):
|
||||
""" Register the API with the HTTP interface. """
|
||||
|
||||
if 'http' not in hass.config.components:
|
||||
_LOGGER.error('Dependency http is not loaded')
|
||||
return False
|
||||
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
|
||||
|
@ -93,6 +89,8 @@ def setup(hass, config):
|
|||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||
_handle_get_api_error_log)
|
||||
|
||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -108,6 +106,7 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||
wfile = handler.wfile
|
||||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
session_id = None
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
|
@ -121,6 +120,7 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||
try:
|
||||
wfile.write(msg.encode("UTF-8"))
|
||||
wfile.flush()
|
||||
handler.server.sessions.extend_validation(session_id)
|
||||
except IOError:
|
||||
block.set()
|
||||
|
||||
|
@ -140,6 +140,7 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/event-stream')
|
||||
session_id = handler.set_session_cookie_header()
|
||||
handler.end_headers()
|
||||
|
||||
hass.bus.listen(MATCH_ALL, forward_events)
|
||||
|
@ -347,9 +348,15 @@ def _handle_get_api_components(handler, path_match, data):
|
|||
|
||||
def _handle_get_api_error_log(handler, path_match, data):
|
||||
""" Returns the logged errors for this session. """
|
||||
error_path = handler.server.hass.config.path(ERROR_LOG_FILENAME)
|
||||
with open(error_path, 'rb') as error_log:
|
||||
handler.write_file_pointer(CONTENT_TYPE_TEXT_PLAIN, error_log)
|
||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
||||
False)
|
||||
|
||||
|
||||
def _handle_post_api_log_out(handler, path_match, data):
|
||||
""" Log user out. """
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.destroy_session()
|
||||
handler.end_headers()
|
||||
|
||||
|
||||
def _services_json(hass):
|
||||
|
|
|
@ -19,7 +19,6 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
|||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
DOMAIN = "arduino"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['PyMata==2.07a']
|
||||
BOARD = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
|
@ -14,7 +14,6 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
|
|
@ -8,7 +8,6 @@ https://home-assistant.io/components/browser/
|
|||
"""
|
||||
|
||||
DOMAIN = "browser"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_BROWSE_URL = "browse_url"
|
||||
|
||||
|
|
|
@ -80,19 +80,21 @@ def setup(hass, config):
|
|||
def _proxy_camera_image(handler, path_match, data):
|
||||
""" Proxies the camera image via the HA server. """
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
camera = None
|
||||
if entity_id in component.entities.keys():
|
||||
camera = component.entities[entity_id]
|
||||
|
||||
if camera:
|
||||
response = camera.camera_image()
|
||||
if response is not None:
|
||||
handler.wfile.write(response)
|
||||
else:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
else:
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
response = camera.camera_image()
|
||||
|
||||
if response is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
handler.wfile.write(response)
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
|
@ -108,12 +110,9 @@ def setup(hass, config):
|
|||
stream even with only a still image URL available.
|
||||
"""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
camera = None
|
||||
if entity_id in component.entities.keys():
|
||||
camera = component.entities[entity_id]
|
||||
|
||||
if not camera:
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
@ -131,7 +130,6 @@ def setup(hass, config):
|
|||
# MJPEG_START_HEADER.format()
|
||||
|
||||
while True:
|
||||
|
||||
img_bytes = camera.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
|
@ -148,12 +146,12 @@ def setup(hass, config):
|
|||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
camera.update_ha_state()
|
||||
|
||||
camera.is_streaming = False
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(
|
||||
|
|
|
@ -4,8 +4,8 @@ homeassistant.components.camera.demo
|
|||
Demo platform that has a fake camera.
|
||||
"""
|
||||
import os
|
||||
from random import randint
|
||||
from homeassistant.components.camera import Camera
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -24,12 +24,12 @@ class DemoCamera(Camera):
|
|||
|
||||
def camera_image(self):
|
||||
""" Return a faked still image response. """
|
||||
now = dt_util.utcnow()
|
||||
|
||||
image_path = os.path.join(os.path.dirname(__file__),
|
||||
'demo_{}.png'.format(randint(1, 5)))
|
||||
'demo_{}.jpg'.format(now.second % 4))
|
||||
with open(image_path, 'rb') as file:
|
||||
output = file.read()
|
||||
return output
|
||||
return file.read()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
BIN
homeassistant/components/camera/demo_0.jpg
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
homeassistant/components/camera/demo_1.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 9.5 KiB |
BIN
homeassistant/components/camera/demo_2.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 9.4 KiB |
BIN
homeassistant/components/camera/demo_3.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 9.5 KiB |
|
@ -15,7 +15,6 @@ from homeassistant.helpers import generate_entity_id
|
|||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
|
||||
DOMAIN = "configurator"
|
||||
DEPENDENCIES = []
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
SERVICE_CONFIGURE = "configure"
|
||||
|
|
|
@ -14,7 +14,6 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
|
||||
DOMAIN = "conversation"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_PROCESS = "process"
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ class NmapDeviceScanner(object):
|
|||
from nmap import PortScanner, PortScannerError
|
||||
scanner = PortScanner()
|
||||
|
||||
options = "-F --host-timeout 5"
|
||||
options = "-F --host-timeout 5s"
|
||||
|
||||
if self.home_interval:
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
|
|
|
@ -17,8 +17,7 @@ from homeassistant.const import (
|
|||
ATTR_SERVICE, ATTR_DISCOVERED)
|
||||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['netdisco==0.5.1']
|
||||
REQUIREMENTS = ['netdisco==0.5.2']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ from homeassistant.helpers import validate_config
|
|||
from homeassistant.util import sanitize_filename
|
||||
|
||||
DOMAIN = "downloader"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_DOWNLOAD_FILE = "download_file"
|
||||
|
||||
|
|
154
homeassistant/components/ecobee.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
"""
|
||||
homeassistant.components.ecobee
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ecobee Component
|
||||
|
||||
This component adds support for Ecobee3 Wireless Thermostats.
|
||||
You will need to setup developer access to your thermostat,
|
||||
and create and API key on the ecobee website.
|
||||
|
||||
The first time you run this component you will see a configuration
|
||||
component card in Home Assistant. This card will contain a PIN code
|
||||
that you will need to use to authorize access to your thermostat. You
|
||||
can do this at https://www.ecobee.com/consumerportal/index.html
|
||||
Click My Apps, Add application, Enter Pin and click Authorize.
|
||||
|
||||
After authorizing the application click the button in the configuration
|
||||
card. Now your thermostat and sensors should shown in home-assistant.
|
||||
|
||||
You can use the optional hold_temp parameter to set whether or not holds
|
||||
are set indefintely or until the next scheduled event.
|
||||
|
||||
ecobee:
|
||||
api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf
|
||||
hold_temp: True
|
||||
|
||||
"""
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY)
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
DOMAIN = "ecobee"
|
||||
DISCOVER_THERMOSTAT = "ecobee.thermostat"
|
||||
DISCOVER_SENSORS = "ecobee.sensor"
|
||||
NETWORK = None
|
||||
HOLD_TEMP = 'hold_temp'
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-ecobee-api/archive/'
|
||||
'd35596b67c75451fa47001c493a15eebee195e93.zip#python-ecobee==0.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
_CONFIGURING = {}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
||||
|
||||
|
||||
def request_configuration(network, hass, config):
|
||||
""" Request configuration steps from the user. """
|
||||
configurator = get_component('configurator')
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING['ecobee'], "Failed to register, please try again.")
|
||||
|
||||
return
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def ecobee_configuration_callback(callback_data):
|
||||
""" Actions to do when our configuration callback is called. """
|
||||
network.request_tokens()
|
||||
network.update()
|
||||
setup_ecobee(hass, network, config)
|
||||
|
||||
_CONFIGURING['ecobee'] = configurator.request_config(
|
||||
hass, "Ecobee", ecobee_configuration_callback,
|
||||
description=(
|
||||
'Please authorize this app at https://www.ecobee.com/consumer'
|
||||
'portal/index.html with pin code: ' + network.pin),
|
||||
description_image="/static/images/config_ecobee_thermostat.png",
|
||||
submit_caption="I have authorized the app."
|
||||
)
|
||||
|
||||
|
||||
def setup_ecobee(hass, network, config):
|
||||
""" Setup ecobee thermostat """
|
||||
# If ecobee has a PIN then it needs to be configured.
|
||||
if network.pin is not None:
|
||||
request_configuration(network, hass, config)
|
||||
return
|
||||
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(_CONFIGURING.pop('ecobee'))
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, 'thermostat', config)
|
||||
bootstrap.setup_component(hass, 'sensor', config)
|
||||
|
||||
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
|
||||
|
||||
# Fire thermostat discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: DISCOVER_THERMOSTAT,
|
||||
ATTR_DISCOVERED: {'hold_temp': hold_temp}
|
||||
})
|
||||
|
||||
# Fire sensor discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: DISCOVER_SENSORS,
|
||||
ATTR_DISCOVERED: {}
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EcobeeData(object):
|
||||
""" Gets the latest data and update the states. """
|
||||
|
||||
def __init__(self, config_file):
|
||||
from pyecobee import Ecobee
|
||||
self.ecobee = Ecobee(config_file)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Get the latest data from pyecobee. """
|
||||
self.ecobee.update()
|
||||
_LOGGER.info("ecobee data updated successfully.")
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""
|
||||
Setup Ecobee.
|
||||
Will automatically load thermostat and sensor components to support
|
||||
devices discovered on the network.
|
||||
"""
|
||||
# pylint: disable=global-statement, import-error
|
||||
global NETWORK
|
||||
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
return
|
||||
|
||||
from pyecobee import config_from_file
|
||||
|
||||
# Create ecobee.conf if it doesn't exist
|
||||
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
|
||||
if config[DOMAIN].get(CONF_API_KEY) is None:
|
||||
_LOGGER.error("No ecobee api_key found in config.")
|
||||
return
|
||||
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
|
||||
config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
|
||||
|
||||
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))
|
||||
|
||||
setup_ecobee(hass, NETWORK.ecobee, config)
|
||||
|
||||
return True
|
|
@ -54,8 +54,7 @@ def setup(hass, config):
|
|||
|
||||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
|
||||
""" Renders the frontend. """
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
handler.end_headers()
|
||||
|
@ -66,7 +65,7 @@ def _handle_get_root(handler, path_match, data):
|
|||
app_url = "frontend-{}.html".format(version.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = ('no_password_set' if handler.server.no_password_set
|
||||
auth = ('no_password_set' if handler.server.api_password is None
|
||||
else data.get('api_password', ''))
|
||||
|
||||
with open(INDEX_PATH) as template_file:
|
||||
|
|
|
@ -4,16 +4,13 @@
|
|||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/static/manifest.json' />
|
||||
<link rel='shortcut icon' href='/static/favicon.ico' />
|
||||
<link rel='icon' type='image/png'
|
||||
href='/static/favicon-192x192.png' sizes='192x192'>
|
||||
<link rel='manifest' href='/static/manifest.json'>
|
||||
<link rel='icon' href='/static/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width,
|
||||
user-scalable=no' />
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<style>
|
||||
#init {
|
||||
|
@ -26,24 +23,17 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
#init div {
|
||||
line-height: 34px;
|
||||
margin-bottom: 89px;
|
||||
margin-bottom: 123px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<div id='init'>
|
||||
<img src='/static/splash.png' height='230' />
|
||||
<div>Initializing</div>
|
||||
</div>
|
||||
<div id='init'><img src='/static/favicon-192x192.png' height='192'></div>
|
||||
<script src='/static/webcomponents-lite.min.js'></script>
|
||||
<link rel='import' href='/static/{{ app_url }}' />
|
||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "dff74f773ea8b0356b0bd8130ed6f0cf"
|
||||
VERSION = "36df87bb6c219a2ee59adf416e3abdfa"
|
||||
|
|
BIN
homeassistant/components/frontend/www_static/favicon-384x384.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -1 +1 @@
|
|||
Subproject commit 39e09d85b74afb332ad2872b5aa556c9c9d113c3
|
||||
Subproject commit 33124030f6d119ad3a58cb520062f2aa58022c6d
|
After Width: | Height: | Size: 30 KiB |
|
@ -3,12 +3,17 @@
|
|||
"short_name": "Assistant",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#03A9F4",
|
||||
"icons": [
|
||||
{
|
||||
"src": "\/static\/favicon-192x192.png",
|
||||
"src": "/static/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
"type": "image/png",
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 51 KiB |
|
@ -17,7 +17,6 @@ from homeassistant.const import (
|
|||
STATE_UNKNOWN)
|
||||
|
||||
DOMAIN = "group"
|
||||
DEPENDENCIES = []
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
|
|
|
@ -12,10 +12,7 @@ import logging
|
|||
import time
|
||||
import gzip
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from datetime import timedelta
|
||||
from homeassistant.util import Throttle
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from http import cookies
|
||||
from socketserver import ThreadingMixIn
|
||||
|
@ -34,7 +31,6 @@ import homeassistant.util.dt as date_util
|
|||
import homeassistant.bootstrap as bootstrap
|
||||
|
||||
DOMAIN = "http"
|
||||
DEPENDENCIES = []
|
||||
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
|
@ -45,40 +41,30 @@ CONF_SESSIONS_ENABLED = "sessions_enabled"
|
|||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
# Throttling time in seconds for expired sessions check
|
||||
MIN_SEC_SESSION_CLEARING = timedelta(seconds=20)
|
||||
SESSION_CLEAR_INTERVAL = timedelta(seconds=20)
|
||||
SESSION_TIMEOUT_SECONDS = 1800
|
||||
SESSION_KEY = 'sessionId'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config=None):
|
||||
def setup(hass, config):
|
||||
""" Sets up the HTTP API and debug interface. """
|
||||
if config is None or DOMAIN not in config:
|
||||
config = {DOMAIN: {}}
|
||||
conf = config[DOMAIN]
|
||||
|
||||
api_password = util.convert(config[DOMAIN].get(CONF_API_PASSWORD), str)
|
||||
|
||||
no_password_set = api_password is None
|
||||
|
||||
if no_password_set:
|
||||
api_password = util.get_random_string()
|
||||
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
||||
|
||||
# If no server host is given, accept all incoming requests
|
||||
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
|
||||
server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT)
|
||||
|
||||
development = str(config[DOMAIN].get(CONF_DEVELOPMENT, "")) == "1"
|
||||
|
||||
sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True)
|
||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
||||
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
||||
|
||||
try:
|
||||
server = HomeAssistantHTTPServer(
|
||||
(server_host, server_port), RequestHandler, hass, api_password,
|
||||
development, no_password_set, sessions_enabled)
|
||||
development)
|
||||
except OSError:
|
||||
# Happens if address already in use
|
||||
# If address already in use
|
||||
_LOGGER.exception("Error setting up HTTP server")
|
||||
return False
|
||||
|
||||
|
@ -103,17 +89,15 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
|||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, server_address, request_handler_class,
|
||||
hass, api_password, development, no_password_set,
|
||||
sessions_enabled):
|
||||
hass, api_password, development):
|
||||
super().__init__(server_address, request_handler_class)
|
||||
|
||||
self.server_address = server_address
|
||||
self.hass = hass
|
||||
self.api_password = api_password
|
||||
self.development = development
|
||||
self.no_password_set = no_password_set
|
||||
self.paths = []
|
||||
self.sessions = SessionStore(sessions_enabled)
|
||||
self.sessions = SessionStore()
|
||||
|
||||
# We will lazy init this one if needed
|
||||
self.event_forwarder = None
|
||||
|
@ -162,12 +146,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
|
||||
def __init__(self, req, client_addr, server):
|
||||
""" Contructor, call the base constructor and set up session """
|
||||
self._session = None
|
||||
# Track if this was an authenticated request
|
||||
self.authenticated = False
|
||||
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
|
||||
|
||||
def log_message(self, fmt, *arguments):
|
||||
""" Redirect built-in log to HA logging """
|
||||
if self.server.no_password_set:
|
||||
if self.server.api_password is None:
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
|
@ -202,18 +187,17 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
self._session = self.get_session()
|
||||
if self.server.no_password_set:
|
||||
api_password = self.server.api_password
|
||||
else:
|
||||
if self.server.api_password is None:
|
||||
self.authenticated = True
|
||||
elif HTTP_HEADER_HA_AUTH in self.headers:
|
||||
api_password = self.headers.get(HTTP_HEADER_HA_AUTH)
|
||||
|
||||
if not api_password and DATA_API_PASSWORD in data:
|
||||
api_password = data[DATA_API_PASSWORD]
|
||||
|
||||
if not api_password and self._session is not None:
|
||||
api_password = self._session.cookie_values.get(
|
||||
CONF_API_PASSWORD)
|
||||
self.authenticated = api_password == self.server.api_password
|
||||
else:
|
||||
self.authenticated = self.verify_session()
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data.pop('_METHOD')
|
||||
|
@ -246,18 +230,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
|
||||
# Did we find a handler for the incoming request?
|
||||
if handle_request_method:
|
||||
|
||||
# For some calls we need a valid password
|
||||
if require_auth and api_password != self.server.api_password:
|
||||
if require_auth and not self.authenticated:
|
||||
self.write_json_message(
|
||||
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
|
||||
return
|
||||
|
||||
else:
|
||||
if self._session is None and require_auth:
|
||||
self._session = self.server.sessions.create(
|
||||
api_password)
|
||||
|
||||
handle_request_method(self, path_match, data)
|
||||
handle_request_method(self, path_match, data)
|
||||
|
||||
elif path_matched_but_not_method:
|
||||
self.send_response(HTTP_METHOD_NOT_ALLOWED)
|
||||
|
@ -308,18 +287,19 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode("UTF-8"))
|
||||
|
||||
def write_file(self, path):
|
||||
def write_file(self, path, cache_headers=True):
|
||||
""" Returns a file to the user. """
|
||||
try:
|
||||
with open(path, 'rb') as inp:
|
||||
self.write_file_pointer(self.guess_type(path), inp)
|
||||
self.write_file_pointer(self.guess_type(path), inp,
|
||||
cache_headers)
|
||||
|
||||
except IOError:
|
||||
self.send_response(HTTP_NOT_FOUND)
|
||||
self.end_headers()
|
||||
_LOGGER.exception("Unable to serve %s", path)
|
||||
|
||||
def write_file_pointer(self, content_type, inp):
|
||||
def write_file_pointer(self, content_type, inp, cache_headers=True):
|
||||
"""
|
||||
Helper function to write a file pointer to the user.
|
||||
Does not do error handling.
|
||||
|
@ -329,7 +309,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
self.send_response(HTTP_OK)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
||||
|
||||
self.set_cache_header()
|
||||
if cache_headers:
|
||||
self.set_cache_header()
|
||||
self.set_session_cookie_header()
|
||||
|
||||
if do_gzip:
|
||||
|
@ -356,75 +337,81 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
|
||||
def set_cache_header(self):
|
||||
""" Add cache headers if not in development """
|
||||
if not self.server.development:
|
||||
# 1 year in seconds
|
||||
cache_time = 365 * 86400
|
||||
if self.server.development:
|
||||
return
|
||||
|
||||
self.send_header(
|
||||
HTTP_HEADER_CACHE_CONTROL,
|
||||
"public, max-age={}".format(cache_time))
|
||||
self.send_header(
|
||||
HTTP_HEADER_EXPIRES,
|
||||
self.date_time_string(time.time()+cache_time))
|
||||
# 1 year in seconds
|
||||
cache_time = 365 * 86400
|
||||
|
||||
self.send_header(
|
||||
HTTP_HEADER_CACHE_CONTROL,
|
||||
"public, max-age={}".format(cache_time))
|
||||
self.send_header(
|
||||
HTTP_HEADER_EXPIRES,
|
||||
self.date_time_string(time.time()+cache_time))
|
||||
|
||||
def set_session_cookie_header(self):
|
||||
""" Add the header for the session cookie """
|
||||
if self.server.sessions.enabled and self._session is not None:
|
||||
existing_sess_id = self.get_current_session_id()
|
||||
""" Add the header for the session cookie and return session id. """
|
||||
if not self.authenticated:
|
||||
return
|
||||
|
||||
if existing_sess_id != self._session.session_id:
|
||||
self.send_header(
|
||||
'Set-Cookie',
|
||||
SESSION_KEY+'='+self._session.session_id)
|
||||
session_id = self.get_cookie_session_id()
|
||||
|
||||
def get_session(self):
|
||||
""" Get the requested session object from cookie value """
|
||||
if self.server.sessions.enabled is not True:
|
||||
return None
|
||||
|
||||
session_id = self.get_current_session_id()
|
||||
if session_id is not None:
|
||||
session = self.server.sessions.get(session_id)
|
||||
if session is not None:
|
||||
session.reset_expiry()
|
||||
return session
|
||||
self.server.sessions.extend_validation(session_id)
|
||||
return
|
||||
|
||||
return None
|
||||
self.send_header(
|
||||
'Set-Cookie',
|
||||
'{}={}'.format(SESSION_KEY, self.server.sessions.create())
|
||||
)
|
||||
|
||||
def get_current_session_id(self):
|
||||
return session_id
|
||||
|
||||
def verify_session(self):
|
||||
""" Verify that we are in a valid session. """
|
||||
return self.get_cookie_session_id() is not None
|
||||
|
||||
def get_cookie_session_id(self):
|
||||
"""
|
||||
Extracts the current session id from the
|
||||
cookie or returns None if not set
|
||||
cookie or returns None if not set or invalid
|
||||
"""
|
||||
if 'Cookie' not in self.headers:
|
||||
return None
|
||||
|
||||
cookie = cookies.SimpleCookie()
|
||||
try:
|
||||
cookie.load(self.headers["Cookie"])
|
||||
except cookies.CookieError:
|
||||
return None
|
||||
|
||||
if self.headers.get('Cookie', None) is not None:
|
||||
cookie.load(self.headers.get("Cookie"))
|
||||
morsel = cookie.get(SESSION_KEY)
|
||||
|
||||
if cookie.get(SESSION_KEY, False):
|
||||
return cookie[SESSION_KEY].value
|
||||
if morsel is None:
|
||||
return None
|
||||
|
||||
session_id = cookie[SESSION_KEY].value
|
||||
|
||||
if self.server.sessions.is_valid(session_id):
|
||||
return session_id
|
||||
|
||||
return None
|
||||
|
||||
def destroy_session(self):
|
||||
""" Destroys session. """
|
||||
session_id = self.get_cookie_session_id()
|
||||
|
||||
class ServerSession:
|
||||
""" A very simple session class """
|
||||
def __init__(self, session_id):
|
||||
""" Set up the expiry time on creation """
|
||||
self._expiry = 0
|
||||
self.reset_expiry()
|
||||
self.cookie_values = {}
|
||||
self.session_id = session_id
|
||||
if session_id is None:
|
||||
return
|
||||
|
||||
def reset_expiry(self):
|
||||
""" Resets the expiry based on current time """
|
||||
self._expiry = date_util.utcnow() + timedelta(
|
||||
seconds=SESSION_TIMEOUT_SECONDS)
|
||||
self.send_header('Set-Cookie', '')
|
||||
self.server.sessions.destroy(session_id)
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
""" Return true if the session is expired based on the expiry time """
|
||||
return self._expiry < date_util.utcnow()
|
||||
|
||||
def session_valid_time():
|
||||
""" Time till when a session will be valid. """
|
||||
return date_util.utcnow() + timedelta(seconds=SESSION_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
class SessionStore(object):
|
||||
|
@ -432,47 +419,42 @@ class SessionStore(object):
|
|||
def __init__(self, enabled=True):
|
||||
""" Set up the session store """
|
||||
self._sessions = {}
|
||||
self.enabled = enabled
|
||||
self.session_lock = threading.RLock()
|
||||
self.lock = threading.RLock()
|
||||
|
||||
@Throttle(MIN_SEC_SESSION_CLEARING)
|
||||
def remove_expired(self):
|
||||
@util.Throttle(SESSION_CLEAR_INTERVAL)
|
||||
def _remove_expired(self):
|
||||
""" Remove any expired sessions. """
|
||||
if self.session_lock.acquire(False):
|
||||
try:
|
||||
keys = []
|
||||
for key in self._sessions.keys():
|
||||
keys.append(key)
|
||||
now = date_util.utcnow()
|
||||
for key in [key for key, valid_time in self._sessions.items()
|
||||
if valid_time < now]:
|
||||
self._sessions.pop(key)
|
||||
|
||||
for key in keys:
|
||||
if self._sessions[key].is_expired:
|
||||
del self._sessions[key]
|
||||
_LOGGER.info("Cleared expired session %s", key)
|
||||
finally:
|
||||
self.session_lock.release()
|
||||
def is_valid(self, key):
|
||||
""" Return True if a valid session is given. """
|
||||
with self.lock:
|
||||
self._remove_expired()
|
||||
|
||||
def add(self, key, session):
|
||||
""" Add a new session to the list of tracked sessions """
|
||||
self.remove_expired()
|
||||
with self.session_lock:
|
||||
self._sessions[key] = session
|
||||
return (key in self._sessions and
|
||||
self._sessions[key] > date_util.utcnow())
|
||||
|
||||
def get(self, key):
|
||||
""" get a session by key """
|
||||
self.remove_expired()
|
||||
session = self._sessions.get(key, None)
|
||||
if session is not None and session.is_expired:
|
||||
return None
|
||||
return session
|
||||
def extend_validation(self, key):
|
||||
""" Extend a session validation time. """
|
||||
with self.lock:
|
||||
self._sessions[key] = session_valid_time()
|
||||
|
||||
def create(self, api_password):
|
||||
""" Creates a new session and adds it to the sessions """
|
||||
if self.enabled is not True:
|
||||
return None
|
||||
def destroy(self, key):
|
||||
""" Destroy a session by key. """
|
||||
with self.lock:
|
||||
self._sessions.pop(key, None)
|
||||
|
||||
chars = string.ascii_letters + string.digits
|
||||
session_id = ''.join([random.choice(chars) for i in range(20)])
|
||||
session = ServerSession(session_id)
|
||||
session.cookie_values[CONF_API_PASSWORD] = api_password
|
||||
self.add(session_id, session)
|
||||
return session
|
||||
def create(self):
|
||||
""" Creates a new session. """
|
||||
with self.lock:
|
||||
session_id = util.get_random_string(20)
|
||||
|
||||
while session_id in self._sessions:
|
||||
session_id = util.get_random_string(20)
|
||||
|
||||
self._sessions[session_id] = session_valid_time()
|
||||
|
||||
return session_id
|
||||
|
|
|
@ -22,8 +22,6 @@ ATTR_VALUE1 = 'value1'
|
|||
ATTR_VALUE2 = 'value2'
|
||||
ATTR_VALUE3 = 'value3'
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
REQUIREMENTS = ['pyfttt==0.3']
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ https://home-assistant.io/components/introduction/
|
|||
import logging
|
||||
|
||||
DOMAIN = 'introduction'
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup(hass, config=None):
|
||||
|
|
|
@ -20,7 +20,6 @@ from homeassistant.const import (
|
|||
ATTR_FRIENDLY_NAME)
|
||||
|
||||
DOMAIN = "isy994"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['PyISY==1.0.5']
|
||||
DISCOVER_LIGHTS = "isy994.lights"
|
||||
DISCOVER_SWITCHES = "isy994.switches"
|
||||
|
|
|
@ -15,7 +15,6 @@ from homeassistant.const import (
|
|||
|
||||
|
||||
DOMAIN = "keyboard"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['pyuserinput==0.1.9']
|
||||
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import homeassistant.util.color as color_util
|
|||
|
||||
|
||||
DOMAIN = "light"
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
GROUP_NAME_ALL_LIGHTS = 'all lights'
|
||||
|
|
|
@ -14,7 +14,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
|
||||
REQUIREMENTS = ["blinkstick==1.1.7"]
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
|
|
@ -8,7 +8,6 @@ https://home-assistant.io/components/light.mqtt/
|
|||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.color as color_util
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (Light,
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
|
||||
|
@ -37,45 +36,40 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
add_devices_callback([MqttLight(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
{"state_topic": config.get('state_topic'),
|
||||
"command_topic": config.get('command_topic'),
|
||||
"brightness_state_topic": config.get('brightness_state_topic'),
|
||||
"brightness_command_topic":
|
||||
config.get('brightness_command_topic'),
|
||||
"rgb_state_topic": config.get('rgb_state_topic'),
|
||||
"rgb_command_topic": config.get('rgb_command_topic')},
|
||||
config.get('rgb', None),
|
||||
{
|
||||
"state_topic": config.get('state_topic'),
|
||||
"command_topic": config.get('command_topic'),
|
||||
"brightness_state_topic": config.get('brightness_state_topic'),
|
||||
"brightness_command_topic": config.get('brightness_command_topic'),
|
||||
"rgb_state_topic": config.get('rgb_state_topic'),
|
||||
"rgb_command_topic": config.get('rgb_command_topic')
|
||||
},
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
{"on": config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
"off": config.get('payload_off', DEFAULT_PAYLOAD_OFF)},
|
||||
config.get('brightness'),
|
||||
{
|
||||
"on": config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
"off": config.get('payload_off', DEFAULT_PAYLOAD_OFF)
|
||||
},
|
||||
config.get('optimistic', DEFAULT_OPTIMISTIC))])
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
|
||||
class MqttLight(Light):
|
||||
""" Provides a MQTT light. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name,
|
||||
topic,
|
||||
rgb, qos,
|
||||
payload,
|
||||
brightness, optimistic):
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
def __init__(self, hass, name, topic, qos, payload, optimistic):
|
||||
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._rgb = rgb
|
||||
self._qos = qos
|
||||
self._payload = payload
|
||||
self._brightness = brightness
|
||||
self._optimistic = optimistic
|
||||
self._optimistic = optimistic or topic["state_topic"] is None
|
||||
self._optimistic_rgb = optimistic or topic["rgb_state_topic"] is None
|
||||
self._optimistic_brightness = (optimistic or
|
||||
topic["brightness_state_topic"] is None)
|
||||
self._state = False
|
||||
self._xy = None
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
def state_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
if payload == self._payload["on"]:
|
||||
self._state = True
|
||||
|
@ -84,27 +78,15 @@ class MqttLight(Light):
|
|||
|
||||
self.update_ha_state()
|
||||
|
||||
if self._topic["state_topic"] is None:
|
||||
# force optimistic mode
|
||||
self._optimistic = True
|
||||
else:
|
||||
# Subscribe the state_topic
|
||||
if self._topic["state_topic"] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic["state_topic"],
|
||||
message_received, self._qos)
|
||||
state_received, self._qos)
|
||||
|
||||
def brightness_received(topic, payload, qos):
|
||||
""" A new MQTT message for the brightness has been received. """
|
||||
self._brightness = int(payload)
|
||||
self.update_ha_state()
|
||||
|
||||
def rgb_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
self._rgb = [int(val) for val in payload.split(',')]
|
||||
self._xy = color_util.color_RGB_to_xy(int(self._rgb[0]),
|
||||
int(self._rgb[1]),
|
||||
int(self._rgb[2]))
|
||||
self.update_ha_state()
|
||||
|
||||
if self._topic["brightness_state_topic"] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic["brightness_state_topic"],
|
||||
brightness_received, self._qos)
|
||||
|
@ -112,12 +94,17 @@ class MqttLight(Light):
|
|||
else:
|
||||
self._brightness = None
|
||||
|
||||
def rgb_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
self._rgb = [int(val) for val in payload.split(',')]
|
||||
self.update_ha_state()
|
||||
|
||||
if self._topic["rgb_state_topic"] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic["rgb_state_topic"],
|
||||
rgb_received, self._qos)
|
||||
self._xy = [0, 0]
|
||||
self._rgb = [255, 255, 255]
|
||||
else:
|
||||
self._xy = None
|
||||
self._rgb = None
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
|
@ -129,11 +116,6 @@ class MqttLight(Light):
|
|||
""" RGB color value. """
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_xy(self):
|
||||
""" RGB color value. """
|
||||
return self._xy
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a MQTT light. """
|
||||
|
@ -151,19 +133,25 @@ class MqttLight(Light):
|
|||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
should_update = False
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs and \
|
||||
self._topic["rgb_command_topic"] is not None:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
rgb = DEFAULT_RGB_PATTERN % tuple(self._rgb)
|
||||
mqtt.publish(self._hass, self._topic["rgb_command_topic"],
|
||||
rgb, self._qos)
|
||||
|
||||
if self._optimistic_rgb:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
should_update = True
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and \
|
||||
self._topic["brightness_command_topic"] is not None:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
mqtt.publish(self._hass, self._topic["brightness_command_topic"],
|
||||
self._brightness, self._qos)
|
||||
if self._optimistic_brightness:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
should_update = True
|
||||
|
||||
mqtt.publish(self._hass, self._topic["command_topic"],
|
||||
self._payload["on"], self._qos)
|
||||
|
@ -171,6 +159,9 @@ class MqttLight(Light):
|
|||
if self._optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._state = True
|
||||
should_update = True
|
||||
|
||||
if should_update:
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
|
|
|
@ -10,6 +10,7 @@ from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
|||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
SIGNAL_REPETITIONS = 1
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -21,13 +22,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
import tellcore.constants as tellcore_constants
|
||||
|
||||
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||
signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS)
|
||||
|
||||
switches_and_lights = core.devices()
|
||||
lights = []
|
||||
|
||||
for switch in switches_and_lights:
|
||||
if switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||
lights.append(TellstickLight(switch))
|
||||
lights.append(TellstickLight(switch, signal_repetitions))
|
||||
|
||||
def _device_event_callback(id_, method, data, cid):
|
||||
""" Called from the TelldusCore library to update one device """
|
||||
|
@ -52,11 +54,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
class TellstickLight(Light):
|
||||
""" Represents a Tellstick light. """
|
||||
|
||||
def __init__(self, tellstick_device):
|
||||
def __init__(self, tellstick_device, signal_repetitions):
|
||||
import tellcore.constants as tellcore_constants
|
||||
|
||||
self.tellstick_device = tellstick_device
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||
self.signal_repetitions = signal_repetitions
|
||||
self._brightness = 0
|
||||
|
||||
self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
||||
|
@ -64,6 +67,7 @@ class TellstickLight(Light):
|
|||
tellcore_constants.TELLSTICK_DIM |
|
||||
tellcore_constants.TELLSTICK_UP |
|
||||
tellcore_constants.TELLSTICK_DOWN)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -82,7 +86,8 @@ class TellstickLight(Light):
|
|||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick_device.turn_off()
|
||||
for _ in range(self.signal_repetitions):
|
||||
self.tellstick_device.turn_off()
|
||||
self._brightness = 0
|
||||
self.update_ha_state()
|
||||
|
||||
|
@ -95,7 +100,8 @@ class TellstickLight(Light):
|
|||
else:
|
||||
self._brightness = brightness
|
||||
|
||||
self.tellstick_device.dim(self._brightness)
|
||||
for _ in range(self.signal_repetitions):
|
||||
self.tellstick_device.dim(self._brightness)
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
|
|
|
@ -20,7 +20,6 @@ from homeassistant.const import (
|
|||
from homeassistant.components import (group, wink)
|
||||
|
||||
DOMAIN = 'lock'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
GROUP_NAME_ALL_LOCKS = 'all locks'
|
||||
|
|
|
@ -10,7 +10,6 @@ import logging
|
|||
from collections import OrderedDict
|
||||
|
||||
DOMAIN = 'logger'
|
||||
DEPENDENCIES = []
|
||||
|
||||
LOGSEVERITY = {
|
||||
'CRITICAL': 50,
|
||||
|
|
|
@ -22,7 +22,6 @@ from homeassistant.const import (
|
|||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 10
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
|
|
@ -13,7 +13,6 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
|||
|
||||
DOMAIN = "modbus"
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/'
|
||||
'd7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0']
|
||||
|
||||
|
|
143
homeassistant/components/motor/__init__.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
"""
|
||||
homeassistant.components.motor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Motor component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/motor/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import group
|
||||
from homeassistant.const import (
|
||||
SERVICE_OPEN, SERVICE_CLOSE, SERVICE_STOP,
|
||||
STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID)
|
||||
|
||||
|
||||
DOMAIN = 'motor'
|
||||
SCAN_INTERVAL = 15
|
||||
|
||||
GROUP_NAME_ALL_MOTORS = 'all motors'
|
||||
ENTITY_ID_ALL_MOTORS = group.ENTITY_ID_FORMAT.format('all_motors')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CURRENT_POSITION = 'current_position'
|
||||
|
||||
|
||||
def is_open(hass, entity_id=None):
|
||||
""" Returns if the motor is open based on the statemachine. """
|
||||
entity_id = entity_id or ENTITY_ID_ALL_MOTORS
|
||||
return hass.states.is_state(entity_id, STATE_OPEN)
|
||||
|
||||
|
||||
def call_open(hass, entity_id=None):
|
||||
""" Open all or specified motor. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN, data)
|
||||
|
||||
|
||||
def call_close(hass, entity_id=None):
|
||||
""" Close all or specified motor. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE, data)
|
||||
|
||||
|
||||
def call_stop(hass, entity_id=None):
|
||||
""" Stops all or specified motor. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_STOP, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for motors. """
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_MOTORS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_motor_service(service):
|
||||
""" Handles calls to the motor services. """
|
||||
target_motors = component.extract_from_service(service)
|
||||
|
||||
for motor in target_motors:
|
||||
if service.service == SERVICE_OPEN:
|
||||
motor.open()
|
||||
elif service.service == SERVICE_CLOSE:
|
||||
motor.close()
|
||||
elif service.service == SERVICE_STOP:
|
||||
motor.stop()
|
||||
|
||||
if motor.should_poll:
|
||||
motor.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_OPEN,
|
||||
handle_motor_service,
|
||||
descriptions.get(SERVICE_OPEN))
|
||||
hass.services.register(DOMAIN, SERVICE_CLOSE,
|
||||
handle_motor_service,
|
||||
descriptions.get(SERVICE_CLOSE))
|
||||
hass.services.register(DOMAIN, SERVICE_STOP,
|
||||
handle_motor_service,
|
||||
descriptions.get(SERVICE_STOP))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MotorDevice(Entity):
|
||||
""" Represents a motor within Home Assistant. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""
|
||||
Return current position of motor.
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the motor. """
|
||||
current = self.current_position
|
||||
|
||||
if current is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return STATE_CLOSED if current == 0 else STATE_OPEN
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
current = self.current_position
|
||||
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_CURRENT_POSITION: current
|
||||
}
|
||||
|
||||
def open(self, **kwargs):
|
||||
""" Open the motor. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def close(self, **kwargs):
|
||||
""" Close the motor. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop(self, **kwargs):
|
||||
""" Stop the motor. """
|
||||
raise NotImplementedError()
|
104
homeassistant/components/motor/mqtt.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
homeassistant.components.motor.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a MQTT motor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/motor.mqtt/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.motor import MotorDevice
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
DEFAULT_NAME = "MQTT Motor"
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_PAYLOAD_OPEN = "OPEN"
|
||||
DEFAULT_PAYLOAD_CLOSE = "CLOSE"
|
||||
DEFAULT_PAYLOAD_STOP = "STOP"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add MQTT Motor """
|
||||
|
||||
if config.get('command_topic') is None:
|
||||
_LOGGER.error("Missing required variable: command_topic")
|
||||
return False
|
||||
|
||||
add_devices_callback([MqttMotor(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('state_topic'),
|
||||
config.get('command_topic'),
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
config.get('payload_open', DEFAULT_PAYLOAD_OPEN),
|
||||
config.get('payload_close', DEFAULT_PAYLOAD_CLOSE),
|
||||
config.get('payload_stop', DEFAULT_PAYLOAD_STOP),
|
||||
config.get('state_format'))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttMotor(MotorDevice):
|
||||
""" Represents a motor that can be controlled using MQTT. """
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_open, payload_close, payload_stop, state_format):
|
||||
self._state = None
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_open = payload_open
|
||||
self._payload_close = payload_close
|
||||
self._payload_stop = payload_stop
|
||||
self._parse = mqtt.FmtParser(state_format)
|
||||
|
||||
if self._state_topic is None:
|
||||
return
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
value = self._parse(payload)
|
||||
if value.isnumeric() and 0 <= int(value) <= 100:
|
||||
self._state = int(value)
|
||||
self.update_ha_state()
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is expected to be an integer between 0 and 100")
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the motor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""
|
||||
Return current position of motor.
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
def open(self, **kwargs):
|
||||
""" Open the device. """
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_open,
|
||||
self._qos)
|
||||
|
||||
def close(self, **kwargs):
|
||||
""" Close the device. """
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_close,
|
||||
self._qos)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
""" Stop the device. """
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_stop,
|
||||
self._qos)
|
0
homeassistant/components/motor/services.yaml
Normal file
|
@ -28,11 +28,11 @@ MQTT_CLIENT = None
|
|||
DEFAULT_PORT = 1883
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_RETAIN = False
|
||||
|
||||
SERVICE_PUBLISH = 'publish'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['paho-mqtt==1.1', 'jsonpath-rw==1.4.0']
|
||||
|
||||
CONF_BROKER = 'broker'
|
||||
|
@ -46,11 +46,12 @@ CONF_CERTIFICATE = 'certificate'
|
|||
ATTR_TOPIC = 'topic'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_QOS = 'qos'
|
||||
ATTR_RETAIN = 'retain'
|
||||
|
||||
MAX_RECONNECT_WAIT = 300 # seconds
|
||||
|
||||
|
||||
def publish(hass, topic, payload, qos=None):
|
||||
def publish(hass, topic, payload, qos=None, retain=None):
|
||||
""" Send an MQTT message. """
|
||||
data = {
|
||||
ATTR_TOPIC: topic,
|
||||
|
@ -58,6 +59,10 @@ def publish(hass, topic, payload, qos=None):
|
|||
}
|
||||
if qos is not None:
|
||||
data[ATTR_QOS] = qos
|
||||
|
||||
if retain is not None:
|
||||
data[ATTR_RETAIN] = retain
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||
|
||||
|
||||
|
@ -119,9 +124,10 @@ def setup(hass, config):
|
|||
msg_topic = call.data.get(ATTR_TOPIC)
|
||||
payload = call.data.get(ATTR_PAYLOAD)
|
||||
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
|
||||
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN)
|
||||
if msg_topic is None or payload is None:
|
||||
return
|
||||
MQTT_CLIENT.publish(msg_topic, payload, qos)
|
||||
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
|
||||
|
||||
|
@ -132,7 +138,7 @@ def setup(hass, config):
|
|||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class _JsonFmtParser(object):
|
||||
""" Implements a json parser on xpath. """
|
||||
""" Implements a JSON parser on xpath. """
|
||||
def __init__(self, jsonpath):
|
||||
import jsonpath_rw
|
||||
self._expr = jsonpath_rw.parse(jsonpath)
|
||||
|
@ -190,9 +196,9 @@ class MQTT(object):
|
|||
|
||||
self._mqttc.connect(broker, port, keepalive)
|
||||
|
||||
def publish(self, topic, payload, qos):
|
||||
def publish(self, topic, payload, qos, retain):
|
||||
""" Publish a MQTT message. """
|
||||
self._mqttc.publish(topic, payload, qos)
|
||||
self._mqttc.publish(topic, payload, qos, retain)
|
||||
|
||||
def start(self):
|
||||
""" Run the MQTT client. """
|
||||
|
|
|
@ -17,7 +17,6 @@ from homeassistant.helpers import config_per_platform
|
|||
from homeassistant.const import CONF_NAME
|
||||
|
||||
DOMAIN = "notify"
|
||||
DEPENDENCIES = []
|
||||
|
||||
# Title of notification
|
||||
ATTR_TITLE = "title"
|
||||
|
|
|
@ -100,7 +100,7 @@ class PushBulletNotificationService(BaseNotificationService):
|
|||
# This also seems works to send to all devices in own account
|
||||
if ttype == 'email':
|
||||
self.pushbullet.push_note(title, message, email=tname)
|
||||
_LOGGER.info('Sent notification to self')
|
||||
_LOGGER.info('Sent notification to email %s', tname)
|
||||
continue
|
||||
|
||||
# Refresh if name not found. While awaiting periodic refresh
|
||||
|
@ -108,18 +108,21 @@ class PushBulletNotificationService(BaseNotificationService):
|
|||
if ttype not in self.pbtargets:
|
||||
_LOGGER.error('Invalid target syntax: %s', target)
|
||||
continue
|
||||
if tname.lower() not in self.pbtargets[ttype] and not refreshed:
|
||||
|
||||
tname = tname.lower()
|
||||
|
||||
if tname not in self.pbtargets[ttype] and not refreshed:
|
||||
self.refresh()
|
||||
refreshed = True
|
||||
|
||||
# Attempt push_note on a dict value. Keys are types & target
|
||||
# name. Dict pbtargets has all *actual* targets.
|
||||
try:
|
||||
self.pbtargets[ttype][tname.lower()].push_note(title, message)
|
||||
self.pbtargets[ttype][tname].push_note(title, message)
|
||||
_LOGGER.info('Sent notification to %s/%s', ttype, tname)
|
||||
except KeyError:
|
||||
_LOGGER.error('No such target: %s/%s', ttype, tname)
|
||||
continue
|
||||
except self.pushbullet.errors.PushError:
|
||||
_LOGGER.error('Notify failed to: %s/%s', ttype, tname)
|
||||
continue
|
||||
_LOGGER.info('Sent notification to %s/%s', ttype, tname)
|
||||
|
|
|
@ -23,7 +23,6 @@ from homeassistant.const import (
|
|||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
DOMAIN = "recorder"
|
||||
DEPENDENCIES = []
|
||||
|
||||
DB_FILE = 'home-assistant.db'
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ https://home-assistant.io/components/rfxtrx/
|
|||
import logging
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip' +
|
||||
'#RFXtrx==0.2']
|
||||
|
||||
|
|
|
@ -76,8 +76,9 @@ def setup(hass, config):
|
|||
_LOGGER.warn("Found invalid key for script: %s. Use %s instead.",
|
||||
object_id, slugify(object_id))
|
||||
continue
|
||||
if not cfg.get(CONF_SEQUENCE):
|
||||
_LOGGER.warn("Missing key 'sequence' for script %s", object_id)
|
||||
if not isinstance(cfg.get(CONF_SEQUENCE), list):
|
||||
_LOGGER.warn("Key 'sequence' for script %s should be a list",
|
||||
object_id)
|
||||
continue
|
||||
alias = cfg.get(CONF_ALIAS, object_id)
|
||||
script = Script(hass, object_id, alias, cfg[CONF_SEQUENCE])
|
||||
|
|
|
@ -9,10 +9,9 @@ https://home-assistant.io/components/sensor/
|
|||
import logging
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import wink, zwave, isy994, verisure
|
||||
from homeassistant.components import wink, zwave, isy994, verisure, ecobee
|
||||
|
||||
DOMAIN = 'sensor'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
@ -22,7 +21,8 @@ DISCOVERY_PLATFORMS = {
|
|||
wink.DISCOVER_SENSORS: 'wink',
|
||||
zwave.DISCOVER_SENSORS: 'zwave',
|
||||
isy994.DISCOVER_SENSORS: 'isy994',
|
||||
verisure.DISCOVER_SENSORS: 'verisure'
|
||||
verisure.DISCOVER_SENSORS: 'verisure',
|
||||
ecobee.DISCOVER_SENSORS: 'ecobee'
|
||||
}
|
||||
|
||||
|
||||
|
|
94
homeassistant/components/sensor/ecobee.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
"""
|
||||
homeassistant.components.sensor.ecobee
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ecobee Thermostat Component
|
||||
|
||||
This component adds support for Ecobee3 Wireless Thermostats.
|
||||
You will need to setup developer access to your thermostat,
|
||||
and create and API key on the ecobee website.
|
||||
|
||||
The first time you run this component you will see a configuration
|
||||
component card in Home Assistant. This card will contain a PIN code
|
||||
that you will need to use to authorize access to your thermostat. You
|
||||
can do this at https://www.ecobee.com/consumerportal/index.html
|
||||
Click My Apps, Add application, Enter Pin and click Authorize.
|
||||
|
||||
After authorizing the application click the button in the configuration
|
||||
card. Now your thermostat and sensors should shown in home-assistant.
|
||||
|
||||
You can use the optional hold_temp parameter to set whether or not holds
|
||||
are set indefintely or until the next scheduled event.
|
||||
|
||||
ecobee:
|
||||
api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf
|
||||
hold_temp: True
|
||||
|
||||
"""
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
import logging
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'temperature': ['Temperature', TEMP_FAHRENHEIT],
|
||||
'humidity': ['Humidity', '%'],
|
||||
'occupancy': ['Occupancy', '']
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the sensors. """
|
||||
if discovery_info is None:
|
||||
return
|
||||
dev = list()
|
||||
for name, data in ecobee.NETWORK.ecobee.sensors.items():
|
||||
if 'temp' in data:
|
||||
dev.append(EcobeeSensor(name, 'temperature'))
|
||||
if 'humidity' in data:
|
||||
dev.append(EcobeeSensor(name, 'humidity'))
|
||||
if 'occupancy' in data:
|
||||
dev.append(EcobeeSensor(name, 'occupancy'))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
class EcobeeSensor(Entity):
|
||||
""" An ecobee sensor. """
|
||||
|
||||
def __init__(self, sensor_name, sensor_type):
|
||||
self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0]
|
||||
self.sensor_name = sensor_name
|
||||
self.type = sensor_type
|
||||
self._state = None
|
||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name.rstrip()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
ecobee.NETWORK.update()
|
||||
data = ecobee.NETWORK.ecobee.sensors[self.sensor_name]
|
||||
if self.type == 'temperature':
|
||||
self._state = data['temp']
|
||||
elif self.type == 'humidity':
|
||||
self._state = data['humidity']
|
||||
elif self.type == 'occupancy':
|
||||
self._state = data['occupancy']
|
|
@ -97,5 +97,5 @@ class EfergySensor(Entity):
|
|||
self._state = response.json()['sum']
|
||||
else:
|
||||
self._state = 'Unknown'
|
||||
except RequestException:
|
||||
except (RequestException, ValueError):
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
|
|
|
@ -12,7 +12,6 @@ import subprocess
|
|||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'shell_command'
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import homeassistant.util.dt as dt_util
|
|||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['astral==0.8.1']
|
||||
DOMAIN = "sun"
|
||||
ENTITY_ID = "sun.sun"
|
||||
|
|
|
@ -20,7 +20,6 @@ from homeassistant.components import (
|
|||
group, discovery, wink, isy994, verisure, zwave)
|
||||
|
||||
DOMAIN = 'switch'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
GROUP_NAME_ALL_SWITCHES = 'all switches'
|
||||
|
|
|
@ -34,30 +34,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
|
||||
dev = []
|
||||
pins = config.get('pins')
|
||||
pins = config.get('pins', {})
|
||||
for pinnum, pin in pins.items():
|
||||
dev.append(ArestSwitch(resource,
|
||||
config.get('name', response.json()['name']),
|
||||
pin.get('name'),
|
||||
pinnum))
|
||||
dev.append(ArestSwitchPin(resource,
|
||||
config.get('name', response.json()['name']),
|
||||
pin.get('name'),
|
||||
pinnum))
|
||||
|
||||
functions = config.get('functions', {})
|
||||
for funcname, func in functions.items():
|
||||
dev.append(ArestSwitchFunction(resource,
|
||||
config.get('name',
|
||||
response.json()['name']),
|
||||
func.get('name'),
|
||||
funcname))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
class ArestSwitch(SwitchDevice):
|
||||
class ArestSwitchBase(SwitchDevice):
|
||||
""" Implements an aREST switch. """
|
||||
|
||||
def __init__(self, resource, location, name, pin):
|
||||
def __init__(self, resource, location, name):
|
||||
self._resource = resource
|
||||
self._name = '{} {}'.format(location.title(), name.title()) \
|
||||
or DEVICE_DEFAULT_NAME
|
||||
self._pin = pin
|
||||
self._state = None
|
||||
|
||||
request = requests.get('{}/mode/{}/o'.format(self._resource,
|
||||
self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
_LOGGER.error("Can't set mode. Is device offline?")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the switch. """
|
||||
|
@ -68,6 +71,72 @@ class ArestSwitch(SwitchDevice):
|
|||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
|
||||
class ArestSwitchFunction(ArestSwitchBase):
|
||||
""" Implements an aREST switch. Based on functions. """
|
||||
|
||||
def __init__(self, resource, location, name, func):
|
||||
super().__init__(resource, location, name)
|
||||
self._func = func
|
||||
|
||||
request = requests.get('{}/{}'.format(self._resource, self._func),
|
||||
timeout=10)
|
||||
|
||||
if request.status_code is not 200:
|
||||
_LOGGER.error("Can't find function. Is device offline?")
|
||||
return
|
||||
|
||||
try:
|
||||
request.json()['return_value']
|
||||
except KeyError:
|
||||
_LOGGER.error("No return_value received. "
|
||||
"Is the function name correct.")
|
||||
except ValueError:
|
||||
_LOGGER.error("Response invalid. Is the function name correct.")
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
request = requests.get('{}/{}'.format(self._resource, self._func),
|
||||
timeout=10, params={"params": "1"})
|
||||
|
||||
if request.status_code == 200:
|
||||
self._state = True
|
||||
else:
|
||||
_LOGGER.error("Can't turn on function %s at %s. "
|
||||
"Is device offline?",
|
||||
self._func, self._resource)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
request = requests.get('{}/{}'.format(self._resource, self._func),
|
||||
timeout=10, params={"params": "0"})
|
||||
|
||||
if request.status_code == 200:
|
||||
self._state = False
|
||||
else:
|
||||
_LOGGER.error("Can't turn off function %s at %s. "
|
||||
"Is device offline?",
|
||||
self._func, self._resource)
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API and updates the state. """
|
||||
request = requests.get('{}/{}'.format(self._resource,
|
||||
self._func), timeout=10)
|
||||
self._state = request.json()['return_value'] != 0
|
||||
|
||||
|
||||
class ArestSwitchPin(ArestSwitchBase):
|
||||
""" Implements an aREST switch. Based on digital I/O """
|
||||
|
||||
def __init__(self, resource, location, name, pin):
|
||||
super().__init__(resource, location, name)
|
||||
self._pin = pin
|
||||
|
||||
request = requests.get('{}/mode/{}/o'.format(self._resource,
|
||||
self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
_LOGGER.error("Can't set mode. Is device offline?")
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
request = requests.get('{}/digital/{}/1'.format(self._resource,
|
||||
|
|
|
@ -17,6 +17,7 @@ DEFAULT_QOS = 0
|
|||
DEFAULT_PAYLOAD_ON = "ON"
|
||||
DEFAULT_PAYLOAD_OFF = "OFF"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
DEFAULT_RETAIN = False
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
@ -35,6 +36,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
config.get('state_topic'),
|
||||
config.get('command_topic'),
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
config.get('retain', DEFAULT_RETAIN),
|
||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||
config.get('optimistic', DEFAULT_OPTIMISTIC),
|
||||
|
@ -44,7 +46,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttSwitch(SwitchDevice):
|
||||
""" Represents a switch that can be toggled using MQTT. """
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos, retain,
|
||||
payload_on, payload_off, optimistic, state_format):
|
||||
self._state = False
|
||||
self._hass = hass
|
||||
|
@ -52,6 +54,7 @@ class MqttSwitch(SwitchDevice):
|
|||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._optimistic = optimistic
|
||||
|
@ -93,7 +96,7 @@ class MqttSwitch(SwitchDevice):
|
|||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_on,
|
||||
self._qos)
|
||||
self._qos, self._retain)
|
||||
if self._optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._state = True
|
||||
|
@ -102,7 +105,7 @@ class MqttSwitch(SwitchDevice):
|
|||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_off,
|
||||
self._qos)
|
||||
self._qos, self._retain)
|
||||
if self._optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._state = False
|
||||
|
|
|
@ -126,5 +126,8 @@ class VeraSwitch(ToggleEntity):
|
|||
def update(self):
|
||||
# We need to debounce the status call after turning switch on or off
|
||||
# because the vera has some lag in updating the device status
|
||||
if (self.last_command_send + 5) < time.time():
|
||||
self.is_on_status = self.vera_device.is_switched_on()
|
||||
try:
|
||||
if (self.last_command_send + 5) < time.time():
|
||||
self.is_on_status = self.vera_device.is_switched_on()
|
||||
except RequestException:
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.3.2']
|
||||
REQUIREMENTS = ['pywemo==0.3.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
|
@ -15,12 +15,12 @@ from homeassistant.config import load_yaml_config_file
|
|||
import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELCIUS)
|
||||
|
||||
DOMAIN = "thermostat"
|
||||
DEPENDENCIES = []
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
SCAN_INTERVAL = 60
|
||||
|
@ -42,6 +42,10 @@ ATTR_OPERATION = "current_operation"
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
ecobee.DISCOVER_THERMOSTAT: 'ecobee',
|
||||
}
|
||||
|
||||
|
||||
def set_away_mode(hass, away_mode, entity_id=None):
|
||||
""" Turn all or specified thermostat away mode on. """
|
||||
|
@ -67,7 +71,8 @@ def set_temperature(hass, temperature, entity_id=None):
|
|||
|
||||
def setup(hass, config):
|
||||
""" Setup thermostats. """
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
|
||||
component.setup(config)
|
||||
|
||||
def thermostat_service(service):
|
||||
|
@ -142,13 +147,13 @@ class ThermostatDevice(Entity):
|
|||
data = {
|
||||
ATTR_CURRENT_TEMPERATURE:
|
||||
self._convert(self.current_temperature, 1),
|
||||
ATTR_MIN_TEMP: self._convert(self.min_temp, 0),
|
||||
ATTR_MAX_TEMP: self._convert(self.max_temp, 0),
|
||||
ATTR_TEMPERATURE: self._convert(self.target_temperature, 0),
|
||||
ATTR_MIN_TEMP: self._convert(self.min_temp, 1),
|
||||
ATTR_MAX_TEMP: self._convert(self.max_temp, 1),
|
||||
ATTR_TEMPERATURE: self._convert(self.target_temperature, 1),
|
||||
ATTR_TEMPERATURE_LOW:
|
||||
self._convert(self.target_temperature_low, 0),
|
||||
self._convert(self.target_temperature_low, 1),
|
||||
ATTR_TEMPERATURE_HIGH:
|
||||
self._convert(self.target_temperature_high, 0),
|
||||
self._convert(self.target_temperature_high, 1),
|
||||
}
|
||||
|
||||
operation = self.operation
|
||||
|
|
209
homeassistant/components/thermostat/ecobee.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
"""
|
||||
homeassistant.components.thermostat.ecobee
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ecobee Thermostat Component
|
||||
|
||||
This component adds support for Ecobee3 Wireless Thermostats.
|
||||
You will need to setup developer access to your thermostat,
|
||||
and create and API key on the ecobee website.
|
||||
|
||||
The first time you run this component you will see a configuration
|
||||
component card in Home Assistant. This card will contain a PIN code
|
||||
that you will need to use to authorize access to your thermostat. You
|
||||
can do this at https://www.ecobee.com/consumerportal/index.html
|
||||
Click My Apps, Add application, Enter Pin and click Authorize.
|
||||
|
||||
After authorizing the application click the button in the configuration
|
||||
card. Now your thermostat and sensors should shown in home-assistant.
|
||||
|
||||
You can use the optional hold_temp parameter to set whether or not holds
|
||||
are set indefintely or until the next scheduled event.
|
||||
|
||||
ecobee:
|
||||
api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf
|
||||
hold_temp: True
|
||||
|
||||
"""
|
||||
from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL,
|
||||
STATE_IDLE, STATE_HEAT)
|
||||
from homeassistant.const import (TEMP_FAHRENHEIT, STATE_ON, STATE_OFF)
|
||||
from homeassistant.components import ecobee
|
||||
import logging
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
_CONFIGURING = {}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup Platform """
|
||||
if discovery_info is None:
|
||||
return
|
||||
data = ecobee.NETWORK
|
||||
hold_temp = discovery_info['hold_temp']
|
||||
_LOGGER.info("Loading ecobee thermostat component with hold_temp set to "
|
||||
+ str(hold_temp))
|
||||
add_devices(Thermostat(data, index, hold_temp)
|
||||
for index in range(len(data.ecobee.thermostats)))
|
||||
|
||||
|
||||
class Thermostat(ThermostatDevice):
|
||||
""" Thermostat class for Ecobee """
|
||||
|
||||
def __init__(self, data, thermostat_index, hold_temp):
|
||||
self.data = data
|
||||
self.thermostat_index = thermostat_index
|
||||
self.thermostat = self.data.ecobee.get_thermostat(
|
||||
self.thermostat_index)
|
||||
self._name = self.thermostat['name']
|
||||
self._away = 'away' in self.thermostat['program']['currentClimateRef']
|
||||
self.hold_temp = hold_temp
|
||||
|
||||
def update(self):
|
||||
self.data.update()
|
||||
self.thermostat = self.data.ecobee.get_thermostat(
|
||||
self.thermostat_index)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the Ecobee Thermostat. """
|
||||
return self.thermostat['name']
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement this thermostat expresses itself in. """
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
""" Returns the current temperature. """
|
||||
return self.thermostat['runtime']['actualTemperature'] / 10
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
return (self.target_temperature_low + self.target_temperature_high) / 2
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
""" Returns the lower bound temperature we try to reach. """
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
""" Returns the upper bound temperature we try to reach. """
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
""" Returns the current humidity. """
|
||||
return self.thermostat['runtime']['actualHumidity']
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
""" Returns the desired fan mode of operation. """
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def fan(self):
|
||||
""" Returns the current fan state. """
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
""" Returns current operation ie. heat, cool, idle """
|
||||
status = self.thermostat['equipmentStatus']
|
||||
if status == '':
|
||||
return STATE_IDLE
|
||||
elif 'Cool' in status:
|
||||
return STATE_COOL
|
||||
elif 'auxHeat' in status:
|
||||
return STATE_HEAT
|
||||
elif 'heatPump' in status:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return status
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
""" Returns current mode ie. home, away, sleep """
|
||||
mode = self.thermostat['program']['currentClimateRef']
|
||||
self._away = 'away' in mode
|
||||
return mode
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
""" Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off """
|
||||
return self.thermostat['settings']['hvacMode']
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.humidity,
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"hvac_mode": self.hvac_mode
|
||||
}
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
""" Returns if away mode is on. """
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
""" Turns away on. """
|
||||
self._away = True
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_climate_hold("away", "indefinite")
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold("away")
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
""" Turns away off. """
|
||||
self._away = False
|
||||
self.data.ecobee.resume_program()
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
""" Set new target temperature """
|
||||
temperature = int(temperature)
|
||||
low_temp = temperature - 1
|
||||
high_temp = temperature + 1
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_hold_temp(low_temp, high_temp, "indefinite")
|
||||
else:
|
||||
self.data.ecobee.set_hold_temp(low_temp, high_temp)
|
||||
|
||||
def set_hvac_mode(self, mode):
|
||||
""" Set HVAC mode (auto, auxHeatOnly, cool, heat, off) """
|
||||
self.data.ecobee.set_hvac_mode(mode)
|
||||
|
||||
# Home and Sleep mode aren't used in UI yet:
|
||||
|
||||
# def turn_home_mode_on(self):
|
||||
# """ Turns home mode on. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.set_climate_hold("home")
|
||||
|
||||
# def turn_home_mode_off(self):
|
||||
# """ Turns home mode off. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.resume_program()
|
||||
|
||||
# def turn_sleep_mode_on(self):
|
||||
# """ Turns sleep mode on. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.set_climate_hold("sleep")
|
||||
|
||||
# def turn_sleep_mode_off(self):
|
||||
# """ Turns sleep mode off. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.resume_program()
|
|
@ -16,7 +16,6 @@ from homeassistant.helpers import event
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PYPI_URL = 'https://pypi.python.org/pypi/homeassistant/json'
|
||||
DEPENDENCIES = []
|
||||
DOMAIN = 'updater'
|
||||
ENTITY_ID = 'updater.updater'
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ from homeassistant.const import (
|
|||
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
|
||||
|
||||
DOMAIN = "wink"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
||||
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
|
||||
'#python-wink==0.2']
|
||||
|
|
|
@ -15,7 +15,6 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.util.location import distance
|
||||
|
||||
DOMAIN = "zone"
|
||||
DEPENDENCIES = []
|
||||
ENTITY_ID_FORMAT = 'zone.{}'
|
||||
ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home')
|
||||
STATE = 'zoning'
|
||||
|
|
|
@ -17,7 +17,6 @@ from homeassistant.const import (
|
|||
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED)
|
||||
|
||||
DOMAIN = "zwave"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['pydispatcher==2.0.5']
|
||||
|
||||
CONF_USB_STICK_PATH = "usb_path"
|
||||
|
|
|
@ -143,6 +143,10 @@ SERVICE_ALARM_TRIGGER = "alarm_trigger"
|
|||
SERVICE_LOCK = "lock"
|
||||
SERVICE_UNLOCK = "unlock"
|
||||
|
||||
SERVICE_OPEN = 'open'
|
||||
SERVICE_CLOSE = 'close'
|
||||
SERVICE_STOP = 'stop'
|
||||
|
||||
# #### API / REMOTE ####
|
||||
SERVER_PORT = 8123
|
||||
|
||||
|
@ -160,6 +164,7 @@ URL_API_EVENT_FORWARD = "/api/event_forwarding"
|
|||
URL_API_COMPONENTS = "/api/components"
|
||||
URL_API_BOOTSTRAP = "/api/bootstrap"
|
||||
URL_API_ERROR_LOG = "/api/error_log"
|
||||
URL_API_LOG_OUT = "/api/log_out"
|
||||
|
||||
HTTP_OK = 200
|
||||
HTTP_CREATED = 201
|
||||
|
|
|
@ -4,6 +4,8 @@ homeassistant.helpers.entity_component
|
|||
|
||||
Provides helpers for components that manage entities.
|
||||
"""
|
||||
from threading import Lock
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.helpers import (
|
||||
generate_entity_id, config_per_platform, extract_entity_ids)
|
||||
|
@ -37,6 +39,7 @@ class EntityComponent(object):
|
|||
self.is_polling = False
|
||||
|
||||
self.config = None
|
||||
self.lock = Lock()
|
||||
|
||||
def setup(self, config):
|
||||
"""
|
||||
|
@ -61,8 +64,11 @@ class EntityComponent(object):
|
|||
Takes in a list of new entities. For each entity will see if it already
|
||||
exists. If not, will add it, set it up and push the first state.
|
||||
"""
|
||||
for entity in new_entities:
|
||||
if entity is not None and entity not in self.entities.values():
|
||||
with self.lock:
|
||||
for entity in new_entities:
|
||||
if entity is None or entity in self.entities.values():
|
||||
continue
|
||||
|
||||
entity.hass = self.hass
|
||||
|
||||
if getattr(entity, 'entity_id', None) is None:
|
||||
|
@ -74,23 +80,33 @@ class EntityComponent(object):
|
|||
|
||||
entity.update_ha_state()
|
||||
|
||||
if self.group is None and self.group_name is not None:
|
||||
self.group = group.Group(self.hass, self.group_name,
|
||||
user_defined=False)
|
||||
if self.group is None and self.group_name is not None:
|
||||
self.group = group.Group(self.hass, self.group_name,
|
||||
user_defined=False)
|
||||
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(self.entities.keys())
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(self.entities.keys())
|
||||
|
||||
self._start_polling()
|
||||
if self.is_polling or \
|
||||
not any(entity.should_poll for entity
|
||||
in self.entities.values()):
|
||||
return
|
||||
|
||||
self.is_polling = True
|
||||
|
||||
track_utc_time_change(
|
||||
self.hass, self._update_entity_states,
|
||||
second=range(0, 60, self.scan_interval))
|
||||
|
||||
def extract_from_service(self, service):
|
||||
"""
|
||||
Takes a service and extracts all known entities.
|
||||
Will return all if no entity IDs given in service.
|
||||
"""
|
||||
if ATTR_ENTITY_ID not in service.data:
|
||||
return self.entities.values()
|
||||
else:
|
||||
with self.lock:
|
||||
if ATTR_ENTITY_ID not in service.data:
|
||||
return list(self.entities.values())
|
||||
|
||||
return [self.entities[entity_id] for entity_id
|
||||
in extract_entity_ids(self.hass, service)
|
||||
if entity_id in self.entities]
|
||||
|
@ -99,9 +115,10 @@ class EntityComponent(object):
|
|||
""" Update the states of all the entities. """
|
||||
self.logger.info("Updating %s entities", self.domain)
|
||||
|
||||
for entity in self.entities.values():
|
||||
if entity.should_poll:
|
||||
entity.update_ha_state(True)
|
||||
with self.lock:
|
||||
for entity in self.entities.values():
|
||||
if entity.should_poll:
|
||||
entity.update_ha_state(True)
|
||||
|
||||
def _entity_discovered(self, service, info):
|
||||
""" Called when a entity is discovered. """
|
||||
|
@ -110,18 +127,6 @@ class EntityComponent(object):
|
|||
|
||||
self._setup_platform(self.discovery_platforms[service], {}, info)
|
||||
|
||||
def _start_polling(self):
|
||||
""" Start polling entities if necessary. """
|
||||
if self.is_polling or \
|
||||
not any(entity.should_poll for entity in self.entities.values()):
|
||||
return
|
||||
|
||||
self.is_polling = True
|
||||
|
||||
track_utc_time_change(
|
||||
self.hass, self._update_entity_states,
|
||||
second=range(0, 60, self.scan_interval))
|
||||
|
||||
def _setup_platform(self, platform_type, platform_config,
|
||||
discovery_info=None):
|
||||
""" Tries to setup a platform for this component. """
|
||||
|
|
|
@ -193,7 +193,7 @@ def _load_order_component(comp_name, load_order, loading):
|
|||
|
||||
loading.add(comp_name)
|
||||
|
||||
for dependency in component.DEPENDENCIES:
|
||||
for dependency in getattr(component, 'DEPENDENCIES', []):
|
||||
# Check not already loaded
|
||||
if dependency in load_order:
|
||||
continue
|
||||
|
|
|
@ -18,7 +18,10 @@ python-nmap==0.4.3
|
|||
pysnmp==4.2.5
|
||||
|
||||
# homeassistant.components.discovery
|
||||
netdisco==0.5.1
|
||||
netdisco==0.5.2
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
https://github.com/nkgilley/python-ecobee-api/archive/d35596b67c75451fa47001c493a15eebee195e93.zip#python-ecobee==0.0.1
|
||||
|
||||
# homeassistant.components.ifttt
|
||||
pyfttt==0.3
|
||||
|
@ -75,6 +78,8 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
|
|||
|
||||
# homeassistant.components.mqtt
|
||||
paho-mqtt==1.1
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
jsonpath-rw==1.4.0
|
||||
|
||||
# homeassistant.components.notify.pushbullet
|
||||
|
@ -149,7 +154,7 @@ hikvision==0.4
|
|||
orvibo==1.0.0
|
||||
|
||||
# homeassistant.components.switch.wemo
|
||||
pywemo==0.3.2
|
||||
pywemo==0.3.3
|
||||
|
||||
# homeassistant.components.thermostat.honeywell
|
||||
evohomeclient==0.2.3
|
||||
|
|
0
tests/components/alarm_control_panel/__init__.py
Normal file
|
@ -101,7 +101,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase):
|
|||
|
||||
alarm_control_panel.alarm_arm_home(self.hass)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(('alarm/command', 'ARM_HOME', 0),
|
||||
self.assertEqual(('alarm/command', 'ARM_HOME', 0, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
|
||||
def test_arm_home_not_publishes_mqtt_with_invalid_code(self):
|
||||
|
@ -130,7 +130,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase):
|
|||
|
||||
alarm_control_panel.alarm_arm_away(self.hass)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(('alarm/command', 'ARM_AWAY', 0),
|
||||
self.assertEqual(('alarm/command', 'ARM_AWAY', 0, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
|
||||
def test_arm_away_not_publishes_mqtt_with_invalid_code(self):
|
||||
|
@ -159,7 +159,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase):
|
|||
|
||||
alarm_control_panel.alarm_disarm(self.hass)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(('alarm/command', 'DISARM', 0),
|
||||
self.assertEqual(('alarm/command', 'DISARM', 0, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
|
||||
def test_disarm_not_publishes_mqtt_with_invalid_code(self):
|
||||
|
|
|
@ -44,7 +44,6 @@ light:
|
|||
payload_off: "off"
|
||||
"""
|
||||
import unittest
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
import homeassistant.core as ha
|
||||
|
@ -63,6 +62,29 @@ class TestLightMQTT(unittest.TestCase):
|
|||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_no_color_or_brightness_if_no_topics(self):
|
||||
self.assertTrue(light.setup(self.hass, {
|
||||
'light': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'test_light_rgb/status',
|
||||
'command_topic': 'test_light_rgb/set',
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('light.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
self.assertIsNone(state.attributes.get('rgb_color'))
|
||||
self.assertIsNone(state.attributes.get('brightness'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'on')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('light.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
self.assertIsNone(state.attributes.get('rgb_color'))
|
||||
self.assertIsNone(state.attributes.get('brightness'))
|
||||
|
||||
def test_controlling_state_via_topic(self):
|
||||
self.assertTrue(light.setup(self.hass, {
|
||||
'light': {
|
||||
|
@ -82,12 +104,16 @@ class TestLightMQTT(unittest.TestCase):
|
|||
|
||||
state = self.hass.states.get('light.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
self.assertIsNone(state.attributes.get('rgb_color'))
|
||||
self.assertIsNone(state.attributes.get('brightness'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'on')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('light.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
|
||||
self.assertEqual(255, state.attributes.get('brightness'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'off')
|
||||
self.hass.pool.block_till_done()
|
||||
|
@ -139,7 +165,7 @@ class TestLightMQTT(unittest.TestCase):
|
|||
light.turn_on(self.hass, 'light.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('test_light_rgb/set', 'on', 2),
|
||||
self.assertEqual(('test_light_rgb/set', 'on', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('light.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
@ -147,7 +173,7 @@ class TestLightMQTT(unittest.TestCase):
|
|||
light.turn_off(self.hass, 'light.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('test_light_rgb/set', 'off', 2),
|
||||
self.assertEqual(('test_light_rgb/set', 'off', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('light.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
|
0
tests/components/motor/__init__.py
Normal file
166
tests/components/motor/test_mqtt.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
"""
|
||||
tests.components.motor.test_mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests mqtt motor.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.components.motor as motor
|
||||
from tests.common import mock_mqtt_component, fire_mqtt_message
|
||||
|
||||
|
||||
class TestMotorMQTT(unittest.TestCase):
|
||||
""" Test the MQTT motor. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
self.hass = ha.HomeAssistant()
|
||||
self.mock_publish = mock_mqtt_component(self.hass)
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_controlling_state_via_topic(self):
|
||||
self.assertTrue(motor.setup(self.hass, {
|
||||
'motor': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'state-topic',
|
||||
'command_topic': 'command-topic',
|
||||
'qos': 0,
|
||||
'payload_open': 'OPEN',
|
||||
'payload_close': 'CLOSE',
|
||||
'payload_stop': 'STOP'
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', '0')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_CLOSED, state.state)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', '50')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_OPEN, state.state)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', '100')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_OPEN, state.state)
|
||||
|
||||
def test_send_open_command(self):
|
||||
self.assertTrue(motor.setup(self.hass, {
|
||||
'motor': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'state-topic',
|
||||
'command_topic': 'command-topic',
|
||||
'qos': 2
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
motor.call_open(self.hass, 'motor.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'OPEN', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
def test_send_close_command(self):
|
||||
self.assertTrue(motor.setup(self.hass, {
|
||||
'motor': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'state-topic',
|
||||
'command_topic': 'command-topic',
|
||||
'qos': 2
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
motor.call_close(self.hass, 'motor.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'CLOSE', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
def test_send_stop_command(self):
|
||||
self.assertTrue(motor.setup(self.hass, {
|
||||
'motor': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'state-topic',
|
||||
'command_topic': 'command-topic',
|
||||
'qos': 2
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
motor.call_stop(self.hass, 'motor.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'STOP', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('motor.test')
|
||||
self.assertEqual(STATE_UNKNOWN, state.state)
|
||||
|
||||
def test_state_attributes_current_position(self):
|
||||
self.assertTrue(motor.setup(self.hass, {
|
||||
'motor': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'state-topic',
|
||||
'command_topic': 'command-topic',
|
||||
'payload_open': 'OPEN',
|
||||
'payload_close': 'CLOSE',
|
||||
'payload_stop': 'STOP'
|
||||
}
|
||||
}))
|
||||
|
||||
state_attributes_dict = self.hass.states.get(
|
||||
'motor.test').attributes
|
||||
self.assertFalse('current_position' in state_attributes_dict)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', '0')
|
||||
self.hass.pool.block_till_done()
|
||||
current_position = self.hass.states.get(
|
||||
'motor.test').attributes['current_position']
|
||||
self.assertEqual(0, current_position)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', '50')
|
||||
self.hass.pool.block_till_done()
|
||||
current_position = self.hass.states.get(
|
||||
'motor.test').attributes['current_position']
|
||||
self.assertEqual(50, current_position)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', '101')
|
||||
self.hass.pool.block_till_done()
|
||||
current_position = self.hass.states.get(
|
||||
'motor.test').attributes['current_position']
|
||||
self.assertEqual(50, current_position)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', 'non-numeric')
|
||||
self.hass.pool.block_till_done()
|
||||
current_position = self.hass.states.get(
|
||||
'motor.test').attributes['current_position']
|
||||
self.assertEqual(50, current_position)
|
|
@ -68,7 +68,7 @@ class TestSensorMQTT(unittest.TestCase):
|
|||
switch.turn_on(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'beer on', 2),
|
||||
self.assertEqual(('command-topic', 'beer on', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
@ -76,7 +76,7 @@ class TestSensorMQTT(unittest.TestCase):
|
|||
switch.turn_off(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'beer off', 2),
|
||||
self.assertEqual(('command-topic', 'beer off', 2, False),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
|
|
@ -8,14 +8,13 @@ Tests Home Assistant HTTP component does what it should do.
|
|||
import unittest
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant import bootstrap, const
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.remote as remote
|
||||
import homeassistant.components.http as http
|
||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||
|
||||
API_PASSWORD = "test1234"
|
||||
|
||||
|
@ -26,7 +25,7 @@ SERVER_PORT = 8120
|
|||
|
||||
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
|
||||
|
||||
HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD}
|
||||
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD}
|
||||
|
||||
hass = None
|
||||
|
||||
|
@ -68,20 +67,20 @@ class TestAPI(unittest.TestCase):
|
|||
# TODO move back to http component and test with use_auth.
|
||||
def test_access_denied_without_password(self):
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test")))
|
||||
_url(const.URL_API_STATES_ENTITY.format("test")))
|
||||
|
||||
self.assertEqual(401, req.status_code)
|
||||
|
||||
def test_access_denied_with_wrong_password(self):
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test")),
|
||||
headers={HTTP_HEADER_HA_AUTH: 'wrongpassword'})
|
||||
_url(const.URL_API_STATES_ENTITY.format("test")),
|
||||
headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'})
|
||||
|
||||
self.assertEqual(401, req.status_code)
|
||||
|
||||
def test_api_list_state_entities(self):
|
||||
""" Test if the debug interface allows us to list state entities. """
|
||||
req = requests.get(_url(remote.URL_API_STATES),
|
||||
req = requests.get(_url(const.URL_API_STATES),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
remote_data = [ha.State.from_dict(item) for item in req.json()]
|
||||
|
@ -91,7 +90,7 @@ class TestAPI(unittest.TestCase):
|
|||
def test_api_get_state(self):
|
||||
""" Test if the debug interface allows us to get a state. """
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test.test")),
|
||||
_url(const.URL_API_STATES_ENTITY.format("test.test")),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
data = ha.State.from_dict(req.json())
|
||||
|
@ -105,7 +104,7 @@ class TestAPI(unittest.TestCase):
|
|||
def test_api_get_non_existing_state(self):
|
||||
""" Test if the debug interface allows us to get a state. """
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("does_not_exist")),
|
||||
_url(const.URL_API_STATES_ENTITY.format("does_not_exist")),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
self.assertEqual(404, req.status_code)
|
||||
|
@ -115,7 +114,7 @@ class TestAPI(unittest.TestCase):
|
|||
|
||||
hass.states.set("test.test", "not_to_be_set")
|
||||
|
||||
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
|
||||
requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")),
|
||||
data=json.dumps({"state": "debug_state_change2"}),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
|
@ -130,7 +129,7 @@ class TestAPI(unittest.TestCase):
|
|||
new_state = "debug_state_change"
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_STATES_ENTITY.format(
|
||||
_url(const.URL_API_STATES_ENTITY.format(
|
||||
"test_entity.that_does_not_exist")),
|
||||
data=json.dumps({'state': new_state}),
|
||||
headers=HA_HEADERS)
|
||||
|
@ -146,7 +145,7 @@ class TestAPI(unittest.TestCase):
|
|||
""" Test if API sends appropriate error if we omit state. """
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_STATES_ENTITY.format(
|
||||
_url(const.URL_API_STATES_ENTITY.format(
|
||||
"test_entity.that_does_not_exist")),
|
||||
data=json.dumps({}),
|
||||
headers=HA_HEADERS)
|
||||
|
@ -165,7 +164,7 @@ class TestAPI(unittest.TestCase):
|
|||
hass.bus.listen_once("test.event_no_data", listener)
|
||||
|
||||
requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
|
||||
_url(const.URL_API_EVENTS_EVENT.format("test.event_no_data")),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
hass.pool.block_till_done()
|
||||
|
@ -186,7 +185,7 @@ class TestAPI(unittest.TestCase):
|
|||
hass.bus.listen_once("test_event_with_data", listener)
|
||||
|
||||
requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
|
||||
_url(const.URL_API_EVENTS_EVENT.format("test_event_with_data")),
|
||||
data=json.dumps({"test": 1}),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
|
@ -206,7 +205,7 @@ class TestAPI(unittest.TestCase):
|
|||
hass.bus.listen_once("test_event_bad_data", listener)
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
||||
_url(const.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
||||
data=json.dumps('not an object'),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
|
@ -217,7 +216,7 @@ class TestAPI(unittest.TestCase):
|
|||
|
||||
# Try now with valid but unusable JSON
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
||||
_url(const.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
||||
data=json.dumps([1, 2, 3]),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
|
@ -226,9 +225,31 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertEqual(422, req.status_code)
|
||||
self.assertEqual(0, len(test_value))
|
||||
|
||||
def test_api_get_config(self):
|
||||
req = requests.get(_url(const.URL_API_CONFIG),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(hass.config.as_dict(), req.json())
|
||||
|
||||
def test_api_get_components(self):
|
||||
req = requests.get(_url(const.URL_API_COMPONENTS),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(hass.config.components, req.json())
|
||||
|
||||
def test_api_get_error_log(self):
|
||||
test_content = 'Test String'
|
||||
with tempfile.NamedTemporaryFile() as log:
|
||||
log.write(test_content.encode('utf-8'))
|
||||
log.flush()
|
||||
|
||||
with patch.object(hass.config, 'path', return_value=log.name):
|
||||
req = requests.get(_url(const.URL_API_ERROR_LOG),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(test_content, req.text)
|
||||
self.assertIsNone(req.headers.get('expires'))
|
||||
|
||||
def test_api_get_event_listeners(self):
|
||||
""" Test if we can get the list of events being listened for. """
|
||||
req = requests.get(_url(remote.URL_API_EVENTS),
|
||||
req = requests.get(_url(const.URL_API_EVENTS),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
local = hass.bus.listeners
|
||||
|
@ -241,7 +262,7 @@ class TestAPI(unittest.TestCase):
|
|||
|
||||
def test_api_get_services(self):
|
||||
""" Test if we can get a dict describing current services. """
|
||||
req = requests.get(_url(remote.URL_API_SERVICES),
|
||||
req = requests.get(_url(const.URL_API_SERVICES),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
local_services = hass.services.services
|
||||
|
@ -262,7 +283,7 @@ class TestAPI(unittest.TestCase):
|
|||
hass.services.register("test_domain", "test_service", listener)
|
||||
|
||||
requests.post(
|
||||
_url(remote.URL_API_SERVICES_SERVICE.format(
|
||||
_url(const.URL_API_SERVICES_SERVICE.format(
|
||||
"test_domain", "test_service")),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
|
@ -283,7 +304,7 @@ class TestAPI(unittest.TestCase):
|
|||
hass.services.register("test_domain", "test_service", listener)
|
||||
|
||||
requests.post(
|
||||
_url(remote.URL_API_SERVICES_SERVICE.format(
|
||||
_url(const.URL_API_SERVICES_SERVICE.format(
|
||||
"test_domain", "test_service")),
|
||||
data=json.dumps({"test": 1}),
|
||||
headers=HA_HEADERS)
|
||||
|
@ -296,24 +317,24 @@ class TestAPI(unittest.TestCase):
|
|||
""" Test setting up event forwarding. """
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(400, req.status_code)
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({'host': '127.0.0.1'}),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(400, req.status_code)
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({'api_password': 'bla-di-bla'}),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(400, req.status_code)
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({
|
||||
'api_password': 'bla-di-bla',
|
||||
'host': '127.0.0.1',
|
||||
|
@ -323,7 +344,7 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertEqual(422, req.status_code)
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({
|
||||
'api_password': 'bla-di-bla',
|
||||
'host': '127.0.0.1',
|
||||
|
@ -334,7 +355,7 @@ class TestAPI(unittest.TestCase):
|
|||
|
||||
# Setup a real one
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({
|
||||
'api_password': API_PASSWORD,
|
||||
'host': '127.0.0.1',
|
||||
|
@ -345,13 +366,13 @@ class TestAPI(unittest.TestCase):
|
|||
|
||||
# Delete it again..
|
||||
req = requests.delete(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({}),
|
||||
headers=HA_HEADERS)
|
||||
self.assertEqual(400, req.status_code)
|
||||
|
||||
req = requests.delete(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({
|
||||
'host': '127.0.0.1',
|
||||
'port': 'abcd'
|
||||
|
@ -360,7 +381,7 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertEqual(422, req.status_code)
|
||||
|
||||
req = requests.delete(
|
||||
_url(remote.URL_API_EVENT_FORWARD),
|
||||
_url(const.URL_API_EVENT_FORWARD),
|
||||
data=json.dumps({
|
||||
'host': '127.0.0.1',
|
||||
'port': SERVER_PORT
|
||||
|
|
|
@ -27,23 +27,10 @@ class TestScript(unittest.TestCase):
|
|||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_setup_with_empty_sequence(self):
|
||||
self.assertTrue(script.setup(self.hass, {
|
||||
'script': {
|
||||
'test': {
|
||||
'sequence': []
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.assertIsNone(self.hass.states.get(ENTITY_ID))
|
||||
|
||||
def test_setup_with_missing_sequence(self):
|
||||
self.assertTrue(script.setup(self.hass, {
|
||||
'script': {
|
||||
'test': {
|
||||
'sequence': []
|
||||
}
|
||||
'test': {}
|
||||
}
|
||||
}))
|
||||
|
||||
|
@ -60,6 +47,19 @@ class TestScript(unittest.TestCase):
|
|||
|
||||
self.assertEqual(0, len(self.hass.states.entity_ids('script')))
|
||||
|
||||
def test_setup_with_dict_as_sequence(self):
|
||||
self.assertTrue(script.setup(self.hass, {
|
||||
'script': {
|
||||
'test': {
|
||||
'sequence': {
|
||||
'event': 'test_event'
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.assertEqual(0, len(self.hass.states.entity_ids('script')))
|
||||
|
||||
def test_firing_event(self):
|
||||
event = 'test_event'
|
||||
calls = []
|
||||
|
|