mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
commit
8c505e625b
10
.coveragerc
10
.coveragerc
@ -78,6 +78,9 @@ omit =
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
@ -92,6 +95,7 @@ omit =
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
@ -126,6 +130,7 @@ omit =
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
@ -171,16 +176,18 @@ omit =
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@ -196,6 +203,7 @@ omit =
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rest.py
|
||||
|
567
docs/swagger.yaml
Normal file
567
docs/swagger.yaml
Normal file
@ -0,0 +1,567 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.0"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
# array of all schemes that your API supports
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: api_password
|
||||
in: query
|
||||
|
||||
# api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: x-ha-access
|
||||
# in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/config:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
schema:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/discovery_info:
|
||||
get:
|
||||
summary: Basic information about Home Assistant instance
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Basic information
|
||||
schema:
|
||||
$ref: '#/definitions/DiscoveryInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/bootstrap:
|
||||
get:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
schema:
|
||||
$ref: '#/definitions/BootstrapInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events:
|
||||
get:
|
||||
summary: Array of event objects.
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services:
|
||||
get:
|
||||
summary: Array of service objects.
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/history:
|
||||
get:
|
||||
summary: Array of state changes in the past.
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/History'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states:
|
||||
get:
|
||||
summary: Array of state objects.
|
||||
description: |
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states/{entity_id}:
|
||||
get:
|
||||
summary: Specific state object.
|
||||
description: |
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the entity to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: State
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
404:
|
||||
description: Not found
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
post:
|
||||
description: |
|
||||
Updates or creates the current state of an entity.
|
||||
tags:
|
||||
- State
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id to set the state of
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/State'
|
||||
responses:
|
||||
200:
|
||||
description: State of existing entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
201:
|
||||
description: State of new entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
headers:
|
||||
location:
|
||||
type: string
|
||||
description: location of the new entity
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/error_log:
|
||||
get:
|
||||
summary: Error log
|
||||
description: |
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
200:
|
||||
description: Plain text error log
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/camera_proxy/camera.{entity_id}:
|
||||
get:
|
||||
summary: Camera image.
|
||||
description: |
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the camera to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Camera image
|
||||
schema:
|
||||
type: file
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events/{event_type}:
|
||||
post:
|
||||
description: |
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: event_type
|
||||
in: path
|
||||
description: event_type to fire event with
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/EventData'
|
||||
responses:
|
||||
200:
|
||||
description: Response message
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services/{domain}/{service}:
|
||||
post:
|
||||
description: |
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
description: domain of the service
|
||||
required: true
|
||||
type: string
|
||||
- name: service
|
||||
in: path
|
||||
description: service to call
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/ServiceData'
|
||||
responses:
|
||||
200:
|
||||
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/template:
|
||||
post:
|
||||
description: |
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- text/plain
|
||||
parameters:
|
||||
- $ref: '#/parameters/Template'
|
||||
responses:
|
||||
200:
|
||||
description: Returns the rendered template in plain text.
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/event_forwarding:
|
||||
post:
|
||||
description: |
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was setup successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
delete:
|
||||
description: |
|
||||
Cancel event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was cancelled successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/stream:
|
||||
get:
|
||||
summary: Server-sent events
|
||||
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
- name: restrict
|
||||
in: query
|
||||
description: comma-separated list of event_types to filter
|
||||
required: false
|
||||
type: string
|
||||
responses:
|
||||
default:
|
||||
description: Stream of events
|
||||
schema:
|
||||
type: object
|
||||
x-events:
|
||||
state_changed:
|
||||
type: object
|
||||
properties:
|
||||
entity_id:
|
||||
type: string
|
||||
old_state:
|
||||
$ref: '#/definitions/State'
|
||||
new_state:
|
||||
$ref: '#/definitions/State'
|
||||
definitions:
|
||||
ApiConfig:
|
||||
type: object
|
||||
properties:
|
||||
components:
|
||||
type: array
|
||||
description: List of component types
|
||||
items:
|
||||
type: string
|
||||
description: Component type
|
||||
latitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Latitude of Home Assistant server
|
||||
longitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Longitude of Home Assistant server
|
||||
location_name:
|
||||
type: string
|
||||
temperature_unit:
|
||||
type: string
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
DiscoveryInfo:
|
||||
type: object
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
location_name:
|
||||
type: string
|
||||
requires_api_password:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
BootstrapInfo:
|
||||
type: object
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
events:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
Event:
|
||||
type: object
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
listener_count:
|
||||
type: integer
|
||||
Service:
|
||||
type: object
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
services:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/DomainService'
|
||||
DomainService:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
type: object
|
||||
description: Object with service fields that can be called
|
||||
State:
|
||||
type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
entity_id:
|
||||
type: string
|
||||
last_changed:
|
||||
type: string
|
||||
format: date-time
|
||||
StateAttributes:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
History:
|
||||
allOf:
|
||||
- $ref: '#/definitions/State'
|
||||
- type: object
|
||||
properties:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
Message:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
parameters:
|
||||
State:
|
||||
name: body
|
||||
in: body
|
||||
description: State parameter
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- state
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
EventData:
|
||||
name: body
|
||||
in: body
|
||||
description: event_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
ServiceData:
|
||||
name: body
|
||||
in: body
|
||||
description: service_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
Template:
|
||||
name: body
|
||||
in: body
|
||||
description: Template to render
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- template
|
||||
properties:
|
||||
template:
|
||||
description: Jinja2 template string
|
||||
type: string
|
||||
EventForwarding:
|
||||
name: body
|
||||
in: body
|
||||
description: Event Forwarding parameter
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- host
|
||||
- api_password
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
api_password:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
@ -9,7 +9,6 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import verisure
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
@ -24,11 +23,6 @@ SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
verisure.DISCOVER_ALARMS: 'verisure'
|
||||
}
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_DISARM: 'alarm_disarm',
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
@ -50,8 +44,7 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
|
@ -9,8 +9,6 @@ import logging
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.components import (
|
||||
bloomsky, mysensors, zwave, vera, wemo, wink)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
@ -35,22 +33,11 @@ SENSOR_CLASSES = [
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
]
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_BINARY_SENSORS: 'bloomsky',
|
||||
mysensors.DISCOVER_BINARY_SENSORS: 'mysensors',
|
||||
zwave.DISCOVER_BINARY_SENSORS: 'zwave',
|
||||
vera.DISCOVER_BINARY_SENSORS: 'vera',
|
||||
wemo.DISCOVER_BINARY_SENSORS: 'wemo',
|
||||
wink.DISCOVER_BINARY_SENSORS: 'wink'
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
|
@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
_LOGGER.error('Unable to fetch REST data')
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
@ -57,6 +57,7 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._state = False
|
||||
self._previous_data = None
|
||||
self._value_template = value_template
|
||||
self.update()
|
||||
|
||||
@ -77,9 +78,14 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
response = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
except ValueError:
|
||||
return {"true": True, "on": True, "open": True,
|
||||
"yes": True}.get(response.lower(), False)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from REST API and updates the state."""
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
|
@ -9,9 +9,8 @@ from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "bloomsky"
|
||||
@ -23,10 +22,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# no point in polling the API more frequently
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
DISCOVER_SENSORS = 'bloomsky.sensors'
|
||||
DISCOVER_BINARY_SENSORS = 'bloomsky.binary_sensor'
|
||||
DISCOVER_CAMERAS = 'bloomsky.camera'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
@ -45,11 +40,8 @@ def setup(hass, config):
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
for component, discovery_service in (
|
||||
('camera', DISCOVER_CAMERAS), ('sensor', DISCOVER_SENSORS),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS)):
|
||||
discovery.discover(hass, discovery_service, component=component,
|
||||
hass_config=config)
|
||||
for component in 'camera', 'binary_sensor', 'sensor':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -9,7 +9,6 @@ import logging
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import bloomsky
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@ -18,11 +17,6 @@ DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = 30
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
|
||||
}
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
@ -34,8 +28,7 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
def setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
||||
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
@ -49,7 +49,7 @@ class FoscamCamera(Camera):
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = requests.get(self._snap_picture_url)
|
||||
response = requests.get(self._snap_picture_url, timeout=10)
|
||||
|
||||
return response.content
|
||||
|
||||
|
@ -43,13 +43,14 @@ class GenericCamera(Camera):
|
||||
try:
|
||||
response = requests.get(
|
||||
self._still_image_url,
|
||||
auth=HTTPBasicAuth(self._username, self._password))
|
||||
auth=HTTPBasicAuth(self._username, self._password),
|
||||
timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
response = requests.get(self._still_image_url)
|
||||
response = requests.get(self._still_image_url, timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
|
53
homeassistant/components/camera/local_file.py
Normal file
53
homeassistant/components/camera/local_file.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Camera that loads a picture from a local file."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
# check for missing required configuration variable
|
||||
if config.get("file_path") is None:
|
||||
_LOGGER.error("Missing required variable: file_path")
|
||||
return False
|
||||
|
||||
setup_config = (
|
||||
{
|
||||
"name": config.get("name", "Local File"),
|
||||
"file_path": config.get("file_path")
|
||||
}
|
||||
)
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(setup_config["file_path"], os.R_OK):
|
||||
_LOGGER.error("file path is not readable")
|
||||
return False
|
||||
|
||||
add_devices([
|
||||
LocalFile(setup_config)
|
||||
])
|
||||
|
||||
|
||||
class LocalFile(Camera):
|
||||
"""Local camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize Local File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = device_info["name"]
|
||||
self._config = device_info
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._config["file_path"], 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
@ -46,10 +46,9 @@ class MjpegCamera(Camera):
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)
|
||||
stream=True, timeout=10)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url,
|
||||
stream=True)
|
||||
return requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
104
homeassistant/components/camera/netatmo.py
Normal file
104
homeassistant/components/camera/netatmo.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""
|
||||
Support for the Netatmo Welcome camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOME = 'home'
|
||||
ATTR_CAMERAS = 'cameras'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup access to Netatmo Welcome cameras."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if ATTR_CAMERAS in config:
|
||||
if camera_name not in config[ATTR_CAMERAS]:
|
||||
continue
|
||||
add_devices_callback([WelcomeCamera(data, camera_name, home)])
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
|
||||
def __init__(self, data, camera_name, home):
|
||||
"""Setup for access to the BloomSky camera images."""
|
||||
super(WelcomeCamera, self).__init__()
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
if self._localurl:
|
||||
response = requests.get('{0}/live/snapshot_720.jpg'.format(
|
||||
self._localurl), timeout=10)
|
||||
else:
|
||||
response = requests.get('{0}/live/snapshot_720.jpg'.format(
|
||||
self._vpnurl), timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Welcome VPN url changed: %s', error)
|
||||
self._data.update()
|
||||
(self._vpnurl, self._localurl) = \
|
||||
self._data.welcomedata.cameraUrls(camera=self._camera_name)
|
||||
return None
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this Netatmo Welcome device."""
|
||||
return self._name
|
||||
|
||||
|
||||
class WelcomeData(object):
|
||||
"""Get the latest data from NetAtmo."""
|
||||
|
||||
def __init__(self, auth, home=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.welcomedata = None
|
||||
self.camera_names = []
|
||||
self.home = home
|
||||
|
||||
def get_camera_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras.keys():
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
for camera in self.welcomedata.cameras[self.home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
return self.camera_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
@ -8,7 +8,7 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
DOMAIN = "configurator"
|
||||
@ -118,6 +118,7 @@ class Configurator(object):
|
||||
data = {
|
||||
ATTR_CONFIGURE_ID: request_id,
|
||||
ATTR_FIELDS: fields,
|
||||
ATTR_FRIENDLY_NAME: name,
|
||||
}
|
||||
|
||||
data.update({
|
||||
|
@ -67,7 +67,9 @@ def setup(hass, config):
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group(hass, 'bedroom', [lights[0], switches[1], media_players[0]])
|
||||
group.Group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance'])
|
||||
group.Group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group(hass, 'doors', [
|
||||
@ -145,6 +147,17 @@ def setup(hass, config):
|
||||
{'input_boolean': {'notify': {'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}})
|
||||
|
||||
# Set up input boolean
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
'name': 'Allowed Noise',
|
||||
'unit_of_measurement': 'dB'}}})
|
||||
|
||||
# Set up weblink
|
||||
bootstrap.setup_component(
|
||||
hass, 'weblink',
|
||||
|
@ -12,10 +12,11 @@ import os
|
||||
import threading
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import discovery, group, zone
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
@ -62,7 +63,7 @@ ATTR_GPS = 'gps'
|
||||
ATTR_BATTERY = 'battery'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_NETGEAR: 'netgear',
|
||||
SERVICE_NETGEAR: 'netgear',
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -95,8 +96,11 @@ def setup(hass, config):
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
if isinstance(conf, list) and len(conf) > 0:
|
||||
conf = conf[0]
|
||||
|
||||
# Config can be an empty list. In that case, substitute a dict
|
||||
if isinstance(conf, list):
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
|
||||
consider_home = timedelta(
|
||||
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||
DEFAULT_CONSIDER_HOME))
|
||||
|
@ -6,8 +6,10 @@ https://home-assistant.io/components/device_tracker.asuswrt/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
@ -28,6 +30,21 @@ _LEASES_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
# command to get both 5GHz and 2.4GHz clients
|
||||
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
|
||||
_WL_REGEX = re.compile(
|
||||
r'\w+\s' +
|
||||
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
|
||||
|
||||
_ARP_CMD = 'arp -n'
|
||||
_ARP_REGEX = re.compile(
|
||||
r'.+\s' +
|
||||
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
|
||||
r'.+\s' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
|
||||
r'\s' +
|
||||
r'.*')
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
@ -41,24 +58,35 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an ASUS-WRT scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME]},
|
||||
_LOGGER):
|
||||
return None
|
||||
elif CONF_PASSWORD not in config[DOMAIN] and \
|
||||
'pub_key' not in config[DOMAIN]:
|
||||
_LOGGER.error("Either a public key or password must be provided")
|
||||
return None
|
||||
|
||||
scanner = AsusWrtDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-branches
|
||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config[CONF_PASSWORD])
|
||||
self.password = str(config.get(CONF_PASSWORD))
|
||||
self.pub_key = str(config.get('pub_key'))
|
||||
self.protocol = config.get('protocol')
|
||||
self.mode = config.get('mode')
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
@ -106,21 +134,40 @@ class AsusWrtDeviceScanner(object):
|
||||
|
||||
def ssh_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the ssh protocol."""
|
||||
from pexpect import pxssh
|
||||
from pexpect import pxssh, exceptions
|
||||
|
||||
try:
|
||||
ssh = pxssh.pxssh()
|
||||
ssh.login(self.host, self.username, self.password)
|
||||
if self.pub_key:
|
||||
ssh.login(self.host, self.username, ssh_key=self.pub_key)
|
||||
elif self.password:
|
||||
ssh.login(self.host, self.username, self.password)
|
||||
else:
|
||||
_LOGGER.error('No password or public key specified')
|
||||
return None
|
||||
ssh.sendline(_IP_NEIGH_CMD)
|
||||
ssh.prompt()
|
||||
neighbors = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
if self.mode == 'ap':
|
||||
ssh.sendline(_ARP_CMD)
|
||||
ssh.prompt()
|
||||
arp_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_WL_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
else:
|
||||
arp_result = ['']
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return (neighbors, leases_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.exception('Unexpected response from router: %s', exc)
|
||||
return ('', '')
|
||||
_LOGGER.error('Unexpected response from router: %s', exc)
|
||||
return None
|
||||
except exceptions.EOF:
|
||||
_LOGGER.error('Connection refused or no route to host')
|
||||
return None
|
||||
|
||||
def telnet_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the telnet protocol."""
|
||||
@ -133,47 +180,99 @@ class AsusWrtDeviceScanner(object):
|
||||
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
|
||||
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
||||
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
if self.mode == 'ap':
|
||||
telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||
arp_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
else:
|
||||
arp_result = ['']
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return (neighbors, leases_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return ('', '')
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router,"
|
||||
" is telnet enabled?")
|
||||
return ('', '')
|
||||
_LOGGER.error("Connection refused by router, is telnet enabled?")
|
||||
return None
|
||||
except socket.gaierror as exc:
|
||||
_LOGGER.error("Socket exception: %s", exc)
|
||||
return None
|
||||
except OSError as exc:
|
||||
_LOGGER.error("OSError: %s", exc)
|
||||
return None
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
if self.protocol == 'telnet':
|
||||
neighbors, leases_result = self.telnet_connection()
|
||||
if self.protocol == 'ssh':
|
||||
result = self.ssh_connection()
|
||||
elif self.protocol == 'telnet':
|
||||
result = self.telnet_connection()
|
||||
else:
|
||||
neighbors, leases_result = self.ssh_connection()
|
||||
# autodetect protocol
|
||||
result = self.ssh_connection()
|
||||
if result:
|
||||
self.protocol = 'ssh'
|
||||
else:
|
||||
result = self.telnet_connection()
|
||||
if result:
|
||||
self.protocol = 'telnet'
|
||||
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
if self.mode == 'ap':
|
||||
for lease in result.leases:
|
||||
match = _WL_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse lease row: %s", lease)
|
||||
continue
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse wl row: %s", lease)
|
||||
continue
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure it is
|
||||
# blank and not '*', which breaks the entity_id down the line.
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
# match mac addresses to IP addresses in ARP table
|
||||
for arp in result.arp:
|
||||
if match.group('mac').lower() in arp.decode('utf-8'):
|
||||
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
|
||||
if not arp_match:
|
||||
_LOGGER.warning("Could not parse arp row: %s", arp)
|
||||
continue
|
||||
|
||||
for neighbor in neighbors:
|
||||
devices[arp_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
else:
|
||||
for lease in result.leases:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse lease row: %s", lease)
|
||||
continue
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure it
|
||||
# is blank and not '*', which breaks entity_id down the line.
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
|
141
homeassistant/components/device_tracker/bt_home_hub_5.py
Normal file
141
homeassistant/components/device_tracker/bt_home_hub_5.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""
|
||||
Support for BT Home Hub 5.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.bt_home_hub_5/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Return a BT Home Hub 5 scanner if successful."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST]},
|
||||
_LOGGER):
|
||||
return None
|
||||
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class BTHomeHub5DeviceScanner(object):
|
||||
"""This class queries a BT Home Hub 5."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialise the scanner."""
|
||||
_LOGGER.info("Initialising BT Home Hub 5")
|
||||
self.host = config.get(CONF_HOST, '192.168.1.254')
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
|
||||
|
||||
# Test the router is accessible
|
||||
data = _get_homehub_data(self.url)
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return (device for device in self.last_results)
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.last_results:
|
||||
self._update_info()
|
||||
|
||||
if not self.last_results:
|
||||
return None
|
||||
|
||||
return self.last_results.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the BT Home Hub 5 is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Scanning")
|
||||
|
||||
data = _get_homehub_data(self.url)
|
||||
|
||||
if not data:
|
||||
_LOGGER.warning('Error scanning devices')
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_homehub_data(url):
|
||||
"""Retrieve data from BT Home Hub 5 and return parsed result."""
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception("Connection to the router timed out")
|
||||
return
|
||||
if response.status_code == 200:
|
||||
return _parse_homehub_response(response.text)
|
||||
else:
|
||||
_LOGGER.error("Invalid response from Home Hub: %s", response)
|
||||
|
||||
|
||||
def _parse_homehub_response(data_str):
|
||||
"""Parse the BT Home Hub 5 data format."""
|
||||
root = ET.fromstring(data_str)
|
||||
|
||||
dirty_json = root.find('known_device_list').get('value')
|
||||
|
||||
# Normalise the JavaScript data to JSON.
|
||||
clean_json = unquote(dirty_json.replace('\'', '\"')
|
||||
.replace('{', '{\"')
|
||||
.replace(':\"', '\":\"')
|
||||
.replace('\",', '\",\"'))
|
||||
|
||||
known_devices = [x for x in json.loads(clean_json) if x]
|
||||
|
||||
devices = {}
|
||||
|
||||
for device in known_devices:
|
||||
name = device.get('name')
|
||||
mac = device.get('mac')
|
||||
|
||||
if _MAC_REGEX.match(mac) or ',' in mac:
|
||||
for mac_addr in mac.split(','):
|
||||
if _MAC_REGEX.match(mac_addr):
|
||||
devices[mac_addr] = name
|
||||
else:
|
||||
devices[mac] = name
|
||||
|
||||
return devices
|
@ -26,7 +26,7 @@ class LocativeView(HomeAssistantView):
|
||||
"""View to handle locative requests."""
|
||||
|
||||
url = "/api/locative"
|
||||
name = "api:bootstrap"
|
||||
name = "api:locative"
|
||||
|
||||
def __init__(self, hass, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
|
@ -9,100 +9,30 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.6.7']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
LOAD_PLATFORM = 'load_platform'
|
||||
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_CAST = 'google_cast'
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_SONOS = 'sonos'
|
||||
SERVICE_PLEX = 'plex_mediaserver'
|
||||
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
|
||||
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
|
||||
SERVICE_ROKU = 'roku'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: "wemo",
|
||||
SERVICE_CAST: "media_player",
|
||||
SERVICE_HUE: "light",
|
||||
SERVICE_NETGEAR: 'device_tracker',
|
||||
SERVICE_SONOS: 'media_player',
|
||||
SERVICE_PLEX: 'media_player',
|
||||
SERVICE_SQUEEZEBOX: 'media_player',
|
||||
SERVICE_PANASONIC_VIERA: 'media_player',
|
||||
SERVICE_ROKU: 'media_player',
|
||||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
'roku': ('media_player', 'roku'),
|
||||
'sonos': ('media_player', 'sonos'),
|
||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||
}
|
||||
|
||||
|
||||
def listen(hass, service, callback):
|
||||
"""Setup listener for discovery of specific service.
|
||||
|
||||
Service can be a string or a list/tuple.
|
||||
"""
|
||||
if isinstance(service, str):
|
||||
service = (service,)
|
||||
else:
|
||||
service = tuple(service)
|
||||
|
||||
def discovery_event_listener(event):
|
||||
"""Listen for discovery events."""
|
||||
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
|
||||
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
|
||||
|
||||
|
||||
def discover(hass, service, discovered=None, component=None, hass_config=None):
|
||||
"""Fire discovery event. Can ensure a component is loaded."""
|
||||
if component is not None:
|
||||
bootstrap.setup_component(hass, component, hass_config)
|
||||
|
||||
data = {
|
||||
ATTR_SERVICE: service
|
||||
}
|
||||
|
||||
if discovered is not None:
|
||||
data[ATTR_DISCOVERED] = discovered
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
|
||||
|
||||
|
||||
def load_platform(hass, component, platform, info=None, hass_config=None):
|
||||
"""Helper method for generic platform loading.
|
||||
|
||||
This method allows a platform to be loaded dynamically without it being
|
||||
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
|
||||
Advantages of using this method:
|
||||
- Any component & platforms combination can be dynamically added
|
||||
- A component (i.e. light) does not have to import every component
|
||||
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
|
||||
- Custom user components can take advantage of discovery/loading
|
||||
|
||||
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
|
||||
fired to load the platform. The event will contain:
|
||||
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
|
||||
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
|
||||
|
||||
* dev note: This listener can be found in entity_component.py
|
||||
"""
|
||||
if info is None:
|
||||
info = {LOAD_PLATFORM: platform}
|
||||
else:
|
||||
info[LOAD_PLATFORM] = platform
|
||||
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
|
||||
hass_config)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -119,20 +49,18 @@ def setup(hass, config):
|
||||
with lock:
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
component = SERVICE_HANDLERS.get(service)
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not component:
|
||||
if not comp_plat:
|
||||
return
|
||||
|
||||
# This component cannot be setup.
|
||||
if not bootstrap.setup_component(hass, component, config):
|
||||
return
|
||||
component, platform = comp_plat
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: service,
|
||||
ATTR_DISCOVERED: info
|
||||
})
|
||||
if platform is None:
|
||||
discover(hass, service, info, component, config)
|
||||
else:
|
||||
load_platform(hass, component, platform, info, config)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def start_discovery(event):
|
||||
|
@ -8,21 +8,18 @@ import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
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/'
|
||||
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
|
||||
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -70,23 +67,11 @@ def setup_ecobee(hass, network, config):
|
||||
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: {}
|
||||
})
|
||||
discovery.load_platform(hass, 'thermostat', DOMAIN,
|
||||
{'hold_temp': hold_temp}, config)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
BIN
homeassistant/components/frontend/www_static/favicon-512x512.png
Normal file
BIN
homeassistant/components/frontend/www_static/favicon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 0e6dc25fcd09ad1150aab258f8d01491a8ee8db7
|
||||
Subproject commit 168706fdb192219d8074d6709c0ce686180d1c8a
|
@ -4,6 +4,7 @@
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#03A9F4",
|
||||
"background_color": "#FFFFFF",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/favicon-192x192.png",
|
||||
@ -14,6 +15,16 @@
|
||||
"src": "/static/favicon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-1024x1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Binary file not shown.
@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (group, wink)
|
||||
from homeassistant.components import group
|
||||
|
||||
DOMAIN = 'garage_door'
|
||||
SCAN_INTERVAL = 30
|
||||
@ -27,11 +27,6 @@ ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_GARAGE_DOORS: 'wink'
|
||||
}
|
||||
|
||||
GARAGE_DOOR_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
@ -60,8 +55,7 @@ def open_door(hass, entity_id=None):
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for garage door."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_GARAGE_DOORS)
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_garage_door_service(service):
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -39,7 +39,7 @@ def _conf_preprocess(value):
|
||||
return value
|
||||
|
||||
_SINGLE_GROUP_CONFIG = vol.Schema(vol.All(_conf_preprocess, {
|
||||
vol.Optional(CONF_ENTITIES): vol.Any(None, cv.entity_ids),
|
||||
vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
|
||||
CONF_VIEW: bool,
|
||||
CONF_NAME: str,
|
||||
CONF_ICON: cv.icon,
|
||||
|
@ -14,7 +14,6 @@ import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELCIUS)
|
||||
@ -57,10 +56,6 @@ ATTR_SWING_LIST = "swing_list"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
zwave.DISCOVER_HVAC: 'zwave'
|
||||
}
|
||||
|
||||
|
||||
def set_away_mode(hass, away_mode, entity_id=None):
|
||||
"""Turn all or specified hvac away mode on."""
|
||||
@ -139,8 +134,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Setup hvacs."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
|
@ -8,7 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@ -68,18 +68,18 @@ def setup(hass, config):
|
||||
name = cfg.get(CONF_NAME)
|
||||
minimum = cfg.get(CONF_MIN)
|
||||
maximum = cfg.get(CONF_MAX)
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
step = cfg.get(CONF_STEP)
|
||||
state = cfg.get(CONF_INITIAL, minimum)
|
||||
step = cfg.get(CONF_STEP, 1)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if state < minimum:
|
||||
state = minimum
|
||||
if state > maximum:
|
||||
state = maximum
|
||||
|
||||
entities.append(
|
||||
InputSlider(object_id, name, state, minimum, maximum, step, icon)
|
||||
)
|
||||
entities.append(InputSlider(object_id, name, state, minimum, maximum,
|
||||
step, icon, unit))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
@ -103,8 +103,9 @@ def setup(hass, config):
|
||||
class InputSlider(Entity):
|
||||
"""Represent an slider."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, object_id, name, state, minimum, maximum, step, icon):
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, object_id, name, state, minimum, maximum, step, icon,
|
||||
unit):
|
||||
"""Initialize a select input."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
@ -113,6 +114,7 @@ class InputSlider(Entity):
|
||||
self._maximum = maximum
|
||||
self._step = step
|
||||
self._icon = icon
|
||||
self._unit = unit
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -134,6 +136,11 @@ class InputSlider(Entity):
|
||||
"""State of the component."""
|
||||
return self._current_value
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Unit of measurement of slider."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""State attributes."""
|
||||
|
@ -6,17 +6,12 @@ https://home-assistant.io/components/insteon_hub/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
|
||||
DOMAIN = "insteon_hub"
|
||||
REQUIREMENTS = ['insteon_hub==0.4.5']
|
||||
INSTEON = None
|
||||
DISCOVER_LIGHTS = "insteon_hub.lights"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -44,11 +39,7 @@ def setup(hass, config):
|
||||
_LOGGER.error("Could not connect to Insteon service.")
|
||||
return
|
||||
|
||||
comp_name = 'light'
|
||||
discovery = DISCOVER_LIGHTS
|
||||
component = get_component(comp_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
hass.bus.fire(
|
||||
EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
|
||||
for component in 'light':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
@ -7,19 +7,15 @@ https://home-assistant.io/components/isy994/
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "isy994"
|
||||
REQUIREMENTS = ['PyISY==1.0.6']
|
||||
DISCOVER_LIGHTS = "isy994.lights"
|
||||
DISCOVER_SWITCHES = "isy994.switches"
|
||||
DISCOVER_SENSORS = "isy994.sensors"
|
||||
|
||||
ISY = None
|
||||
SENSOR_STRING = 'Sensor'
|
||||
HIDDEN_STRING = '{HIDE ME}'
|
||||
@ -76,15 +72,9 @@ def setup(hass, config):
|
||||
# Listen for HA stop to disconnect.
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
|
||||
|
||||
# Load components for the devices in the ISY controller that we support.
|
||||
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
('switch', DISCOVER_SWITCHES))):
|
||||
component = get_component(comp_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery,
|
||||
ATTR_DISCOVERED: {}})
|
||||
# Load platforms for the devices in the ISY controller that we support.
|
||||
for component in ('sensor', 'light', 'switch'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
ISY.auto_update = True
|
||||
return True
|
||||
|
@ -10,9 +10,7 @@ import csv
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import (
|
||||
group, discovery, wemo, wink, isy994,
|
||||
zwave, insteon_hub, mysensors, tellstick, vera)
|
||||
from homeassistant.components import group
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
@ -60,19 +58,6 @@ EFFECT_WHITE = "white"
|
||||
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
||||
# Maps discovered services to their platforms.
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wemo.DISCOVER_LIGHTS: 'wemo',
|
||||
wink.DISCOVER_LIGHTS: 'wink',
|
||||
insteon_hub.DISCOVER_LIGHTS: 'insteon_hub',
|
||||
isy994.DISCOVER_LIGHTS: 'isy994',
|
||||
discovery.SERVICE_HUE: 'hue',
|
||||
zwave.DISCOVER_LIGHTS: 'zwave',
|
||||
mysensors.DISCOVER_LIGHTS: 'mysensors',
|
||||
tellstick.DISCOVER_LIGHTS: 'tellstick',
|
||||
vera.DISCOVER_LIGHTS: 'vera',
|
||||
}
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
'brightness': ATTR_BRIGHTNESS,
|
||||
'color_temp': ATTR_COLOR_TEMP,
|
||||
@ -172,8 +157,7 @@ def toggle(hass, entity_id=None, transition=None):
|
||||
def setup(hass, config):
|
||||
"""Expose light control via statemachine and services."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_LIGHTS)
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
|
||||
component.setup(config)
|
||||
|
||||
# Load built-in profiles and custom profiles
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.util import color as color_util
|
||||
from homeassistant.util.color import \
|
||||
color_temperature_mired_to_kelvin as mired_to_kelvin
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
@ -65,6 +65,7 @@ class LircInterface(threading.Thread):
|
||||
def run(self):
|
||||
"""Main loop of LIRC interface thread."""
|
||||
import lirc
|
||||
_LOGGER.debug('LIRC interface thread started')
|
||||
while not self.stopped.isSet():
|
||||
try:
|
||||
code = lirc.nextcode() # list; empty if no buttons pressed
|
||||
@ -80,4 +81,5 @@ class LircInterface(threading.Thread):
|
||||
{BUTTON_NAME: code})
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
_LOGGER.info('LIRC interface thread stopped')
|
||||
lirc.deinit()
|
||||
_LOGGER.debug('LIRC interface thread stopped')
|
||||
|
@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
|
||||
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
|
||||
from homeassistant.components import (group, verisure, wink, zwave)
|
||||
from homeassistant.components import group
|
||||
|
||||
DOMAIN = 'lock'
|
||||
SCAN_INTERVAL = 30
|
||||
@ -30,13 +30,6 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LOCKS: 'wink',
|
||||
verisure.DISCOVER_LOCKS: 'verisure',
|
||||
zwave.DISCOVER_LOCKS: 'zwave',
|
||||
}
|
||||
|
||||
LOCK_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_CODE): cv.string,
|
||||
@ -76,8 +69,7 @@ def unlock(hass, entity_id=None, code=None):
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for locks."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_LOCKS)
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_lock_service(service):
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -9,7 +9,6 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@ -30,15 +29,6 @@ SCAN_INTERVAL = 10
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_CAST: 'cast',
|
||||
discovery.SERVICE_SONOS: 'sonos',
|
||||
discovery.SERVICE_PLEX: 'plex',
|
||||
discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
|
||||
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
|
||||
discovery.SERVICE_ROKU: 'roku',
|
||||
}
|
||||
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
SERVICE_SELECT_SOURCE = 'select_source'
|
||||
|
||||
@ -285,8 +275,7 @@ def select_source(hass, source, entity_id=None):
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for media_players."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
MediaPlayerDevice)
|
||||
SUPPORT_STOP, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_UNKNOWN)
|
||||
@ -21,7 +21,7 @@ CONF_IGNORE_CEC = 'ignore_cec'
|
||||
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
||||
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP
|
||||
KNOWN_HOSTS = []
|
||||
|
||||
DEFAULT_PORT = 8009
|
||||
@ -241,6 +241,10 @@ class CastDevice(MediaPlayerDevice):
|
||||
"""Send pause command."""
|
||||
self.cast.media_controller.pause()
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
self.cast.media_controller.stop()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.cast.media_controller.rewind()
|
||||
|
368
homeassistant/components/media_player/pandora.py
Normal file
368
homeassistant/components/media_player/pandora.py
Normal file
@ -0,0 +1,368 @@
|
||||
"""
|
||||
Component for controlling Pandora stations through the pianobar client.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/media_player.pandora/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import signal
|
||||
from datetime import timedelta
|
||||
import shutil
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, MEDIA_TYPE_MUSIC,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
|
||||
SUPPORT_SELECT_SOURCE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_IDLE)
|
||||
from homeassistant import util
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# SUPPORT_VOLUME_SET is close to available but we need volume up/down
|
||||
# controls in the GUI.
|
||||
PANDORA_SUPPORT = \
|
||||
SUPPORT_PAUSE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_SELECT_SOURCE
|
||||
|
||||
CMD_MAP = {SERVICE_MEDIA_NEXT_TRACK: 'n',
|
||||
SERVICE_MEDIA_PLAY_PAUSE: 'p',
|
||||
SERVICE_MEDIA_PLAY: 'p',
|
||||
SERVICE_VOLUME_UP: ')',
|
||||
SERVICE_VOLUME_DOWN: '('}
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2)
|
||||
CURRENT_SONG_PATTERN = re.compile(r'"(.*?)"\s+by\s+"(.*?)"\son\s+"(.*?)"',
|
||||
re.MULTILINE)
|
||||
STATION_PATTERN = re.compile(r'Station\s"(.+?)"', re.MULTILINE)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the media player pandora platform."""
|
||||
if not _pianobar_exists():
|
||||
return False
|
||||
pandora = PandoraMediaPlayer('Pandora')
|
||||
|
||||
# make sure we end the pandora subprocess on exit in case user doesn't
|
||||
# power it down.
|
||||
def _stop_pianobar(_event):
|
||||
pandora.turn_off()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_pianobar)
|
||||
add_devices([pandora])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class PandoraMediaPlayer(MediaPlayerDevice):
|
||||
"""A media player that uses the Pianobar interface to Pandora."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
def __init__(self, name):
|
||||
"""Initialize the demo device."""
|
||||
MediaPlayerDevice.__init__(self)
|
||||
self._name = name
|
||||
self._player_state = STATE_OFF
|
||||
self._station = ''
|
||||
self._media_title = ''
|
||||
self._media_artist = ''
|
||||
self._media_album = ''
|
||||
self._stations = []
|
||||
self._time_remaining = 0
|
||||
self._media_duration = 0
|
||||
self._pianobar = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Should be polled for current state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the media player."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the player."""
|
||||
return self._player_state
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
import pexpect
|
||||
if self._player_state != STATE_OFF:
|
||||
return
|
||||
self._pianobar = pexpect.spawn('pianobar')
|
||||
_LOGGER.info('Started pianobar subprocess')
|
||||
mode = self._pianobar.expect(['Receiving new playlist',
|
||||
'Select station:',
|
||||
'Email:'])
|
||||
if mode == 1:
|
||||
# station list was presented. dismiss it.
|
||||
self._pianobar.sendcontrol('m')
|
||||
elif mode == 2:
|
||||
_LOGGER.warning('The pianobar client is not configured to log in. '
|
||||
'Please create a config file for it as described '
|
||||
'at https://home-assistant.io'
|
||||
'/components/media_player.pandora/')
|
||||
# pass through the email/password prompts to quit cleanly
|
||||
self._pianobar.sendcontrol('m')
|
||||
self._pianobar.sendcontrol('m')
|
||||
self._pianobar.terminate()
|
||||
self._pianobar = None
|
||||
return
|
||||
self._update_stations()
|
||||
self.update_playing_status()
|
||||
|
||||
self._player_state = STATE_IDLE
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
import pexpect
|
||||
if self._pianobar is None:
|
||||
_LOGGER.info('Pianobar subprocess already stopped')
|
||||
return
|
||||
self._pianobar.send('q')
|
||||
try:
|
||||
_LOGGER.info('Stopped Pianobar subprocess')
|
||||
self._pianobar.terminate()
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
# kill the process group
|
||||
os.killpg(os.getpgid(self._pianobar.pid), signal.SIGTERM)
|
||||
_LOGGER.info('Killed Pianobar subprocess')
|
||||
self._pianobar = None
|
||||
self._player_state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
||||
self._player_state = STATE_PLAYING
|
||||
self.update_ha_state()
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
||||
self._player_state = STATE_PAUSED
|
||||
self.update_ha_state()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Go to next track."""
|
||||
self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK)
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Show what this supports."""
|
||||
return PANDORA_SUPPORT
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Name of the current input source."""
|
||||
return self._station
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._stations
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
self.update_playing_status()
|
||||
return self._media_title
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self._media_artist
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self._media_album
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self._media_duration
|
||||
|
||||
def select_source(self, source):
|
||||
"""Choose a different Pandora station and play it."""
|
||||
try:
|
||||
station_index = self._stations.index(source)
|
||||
except ValueError:
|
||||
_LOGGER.warning('Station `%s` is not in list', source)
|
||||
return
|
||||
_LOGGER.info('Setting station %s, %d', source, station_index)
|
||||
self._send_station_list_command()
|
||||
self._pianobar.sendline('{}'.format(station_index))
|
||||
self._pianobar.expect('\r\n')
|
||||
self._player_state = STATE_PLAYING
|
||||
|
||||
def _send_station_list_command(self):
|
||||
"""Send a station list command."""
|
||||
import pexpect
|
||||
self._pianobar.send('s')
|
||||
try:
|
||||
self._pianobar.expect('Select station:', timeout=1)
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
# try again. Buffer was contaminated.
|
||||
self._clear_buffer()
|
||||
self._pianobar.send('s')
|
||||
self._pianobar.expect('Select station:')
|
||||
|
||||
def update_playing_status(self):
|
||||
"""Query pianobar for info about current media_title, station."""
|
||||
response = self._query_for_playing_status()
|
||||
if not response:
|
||||
return
|
||||
self._update_current_station(response)
|
||||
self._update_current_song(response)
|
||||
self._update_song_position()
|
||||
|
||||
def _query_for_playing_status(self):
|
||||
"""Query system for info about current track."""
|
||||
import pexpect
|
||||
self._clear_buffer()
|
||||
self._pianobar.send('i')
|
||||
try:
|
||||
match_idx = self._pianobar.expect([br'(\d\d):(\d\d)/(\d\d):(\d\d)',
|
||||
'No song playing',
|
||||
'Select station',
|
||||
'Receiving new playlist'])
|
||||
except pexpect.exceptions.EOF:
|
||||
_LOGGER.info('Pianobar process already exited.')
|
||||
return None
|
||||
|
||||
self._log_match()
|
||||
if match_idx == 1:
|
||||
# idle.
|
||||
response = None
|
||||
elif match_idx == 2:
|
||||
# stuck on a station selection dialog. Clear it.
|
||||
_LOGGER.warning('On unexpected station list page.')
|
||||
self._pianobar.sendcontrol('m') # press enter
|
||||
self._pianobar.sendcontrol('m') # do it again b/c an 'i' got in
|
||||
response = self.update_playing_status()
|
||||
elif match_idx == 3:
|
||||
_LOGGER.debug('Received new playlist list.')
|
||||
response = self.update_playing_status()
|
||||
else:
|
||||
response = self._pianobar.before.decode('utf-8')
|
||||
return response
|
||||
|
||||
def _update_current_station(self, response):
|
||||
"""Update current station."""
|
||||
station_match = re.search(STATION_PATTERN, response)
|
||||
if station_match:
|
||||
self._station = station_match.group(1)
|
||||
_LOGGER.debug('Got station as: %s', self._station)
|
||||
else:
|
||||
_LOGGER.warning('No station match. ')
|
||||
|
||||
def _update_current_song(self, response):
|
||||
"""Update info about current song."""
|
||||
song_match = re.search(CURRENT_SONG_PATTERN, response)
|
||||
if song_match:
|
||||
(self._media_title, self._media_artist,
|
||||
self._media_album) = song_match.groups()
|
||||
_LOGGER.debug('Got song as: %s', self._media_title)
|
||||
else:
|
||||
_LOGGER.warning('No song match.')
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_song_position(self):
|
||||
"""
|
||||
Get the song position and duration.
|
||||
|
||||
It's hard to predict whether or not the music will start during init
|
||||
so we have to detect state by checking the ticker.
|
||||
|
||||
"""
|
||||
(cur_minutes, cur_seconds,
|
||||
total_minutes, total_seconds) = self._pianobar.match.groups()
|
||||
time_remaining = int(cur_minutes) * 60 + int(cur_seconds)
|
||||
self._media_duration = int(total_minutes) * 60 + int(total_seconds)
|
||||
|
||||
if (time_remaining != self._time_remaining and
|
||||
time_remaining != self._media_duration):
|
||||
self._player_state = STATE_PLAYING
|
||||
elif self._player_state == STATE_PLAYING:
|
||||
self._player_state = STATE_PAUSED
|
||||
self._time_remaining = time_remaining
|
||||
|
||||
def _log_match(self):
|
||||
"""Log grabbed values from console."""
|
||||
_LOGGER.debug('Before: %s\nMatch: %s\nAfter: %s',
|
||||
repr(self._pianobar.before),
|
||||
repr(self._pianobar.match),
|
||||
repr(self._pianobar.after))
|
||||
|
||||
def _send_pianobar_command(self, service_cmd):
|
||||
"""Send a command to Pianobar."""
|
||||
command = CMD_MAP.get(service_cmd)
|
||||
_LOGGER.debug('Sending pinaobar command %s for %s',
|
||||
command, service_cmd)
|
||||
if command is None:
|
||||
_LOGGER.info('Command %s not supported yet', service_cmd)
|
||||
self._clear_buffer()
|
||||
self._pianobar.sendline(command)
|
||||
|
||||
def _update_stations(self):
|
||||
"""List defined Pandora stations."""
|
||||
self._send_station_list_command()
|
||||
station_lines = self._pianobar.before.decode('utf-8')
|
||||
_LOGGER.debug('Getting stations: %s', station_lines)
|
||||
self._stations = []
|
||||
for line in station_lines.split('\r\n'):
|
||||
match = re.search(r'\d+\).....(.+)', line)
|
||||
if match:
|
||||
station = match.group(1).strip()
|
||||
_LOGGER.debug('Found station %s', station)
|
||||
self._stations.append(station)
|
||||
else:
|
||||
_LOGGER.debug('No station match on `%s`', line)
|
||||
self._pianobar.sendcontrol('m') # press enter with blank line
|
||||
self._pianobar.sendcontrol('m') # do it twice in case an 'i' got in
|
||||
|
||||
def _clear_buffer(self):
|
||||
"""
|
||||
Clear buffer from pexpect.
|
||||
|
||||
This is necessary because there are a bunch of 00:00 in the buffer
|
||||
|
||||
"""
|
||||
import pexpect
|
||||
try:
|
||||
while not self._pianobar.expect('.+', timeout=0.1):
|
||||
pass
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
pass
|
||||
|
||||
|
||||
def _pianobar_exists():
|
||||
"""Verify that Pianobar is properly installed."""
|
||||
pianobar_exe = shutil.which('pianobar')
|
||||
if pianobar_exe:
|
||||
return True
|
||||
else:
|
||||
_LOGGER.warning('The Pandora component depends on the Pianobar '
|
||||
'client, which cannot be found. Please install '
|
||||
'using instructions at'
|
||||
'https://home-assistant.io'
|
||||
'/components/media_player.pandora/')
|
||||
return False
|
@ -72,7 +72,7 @@ class SamsungTVDevice(MediaPlayerDevice):
|
||||
def update(self):
|
||||
"""Retrieve the latest data."""
|
||||
# Send an empty key to see if we are still connected
|
||||
return self.send_key('KEY_POWER')
|
||||
return self.send_key('KEY')
|
||||
|
||||
def get_remote(self):
|
||||
"""Create or return a remote control instance."""
|
||||
|
@ -153,3 +153,19 @@ sonos_group_players:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_snapshot:
|
||||
description: Take a snapshot of the media player.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_restore:
|
||||
description: Restore a snapshot of the media player.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
@ -34,6 +34,8 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_SEEK
|
||||
|
||||
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
|
||||
SERVICE_SNAPSHOT = 'sonos_snapshot'
|
||||
SERVICE_RESTORE = 'sonos_restore'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@ -84,6 +86,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
device.group_players()
|
||||
device.update_ha_state(True)
|
||||
|
||||
def snapshot(service):
|
||||
"""Take a snapshot."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.snapshot(service)
|
||||
device.update_ha_state(True)
|
||||
|
||||
def restore(service):
|
||||
"""Restore a snapshot."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.restore(service)
|
||||
device.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@ -91,6 +121,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
group_players_service,
|
||||
descriptions.get(SERVICE_GROUP_PLAYERS))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
|
||||
snapshot,
|
||||
descriptions.get(SERVICE_SNAPSHOT))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RESTORE,
|
||||
restore,
|
||||
descriptions.get(SERVICE_RESTORE))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -136,6 +174,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||
super(SonosDevice, self).__init__()
|
||||
self._player = player
|
||||
self.update()
|
||||
from soco.snapshot import Snapshot
|
||||
self.soco_snapshot = Snapshot(self._player)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -315,6 +355,16 @@ class SonosDevice(MediaPlayerDevice):
|
||||
"""Group all players under this coordinator."""
|
||||
self._player.partymode()
|
||||
|
||||
@only_if_coordinator
|
||||
def snapshot(self, service):
|
||||
"""Snapshot the player."""
|
||||
self.soco_snapshot.snapshot()
|
||||
|
||||
@only_if_coordinator
|
||||
def restore(self, service):
|
||||
"""Restore snapshot for the player."""
|
||||
self.soco_snapshot.restore(True)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if player is reachable, False otherwise."""
|
||||
|
@ -7,14 +7,11 @@ https://home-assistant.io/components/sensor.mysensors/
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.const import (ATTR_BATTERY_LEVEL, ATTR_DISCOVERED,
|
||||
ATTR_SERVICE, CONF_OPTIMISTIC,
|
||||
from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_PLATFORM_DISCOVERED, STATE_OFF,
|
||||
STATE_ON, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import validate_config
|
||||
STATE_OFF, STATE_ON, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
|
||||
CONF_GATEWAYS = 'gateways'
|
||||
CONF_DEVICE = 'device'
|
||||
@ -40,19 +37,6 @@ ATTR_DEVICE = 'device'
|
||||
|
||||
GATEWAYS = None
|
||||
|
||||
DISCOVER_SENSORS = 'mysensors.sensors'
|
||||
DISCOVER_SWITCHES = 'mysensors.switches'
|
||||
DISCOVER_LIGHTS = 'mysensors.lights'
|
||||
DISCOVER_BINARY_SENSORS = 'mysensors.binary_sensor'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_COMPONENTS = [
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config): # pylint: disable=too-many-locals
|
||||
"""Setup the MySensors component."""
|
||||
@ -124,14 +108,8 @@ def setup(hass, config): # pylint: disable=too-many-locals
|
||||
GATEWAYS[device] = setup_gateway(
|
||||
device, persistence_file, baud_rate, tcp_port)
|
||||
|
||||
for (component, discovery_service) in DISCOVERY_COMPONENTS:
|
||||
# Ensure component is loaded
|
||||
if not bootstrap.setup_component(hass, component, config):
|
||||
return False
|
||||
# Fire discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: discovery_service,
|
||||
ATTR_DISCOVERED: {}})
|
||||
for component in 'sensor', 'switch', 'light', 'binary_sensor':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
56
homeassistant/components/netatmo.py
Normal file
56
homeassistant/components/netatmo.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""
|
||||
Support for the Netatmo devices (Weather Station and Welcome camera).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/netatmo/
|
||||
"""
|
||||
import logging
|
||||
from urllib.error import HTTPError
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/jabesq/netatmo-api-python/archive/'
|
||||
'v0.5.0.zip#lnetatmo==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SECRET_KEY = 'secret_key'
|
||||
|
||||
DOMAIN = "netatmo"
|
||||
NETATMO_AUTH = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Netatmo devices."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_API_KEY,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SECRET_KEY]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
import lnetatmo
|
||||
|
||||
global NETATMO_AUTH
|
||||
try:
|
||||
NETATMO_AUTH = lnetatmo.ClientAuth(config[DOMAIN][CONF_API_KEY],
|
||||
config[DOMAIN][CONF_SECRET_KEY],
|
||||
config[DOMAIN][CONF_USERNAME],
|
||||
config[DOMAIN][CONF_PASSWORD],
|
||||
"read_station read_camera "
|
||||
"access_camera")
|
||||
except HTTPError:
|
||||
_LOGGER.error(
|
||||
"Connection error "
|
||||
"Please check your settings for NatAtmo API.")
|
||||
return False
|
||||
|
||||
for component in 'camera', 'sensor':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-telegram-bot==4.2.0']
|
||||
REQUIREMENTS = ['python-telegram-bot==4.2.1']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
|
@ -9,9 +9,8 @@ import logging
|
||||
import time
|
||||
import requests
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
|
||||
DOMAIN = "octoprint"
|
||||
OCTOPRINT = None
|
||||
|
@ -29,9 +29,6 @@ ENTITY_ID_ALL_ROLLERSHUTTERS = group.ENTITY_ID_FORMAT.format(
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CURRENT_POSITION = 'current_position'
|
||||
@ -68,8 +65,7 @@ def stop(hass, entity_id=None):
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for roller shutters."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_ROLLERSHUTTERS)
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_ROLLERSHUTTERS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_rollershutter_service(service):
|
||||
|
92
homeassistant/components/rollershutter/wink.py
Normal file
92
homeassistant/components/rollershutter/wink.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for Wink Shades.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/rollershutter.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink rollershutter platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token. "
|
||||
"Get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices(WinkRollershutterDevice(shade) for shade in
|
||||
pywink.get_shades())
|
||||
|
||||
|
||||
class WinkRollershutterDevice(RollershutterDevice):
|
||||
"""Representation of a Wink rollershutter (shades)."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the rollershutter."""
|
||||
self.wink = wink
|
||||
self._battery = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Wink Shades don't track their position."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink rollershutter."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the rollershutter if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the rollershutter."""
|
||||
return self.wink.update_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def move_down(self):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
def move_up(self):
|
||||
"""Open the shade."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""Return current position of roller shutter.
|
||||
|
||||
Wink reports blind shade positions as 0 or 1.
|
||||
home-assistant expects:
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
state = self.wink.state()
|
||||
if state == 0:
|
||||
return 0
|
||||
elif state == 1:
|
||||
return 100
|
||||
else:
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
"""Can't stop Wink rollershutter due to API."""
|
||||
pass
|
@ -8,34 +8,17 @@ import logging
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components import (
|
||||
wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors,
|
||||
bloomsky, vera)
|
||||
|
||||
DOMAIN = 'sensor'
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_SENSORS: 'bloomsky',
|
||||
wink.DISCOVER_SENSORS: 'wink',
|
||||
zwave.DISCOVER_SENSORS: 'zwave',
|
||||
isy994.DISCOVER_SENSORS: 'isy994',
|
||||
verisure.DISCOVER_SENSORS: 'verisure',
|
||||
ecobee.DISCOVER_SENSORS: 'ecobee',
|
||||
tellduslive.DISCOVER_SENSORS: 'tellduslive',
|
||||
mysensors.DISCOVER_SENSORS: 'mysensors',
|
||||
vera.DISCOVER_SENSORS: 'vera',
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
|
@ -17,16 +17,18 @@ SENSOR_TYPES = ["Temperature",
|
||||
"Humidity",
|
||||
"Pressure",
|
||||
"Luminance",
|
||||
"UVIndex"]
|
||||
"UVIndex",
|
||||
"Voltage"]
|
||||
|
||||
# Sensor units - these do not currently align with the API documentation
|
||||
SENSOR_UNITS = {"Temperature": TEMP_FAHRENHEIT,
|
||||
"Humidity": "%",
|
||||
"Pressure": "inHg",
|
||||
"Luminance": "cd/m²"}
|
||||
"Luminance": "cd/m²",
|
||||
"Voltage": "mV"}
|
||||
|
||||
# Which sensors to format numerically
|
||||
FORMAT_NUMBERS = ["Temperature", "Pressure"]
|
||||
FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -6,8 +6,12 @@ https://home-assistant.io/components/sensor.forecast/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from requests.exceptions import ConnectionError as ConnectError, \
|
||||
HTTPError, Timeout
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@ -48,21 +52,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Forecast.io sensor."""
|
||||
import forecastio
|
||||
|
||||
# Validate the configuration
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
|
||||
try:
|
||||
forecast = forecastio.load_forecast(config.get(CONF_API_KEY, None),
|
||||
hass.config.latitude,
|
||||
hass.config.longitude)
|
||||
forecast.currently()
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Connection error "
|
||||
"Please check your settings for Forecast.io.")
|
||||
elif not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_API_KEY]}, _LOGGER):
|
||||
return False
|
||||
|
||||
if 'units' in config:
|
||||
@ -72,43 +67,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
else:
|
||||
units = 'us'
|
||||
|
||||
data = ForeCastData(config.get(CONF_API_KEY, None),
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
units)
|
||||
# Create a data fetcher to support all of the configured sensors. Then make
|
||||
# the first call to init the data and confirm we can connect.
|
||||
try:
|
||||
forecast_data = ForeCastData(
|
||||
config.get(CONF_API_KEY, None), hass.config.latitude,
|
||||
hass.config.longitude, units)
|
||||
forecast_data.update_currently()
|
||||
except ValueError as error:
|
||||
_LOGGER.error(error)
|
||||
return False
|
||||
|
||||
dev = []
|
||||
# Initialize and add all of the sensors.
|
||||
sensors = []
|
||||
for variable in config['monitored_conditions']:
|
||||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
if variable in SENSOR_TYPES:
|
||||
sensors.append(ForeCastSensor(forecast_data, variable))
|
||||
else:
|
||||
dev.append(ForeCastSensor(data, variable))
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
|
||||
add_devices(dev)
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ForeCastSensor(Entity):
|
||||
"""Implementation of a Forecast.io sensor."""
|
||||
|
||||
def __init__(self, weather_data, sensor_type):
|
||||
def __init__(self, forecast_data, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self.client_name = 'Weather'
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.forecast_client = weather_data
|
||||
self.forecast_data = forecast_data
|
||||
self.type = sensor_type
|
||||
self._state = None
|
||||
self._unit_system = self.forecast_client.unit_system
|
||||
if self._unit_system == 'si':
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
||||
elif self._unit_system == 'us':
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][2]
|
||||
elif self._unit_system == 'ca':
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][3]
|
||||
elif self._unit_system == 'uk':
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][4]
|
||||
elif self._unit_system == 'uk2':
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][5]
|
||||
self._unit_of_measurement = None
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@ -129,75 +122,72 @@ class ForeCastSensor(Entity):
|
||||
@property
|
||||
def unit_system(self):
|
||||
"""Return the unit system of this entity."""
|
||||
return self._unit_system
|
||||
return self.forecast_data.unit_system
|
||||
|
||||
def update_unit_of_measurement(self):
|
||||
"""Update units based on unit system."""
|
||||
unit_index = {
|
||||
'si': 1,
|
||||
'us': 2,
|
||||
'ca': 3,
|
||||
'uk': 4,
|
||||
'uk2': 5
|
||||
}.get(self.unit_system, 1)
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][unit_index]
|
||||
|
||||
# pylint: disable=too-many-branches,too-many-statements
|
||||
def update(self):
|
||||
"""Get the latest data from Forecast.io and updates the states."""
|
||||
import forecastio
|
||||
# Call the API for new forecast data. Each sensor will re-trigger this
|
||||
# same exact call, but thats fine. We cache results for a short period
|
||||
# of time to prevent hitting API limits. Note that forecast.io will
|
||||
# charge users for too many calls in 1 day, so take care when updating.
|
||||
self.forecast_data.update()
|
||||
self.update_unit_of_measurement()
|
||||
|
||||
self.forecast_client.update()
|
||||
if self.type == 'minutely_summary':
|
||||
self.forecast_data.update_minutely()
|
||||
minutely = self.forecast_data.data_minutely
|
||||
self._state = getattr(minutely, 'summary', '')
|
||||
elif self.type == 'hourly_summary':
|
||||
self.forecast_data.update_hourly()
|
||||
hourly = self.forecast_data.data_hourly
|
||||
self._state = getattr(hourly, 'summary', '')
|
||||
elif self.type == 'daily_summary':
|
||||
self.forecast_data.update_daily()
|
||||
daily = self.forecast_data.data_daily
|
||||
self._state = getattr(daily, 'summary', '')
|
||||
else:
|
||||
self.forecast_data.update_currently()
|
||||
currently = self.forecast_data.data_currently
|
||||
self._state = self.get_currently_state(currently)
|
||||
|
||||
try:
|
||||
if self.type == 'minutely_summary':
|
||||
self.forecast_client.update_minutely()
|
||||
self._state = self.forecast_client.data_minutely.summary
|
||||
return
|
||||
def get_currently_state(self, data):
|
||||
"""
|
||||
Helper function that returns a new state based on the type.
|
||||
|
||||
elif self.type == 'hourly_summary':
|
||||
self.forecast_client.update_hourly()
|
||||
self._state = self.forecast_client.data_hourly.summary
|
||||
return
|
||||
If the sensor type is unknown, the current state is returned.
|
||||
"""
|
||||
lookup_type = convert_to_camel(self.type)
|
||||
state = getattr(data, lookup_type, 0)
|
||||
|
||||
elif self.type == 'daily_summary':
|
||||
self.forecast_client.update_daily()
|
||||
self._state = self.forecast_client.data_daily.summary
|
||||
return
|
||||
# Some state data needs to be rounded to whole values or converted to
|
||||
# percentages
|
||||
if self.type in ['precip_probability', 'cloud_cover', 'humidity']:
|
||||
return round(state * 100, 1)
|
||||
elif (self.type in ['dew_point', 'temperature', 'apparent_temperature',
|
||||
'pressure', 'ozone']):
|
||||
return round(state, 1)
|
||||
return state
|
||||
|
||||
except forecastio.utils.PropertyUnavailable:
|
||||
return
|
||||
|
||||
self.forecast_client.update_currently()
|
||||
data = self.forecast_client.data_currently
|
||||
def convert_to_camel(data):
|
||||
"""
|
||||
Convert snake case (foo_bar_bat) to camel case (fooBarBat).
|
||||
|
||||
try:
|
||||
if self.type == 'summary':
|
||||
self._state = data.summary
|
||||
elif self.type == 'icon':
|
||||
self._state = data.icon
|
||||
elif self.type == 'nearest_storm_distance':
|
||||
self._state = data.nearestStormDistance
|
||||
elif self.type == 'nearest_storm_bearing':
|
||||
self._state = data.nearestStormBearing
|
||||
elif self.type == 'precip_intensity':
|
||||
self._state = data.precipIntensity
|
||||
elif self.type == 'precip_type':
|
||||
self._state = data.precipType
|
||||
elif self.type == 'precip_probability':
|
||||
self._state = round(data.precipProbability * 100, 1)
|
||||
elif self.type == 'dew_point':
|
||||
self._state = round(data.dewPoint, 1)
|
||||
elif self.type == 'temperature':
|
||||
self._state = round(data.temperature, 1)
|
||||
elif self.type == 'apparent_temperature':
|
||||
self._state = round(data.apparentTemperature, 1)
|
||||
elif self.type == 'wind_speed':
|
||||
self._state = data.windSpeed
|
||||
elif self.type == 'wind_bearing':
|
||||
self._state = data.windBearing
|
||||
elif self.type == 'cloud_cover':
|
||||
self._state = round(data.cloudCover * 100, 1)
|
||||
elif self.type == 'humidity':
|
||||
self._state = round(data.humidity * 100, 1)
|
||||
elif self.type == 'pressure':
|
||||
self._state = round(data.pressure, 1)
|
||||
elif self.type == 'visibility':
|
||||
self._state = data.visibility
|
||||
elif self.type == 'ozone':
|
||||
self._state = round(data.ozone, 1)
|
||||
|
||||
except forecastio.utils.PropertyUnavailable:
|
||||
pass
|
||||
This is not pythonic, but needed for certain situations
|
||||
"""
|
||||
components = data.split('_')
|
||||
return components[0] + "".join(x.title() for x in components[1:])
|
||||
|
||||
|
||||
class ForeCastData(object):
|
||||
@ -226,10 +216,13 @@ class ForeCastData(object):
|
||||
"""Get the latest data from Forecast.io."""
|
||||
import forecastio
|
||||
|
||||
self.data = forecastio.load_forecast(self._api_key,
|
||||
self.latitude,
|
||||
self.longitude,
|
||||
units=self.units)
|
||||
try:
|
||||
self.data = forecastio.load_forecast(self._api_key,
|
||||
self.latitude,
|
||||
self.longitude,
|
||||
units=self.units)
|
||||
except (ConnectError, HTTPError, Timeout, ValueError) as error:
|
||||
raise ValueError("Unable to init Forecast.io. - %s", error)
|
||||
self.unit_system = self.data.json['flags']['units']
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
|
@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pres.S_POWER: [set_req.V_WATT, set_req.V_KWH],
|
||||
pres.S_DISTANCE: [set_req.V_DISTANCE],
|
||||
pres.S_LIGHT_LEVEL: [set_req.V_LIGHT_LEVEL],
|
||||
pres.S_IR: [set_req.V_IR_SEND, set_req.V_IR_RECEIVE],
|
||||
pres.S_IR: [set_req.V_IR_RECEIVE],
|
||||
pres.S_WATER: [set_req.V_FLOW, set_req.V_VOLUME],
|
||||
pres.S_CUSTOM: [set_req.V_VAR1,
|
||||
set_req.V_VAR2,
|
||||
|
@ -6,18 +6,12 @@ https://home-assistant.io/components/sensor.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/HydrelioxGitHub/netatmo-api-python/archive/'
|
||||
'43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip'
|
||||
'#lnetatmo==0.4.0']
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -32,7 +26,6 @@ SENSOR_TYPES = {
|
||||
'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'],
|
||||
}
|
||||
|
||||
CONF_SECRET_KEY = 'secret_key'
|
||||
CONF_STATION = 'station'
|
||||
ATTR_MODULE = 'modules'
|
||||
|
||||
@ -43,29 +36,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the NetAtmo sensor."""
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_API_KEY,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SECRET_KEY]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
import lnetatmo
|
||||
|
||||
authorization = lnetatmo.ClientAuth(config.get(CONF_API_KEY, None),
|
||||
config.get(CONF_SECRET_KEY, None),
|
||||
config.get(CONF_USERNAME, None),
|
||||
config.get(CONF_PASSWORD, None))
|
||||
|
||||
if not authorization:
|
||||
_LOGGER.error(
|
||||
"Connection error "
|
||||
"Please check your settings for NatAtmo API.")
|
||||
return False
|
||||
|
||||
data = NetAtmoData(authorization, config.get(CONF_STATION, None))
|
||||
"""Setup the available Netatmo weather sensors."""
|
||||
netatmo = get_component('netatmo')
|
||||
data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None))
|
||||
|
||||
dev = []
|
||||
try:
|
||||
|
@ -158,7 +158,7 @@ class NZBGetSensor(Entity):
|
||||
return
|
||||
|
||||
if "DownloadRate" in self.type and value > 0:
|
||||
# Convert download rate from bytes/s to mb/s
|
||||
self._state = value / 1024 / 1024
|
||||
# Convert download rate from Bytes/s to MBytes/s
|
||||
self._state = round(value / 1024 / 1024, 2)
|
||||
else:
|
||||
self._state = value
|
||||
|
@ -10,8 +10,7 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_PLATFORM, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_MONITORED_CONDITIONS)
|
||||
CONF_PLATFORM, CONF_MONITORED_CONDITIONS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
@ -34,8 +33,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_API_KEY): vol.Coerce(str),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
[vol.In(SENSOR_TYPES.keys())],
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude
|
||||
vol.Optional('forecast', default=False): cv.boolean
|
||||
})
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
@ -52,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
|
||||
unit = hass.config.temperature_unit
|
||||
forecast = config.get('forecast', 0)
|
||||
forecast = config.get('forecast')
|
||||
owm = OWM(config.get(CONF_API_KEY, None))
|
||||
|
||||
if not owm:
|
||||
@ -73,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if forecast == 1:
|
||||
if forecast:
|
||||
SENSOR_TYPES['forecast'] = ['Forecast', None]
|
||||
dev.append(OpenWeatherMapSensor(data, 'forecast', unit))
|
||||
|
||||
|
99
homeassistant/components/sensor/plex.py
Normal file
99
homeassistant/components/sensor/plex.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""
|
||||
Support for Plex media server monitoring.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.plex/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_USERNAME,
|
||||
CONF_PASSWORD, CONF_HOST, CONF_PORT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['plexapi==1.1.0']
|
||||
|
||||
CONF_SERVER = 'server'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'plex',
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SERVER): cv.string,
|
||||
vol.Optional(CONF_NAME, default='Plex'): cv.string,
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_PORT, default=32400): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1,
|
||||
max=65535))
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo sensors."""
|
||||
name = config.get(CONF_NAME)
|
||||
plex_user = config.get(CONF_USERNAME)
|
||||
plex_password = config.get(CONF_PASSWORD)
|
||||
plex_server = config.get(CONF_SERVER)
|
||||
plex_host = config.get(CONF_HOST)
|
||||
plex_port = config.get(CONF_PORT)
|
||||
plex_url = 'http://' + plex_host + ':' + str(plex_port)
|
||||
add_devices([PlexSensor(name, plex_url, plex_user,
|
||||
plex_password, plex_server)])
|
||||
|
||||
|
||||
class PlexSensor(Entity):
|
||||
"""Plex now playing sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, plex_url, plex_user, plex_password, plex_server):
|
||||
"""Initialize the sensor."""
|
||||
self._name = name
|
||||
self._state = 0
|
||||
self._now_playing = []
|
||||
|
||||
if plex_user and plex_password:
|
||||
from plexapi.myplex import MyPlexUser
|
||||
user = MyPlexUser.signin(plex_user, plex_password)
|
||||
server = plex_server if plex_server else user.resources()[0].name
|
||||
self._server = user.getResource(server).connect()
|
||||
else:
|
||||
from plexapi.server import PlexServer
|
||||
self._server = PlexServer(plex_url)
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return "Watching"
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {content[0]: content[1] for content in self._now_playing}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update method for plex sensor."""
|
||||
sessions = self._server.sessions()
|
||||
now_playing = [(s.user.title, "{0} ({1})".format(s.title, s.year))
|
||||
for s in sessions]
|
||||
self._state = len(sessions)
|
||||
self._now_playing = now_playing
|
134
homeassistant/components/sensor/snmp.py
Normal file
134
homeassistant/components/sensor/snmp.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""
|
||||
Support for displaying collected data over SNMP.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.snmp/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (CONF_HOST, CONF_PLATFORM, CONF_NAME,
|
||||
CONF_PORT, ATTR_UNIT_OF_MEASUREMENT)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "SNMP"
|
||||
DEFAULT_COMMUNITY = "public"
|
||||
DEFAULT_PORT = "161"
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_BASEOID = "baseoid"
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'snmp',
|
||||
vol.Optional(CONF_NAME): vol.Coerce(str),
|
||||
vol.Required(CONF_HOST): vol.Coerce(str),
|
||||
vol.Optional(CONF_PORT): vol.Coerce(int),
|
||||
vol.Optional(CONF_COMMUNITY): vol.Coerce(str),
|
||||
vol.Required(CONF_BASEOID): vol.Coerce(str),
|
||||
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): vol.Coerce(str),
|
||||
})
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the SNMP sensor."""
|
||||
from pysnmp.hlapi import (getCmd, CommunityData, SnmpEngine,
|
||||
UdpTransportTarget, ContextData, ObjectType,
|
||||
ObjectIdentity)
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT, DEFAULT_PORT)
|
||||
community = config.get(CONF_COMMUNITY, DEFAULT_COMMUNITY)
|
||||
baseoid = config.get(CONF_BASEOID)
|
||||
|
||||
errindication, _, _, _ = next(
|
||||
getCmd(SnmpEngine(),
|
||||
CommunityData(community, mpModel=0),
|
||||
UdpTransportTarget((host, port)),
|
||||
ContextData(),
|
||||
ObjectType(ObjectIdentity(baseoid))))
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error('Please check the details in the configuration file')
|
||||
return False
|
||||
else:
|
||||
data = SnmpData(host, port, community, baseoid)
|
||||
add_devices([SnmpSensor(data,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('unit_of_measurement'))])
|
||||
|
||||
|
||||
class SnmpSensor(Entity):
|
||||
"""Representation of a SNMP sensor."""
|
||||
|
||||
def __init__(self, data, name, unit_of_measurement):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
self.data.update()
|
||||
self._state = self.data.value
|
||||
|
||||
|
||||
class SnmpData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
def __init__(self, host, port, community, baseoid):
|
||||
"""Initialize the data object."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._community = community
|
||||
self._baseoid = baseoid
|
||||
self.value = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from the remote SNMP capable host."""
|
||||
from pysnmp.hlapi import (getCmd, CommunityData, SnmpEngine,
|
||||
UdpTransportTarget, ContextData, ObjectType,
|
||||
ObjectIdentity)
|
||||
errindication, errstatus, errindex, restable = next(
|
||||
getCmd(SnmpEngine(),
|
||||
CommunityData(self._community, mpModel=0),
|
||||
UdpTransportTarget((self._host, self._port)),
|
||||
ContextData(),
|
||||
ObjectType(ObjectIdentity(self._baseoid)))
|
||||
)
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMP error: %s", errindication)
|
||||
elif errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex) - 1] or '?')
|
||||
else:
|
||||
for resrow in restable:
|
||||
self.value = resrow[-1]
|
163
homeassistant/components/sensor/swiss_hydrological_data.py
Normal file
163
homeassistant/components/sensor/swiss_hydrological_data.py
Normal file
@ -0,0 +1,163 @@
|
||||
"""
|
||||
Support for hydrological data from the Federal Office for the Environment FOEN.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.swiss_hydrological_data/
|
||||
"""
|
||||
import logging
|
||||
import collections
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (TEMP_CELSIUS, CONF_PLATFORM, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['beautifulsoup4==4.4.1', 'lxml==3.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RESOURCE = 'http://www.hydrodaten.admin.ch/en/'
|
||||
|
||||
DEFAULT_NAME = 'Water temperature'
|
||||
CONF_STATION = 'station'
|
||||
ICON = 'mdi:cup-water'
|
||||
|
||||
ATTR_DISCHARGE = 'Discharge'
|
||||
ATTR_WATERLEVEL = 'Level'
|
||||
ATTR_DISCHARGE_MEAN = 'Discharge mean'
|
||||
ATTR_WATERLEVEL_MEAN = 'Level mean'
|
||||
ATTR_TEMPERATURE_MEAN = 'Temperature mean'
|
||||
ATTR_DISCHARGE_MAX = 'Discharge max'
|
||||
ATTR_WATERLEVEL_MAX = 'Level max'
|
||||
ATTR_TEMPERATURE_MAX = 'Temperature max'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'swiss_hydrological_data',
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_STATION): cv.string,
|
||||
})
|
||||
|
||||
HydroData = collections.namedtuple(
|
||||
"HydrologicalData",
|
||||
['discharge', 'waterlevel', 'temperature', 'discharge_mean',
|
||||
'waterlevel_mean', 'temperature_mean', 'discharge_max', 'waterlevel_max',
|
||||
'temperature_max'])
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Swiss hydrological sensor."""
|
||||
station = config.get(CONF_STATION)
|
||||
name = config.get(CONF_NAME, DEFAULT_NAME)
|
||||
|
||||
try:
|
||||
response = requests.get('{}/{}.html'.format(_RESOURCE, station),
|
||||
timeout=5)
|
||||
if not response.ok:
|
||||
_LOGGER.error('The given station does not seem to exist: %s',
|
||||
station)
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('The URL is not accessible')
|
||||
return False
|
||||
|
||||
data = HydrologicalData(station)
|
||||
add_devices([SwissHydrologicalDataSensor(name, data)])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SwissHydrologicalDataSensor(Entity):
|
||||
"""Implementation of an Swiss hydrological sensor."""
|
||||
|
||||
def __init__(self, name, data):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self.data.measurings is not None:
|
||||
return {
|
||||
ATTR_DISCHARGE: self.data.measurings.discharge,
|
||||
ATTR_WATERLEVEL: self.data.measurings.waterlevel,
|
||||
ATTR_DISCHARGE_MEAN: self.data.measurings.discharge_mean,
|
||||
ATTR_WATERLEVEL_MEAN: self.data.measurings.waterlevel_mean,
|
||||
ATTR_TEMPERATURE_MEAN: self.data.measurings.temperature_mean,
|
||||
ATTR_DISCHARGE_MAX: self.data.measurings.discharge_max,
|
||||
ATTR_WATERLEVEL_MAX: self.data.measurings.waterlevel_max,
|
||||
ATTR_TEMPERATURE_MAX: self.data.measurings.temperature_max,
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self.data.update()
|
||||
if self.data.measurings is not None:
|
||||
self._state = self.data.measurings.temperature
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class HydrologicalData(object):
|
||||
"""The Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, station):
|
||||
"""Initialize the data object."""
|
||||
self.station = station
|
||||
self.measurings = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from hydrodaten.admin.ch."""
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
try:
|
||||
response = requests.get('{}/{}.html'.format(_RESOURCE,
|
||||
self.station),
|
||||
timeout=5)
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('Unable to retrieve data')
|
||||
response = None
|
||||
|
||||
try:
|
||||
tables = BeautifulSoup(response.content,
|
||||
'lxml').findChildren('table')
|
||||
rows = tables[0].findChildren(['th', 'tr'])
|
||||
|
||||
details = []
|
||||
|
||||
for row in rows:
|
||||
cells = row.findChildren('td')
|
||||
for cell in cells:
|
||||
details.append(cell.string)
|
||||
|
||||
self.measurings = HydroData._make(details)
|
||||
except AttributeError:
|
||||
self.measurings = None
|
@ -11,7 +11,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED,
|
||||
ATTR_BATTERY_LEVEL)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
SENSOR_TYPES = ['temperature', 'humidity']
|
||||
|
||||
|
@ -9,6 +9,8 @@ import subprocess
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'shell_command'
|
||||
@ -30,14 +32,38 @@ def setup(hass, config):
|
||||
|
||||
def service_handler(call):
|
||||
"""Execute a shell command service."""
|
||||
cmd = conf[call.service]
|
||||
cmd, shell = _parse_command(hass, cmd, call.data)
|
||||
if cmd is None:
|
||||
return
|
||||
try:
|
||||
subprocess.call(conf[call.service], shell=True,
|
||||
subprocess.call(cmd, shell=shell,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.exception('Error running command')
|
||||
_LOGGER.exception('Error running command: %s', cmd)
|
||||
|
||||
for name in conf.keys():
|
||||
hass.services.register(DOMAIN, name, service_handler,
|
||||
schema=SHELL_COMMAND_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def _parse_command(hass, cmd, variables):
|
||||
"""Parse command and fill in any template arguments if necessary."""
|
||||
cmds = cmd.split()
|
||||
prog = cmds[0]
|
||||
args = ' '.join(cmds[1:])
|
||||
try:
|
||||
rendered_args = template.render(hass, args, variables=variables)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.exception('Error rendering command template: %s', ex)
|
||||
return None, None
|
||||
if rendered_args == args:
|
||||
# no template used. default behavior
|
||||
shell = True
|
||||
else:
|
||||
# template used. Must break into list and use shell=False for security
|
||||
cmd = [prog] + rendered_args.split()
|
||||
shell = False
|
||||
return cmd, shell
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import location as location_util
|
||||
from homeassistant.const import CONF_ELEVATION
|
||||
|
||||
REQUIREMENTS = ['astral==1.1']
|
||||
REQUIREMENTS = ['astral==1.2']
|
||||
DOMAIN = "sun"
|
||||
ENTITY_ID = "sun.sun"
|
||||
|
||||
|
@ -18,9 +18,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (
|
||||
group, wemo, wink, isy994, verisure,
|
||||
zwave, tellduslive, tellstick, mysensors, vera)
|
||||
from homeassistant.components import group
|
||||
|
||||
DOMAIN = 'switch'
|
||||
SCAN_INTERVAL = 30
|
||||
@ -35,19 +33,6 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wemo.DISCOVER_SWITCHES: 'wemo',
|
||||
wink.DISCOVER_SWITCHES: 'wink',
|
||||
isy994.DISCOVER_SWITCHES: 'isy994',
|
||||
verisure.DISCOVER_SWITCHES: 'verisure',
|
||||
zwave.DISCOVER_SWITCHES: 'zwave',
|
||||
tellduslive.DISCOVER_SWITCHES: 'tellduslive',
|
||||
mysensors.DISCOVER_SWITCHES: 'mysensors',
|
||||
tellstick.DISCOVER_SWITCHES: 'tellstick',
|
||||
vera.DISCOVER_SWITCHES: 'vera',
|
||||
}
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
'current_power_mwh': ATTR_CURRENT_POWER_MWH,
|
||||
'today_power_mw': ATTR_TODAY_MWH,
|
||||
@ -87,8 +72,7 @@ def toggle(hass, entity_id=None):
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for switches."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_SWITCHES)
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES)
|
||||
component.setup(config)
|
||||
|
||||
def handle_switch_service(service):
|
||||
|
@ -5,14 +5,27 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.mysensors/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
ATTR_IR_CODE = 'V_IR_SEND'
|
||||
SERVICE_SEND_IR_CODE = 'mysensors_send_ir_code'
|
||||
|
||||
SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_IR_CODE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for switches."""
|
||||
@ -32,6 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pres.S_SMOKE: [set_req.V_ARMED],
|
||||
pres.S_LIGHT: [set_req.V_LIGHT],
|
||||
pres.S_LOCK: [set_req.V_LOCK_STATUS],
|
||||
pres.S_IR: [set_req.V_IR_SEND],
|
||||
}
|
||||
device_class_map = {
|
||||
pres.S_DOOR: MySensorsSwitch,
|
||||
pres.S_MOTION: MySensorsSwitch,
|
||||
pres.S_SMOKE: MySensorsSwitch,
|
||||
pres.S_LIGHT: MySensorsSwitch,
|
||||
pres.S_LOCK: MySensorsSwitch,
|
||||
pres.S_IR: MySensorsIRSwitch,
|
||||
}
|
||||
if float(gateway.version) >= 1.5:
|
||||
map_sv_types.update({
|
||||
@ -43,15 +65,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pres.S_MOISTURE: [set_req.V_ARMED],
|
||||
})
|
||||
map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS)
|
||||
device_class_map.update({
|
||||
pres.S_BINARY: MySensorsSwitch,
|
||||
pres.S_SPRINKLER: MySensorsSwitch,
|
||||
pres.S_WATER_LEAK: MySensorsSwitch,
|
||||
pres.S_SOUND: MySensorsSwitch,
|
||||
pres.S_VIBRATION: MySensorsSwitch,
|
||||
pres.S_MOISTURE: MySensorsSwitch,
|
||||
})
|
||||
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsSwitch))
|
||||
map_sv_types, devices, add_devices, device_class_map))
|
||||
|
||||
def send_ir_code_service(service):
|
||||
"""Set IR code as device state attribute."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
ir_code = service.data.get(ATTR_IR_CODE)
|
||||
|
||||
if entity_ids:
|
||||
_devices = [device for device in devices.values()
|
||||
if isinstance(device, MySensorsIRSwitch) and
|
||||
device.entity_id in entity_ids]
|
||||
else:
|
||||
_devices = [device for device in devices.values()
|
||||
if isinstance(device, MySensorsIRSwitch)]
|
||||
|
||||
kwargs = {ATTR_IR_CODE: ir_code}
|
||||
for device in _devices:
|
||||
device.turn_on(**kwargs)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SEND_IR_CODE,
|
||||
send_ir_code_service,
|
||||
descriptions.get(SERVICE_SEND_IR_CODE),
|
||||
schema=SEND_IR_CODE_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice):
|
||||
"""Representation of the value of a MySensors Switch child node."""
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of entity."""
|
||||
return self.gateway.optimistic
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if switch is on."""
|
||||
@ -77,7 +137,60 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice):
|
||||
self._values[self.value_type] = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class MySensorsIRSwitch(MySensorsSwitch):
|
||||
"""IR switch child class to MySensorsSwitch."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Setup instance attributes."""
|
||||
MySensorsSwitch.__init__(self, *args)
|
||||
self._ir_code = None
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of entity."""
|
||||
return self.gateway.optimistic
|
||||
def is_on(self):
|
||||
"""Return True if switch is on."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_LIGHT in self._values:
|
||||
return self._values[set_req.V_LIGHT] == STATE_ON
|
||||
return False
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the IR switch on."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_LIGHT not in self._values:
|
||||
_LOGGER.error('missing value_type: %s at node: %s, child: %s',
|
||||
set_req.V_LIGHT.name, self.node_id, self.child_id)
|
||||
return
|
||||
if ATTR_IR_CODE in kwargs:
|
||||
self._ir_code = kwargs[ATTR_IR_CODE]
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type, self._ir_code)
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_LIGHT, 1)
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[self.value_type] = self._ir_code
|
||||
self._values[set_req.V_LIGHT] = STATE_ON
|
||||
self.update_ha_state()
|
||||
# turn off switch after switch was turned on
|
||||
self.turn_off()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the IR switch off."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_LIGHT not in self._values:
|
||||
_LOGGER.error('missing value_type: %s at node: %s, child: %s',
|
||||
set_req.V_LIGHT.name, self.node_id, self.child_id)
|
||||
return
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_LIGHT, 0)
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
MySensorsSwitch.update(self)
|
||||
if self.value_type in self._values:
|
||||
self._ir_code = self._values[self.value_type]
|
||||
|
226
homeassistant/components/switch/netio.py
Normal file
226
homeassistant/components/switch/netio.py
Normal file
@ -0,0 +1,226 @@
|
||||
"""
|
||||
Netio switch component.
|
||||
|
||||
The Netio platform allows you to control your [Netio]
|
||||
(http://www.netio-products.com/en/overview/) Netio4, Netio4 All and Netio 230B.
|
||||
These are smart outlets controllable through ethernet and/or WiFi that reports
|
||||
consumptions (Netio4all).
|
||||
|
||||
To use these devices in your installation, add the following to your
|
||||
configuration.yaml file:
|
||||
```
|
||||
switch:
|
||||
- platform: netio
|
||||
host: netio-living
|
||||
outlets:
|
||||
1: "AppleTV"
|
||||
2: "Htpc"
|
||||
3: "Lampe Gauche"
|
||||
4: "Lampe Droite"
|
||||
- platform: netio
|
||||
host: 192.168.1.43
|
||||
port: 1234
|
||||
username: user
|
||||
password: pwd
|
||||
outlets:
|
||||
1: "Nothing..."
|
||||
4: "Lampe du fer"
|
||||
```
|
||||
|
||||
To get pushed updates from the netio devices, one can add this lua code in the
|
||||
device interface as an action triggered on "Netio" "System variables updated"
|
||||
with an 'Always' schedule:
|
||||
|
||||
``
|
||||
-- this will send socket and consumption status updates via CGI
|
||||
-- to given address. Associate with 'System variables update' event
|
||||
-- to get consumption updates when they show up
|
||||
|
||||
local address='ha:8123'
|
||||
local path = '/api/netio/<host>'
|
||||
|
||||
|
||||
local output = {}
|
||||
for i = 1, 4 do for _, what in pairs({'state', 'consumption',
|
||||
'cumulatedConsumption', 'consumptionStart'}) do
|
||||
local varname = string.format('output%d_%s', i, what)
|
||||
table.insert(output,
|
||||
varname..'='..tostring(devices.system[varname]):gsub(" ","|"))
|
||||
end end
|
||||
|
||||
local qs = table.concat(output, '&')
|
||||
local url = string.format('http://%s%s?%s', address, path, qs)
|
||||
devices.system.CustomCGI{url=url}
|
||||
```
|
||||
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.netio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
from homeassistant import util
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME, \
|
||||
CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, STATE_ON
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
REQUIREMENTS = ['pynetio==0.1.6']
|
||||
DEFAULT_USERNAME = 'admin'
|
||||
DEFAULT_PORT = 1234
|
||||
URL_API_NETIO_EP = "/api/netio/<host>"
|
||||
|
||||
CONF_OUTLETS = "outlets"
|
||||
REQ_CONF = [CONF_HOST, CONF_OUTLETS]
|
||||
ATTR_TODAY_MWH = "today_mwh"
|
||||
ATTR_TOTAL_CONSUMPTION_KWH = "total_energy_kwh"
|
||||
ATTR_CURRENT_POWER_MWH = "current_power_mwh"
|
||||
ATTR_CURRENT_POWER_W = "current_power_w"
|
||||
|
||||
Device = namedtuple('device', ['netio', 'entities'])
|
||||
DEVICES = {}
|
||||
ATTR_START_DATE = 'start_date'
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Configure the netio platform."""
|
||||
from pynetio import Netio
|
||||
|
||||
if validate_config({"conf": config}, {"conf": [CONF_OUTLETS,
|
||||
CONF_HOST]}, _LOGGER):
|
||||
if len(DEVICES) == 0:
|
||||
hass.wsgi.register_view(NetioApiView)
|
||||
|
||||
dev = Netio(config[CONF_HOST],
|
||||
config.get(CONF_PORT, DEFAULT_PORT),
|
||||
config.get(CONF_USERNAME, DEFAULT_USERNAME),
|
||||
config.get(CONF_PASSWORD, DEFAULT_USERNAME))
|
||||
|
||||
DEVICES[config[CONF_HOST]] = Device(dev, [])
|
||||
|
||||
# Throttle the update for all NetioSwitches of one Netio
|
||||
dev.update = util.Throttle(MIN_TIME_BETWEEN_SCANS)(dev.update)
|
||||
|
||||
for key in config[CONF_OUTLETS]:
|
||||
switch = NetioSwitch(DEVICES[config[CONF_HOST]].netio, key,
|
||||
config[CONF_OUTLETS][key])
|
||||
DEVICES[config[CONF_HOST]].entities.append(switch)
|
||||
|
||||
add_devices_callback(DEVICES[config[CONF_HOST]].entities)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, dispose)
|
||||
return True
|
||||
|
||||
|
||||
def dispose(event):
|
||||
"""Close connections to Netio Devices."""
|
||||
for _, value in DEVICES.items():
|
||||
value.netio.stop()
|
||||
|
||||
|
||||
class NetioApiView(HomeAssistantView):
|
||||
"""WSGI handler class."""
|
||||
|
||||
url = URL_API_NETIO_EP
|
||||
name = "api:netio"
|
||||
|
||||
def get(self, request, host):
|
||||
"""Request handler."""
|
||||
data = request.args
|
||||
states, consumptions, cumulated_consumptions, start_dates = \
|
||||
[], [], [], []
|
||||
|
||||
for i in range(1, 5):
|
||||
out = 'output%d' % i
|
||||
states.append(data.get('%s_state' % out) == STATE_ON)
|
||||
consumptions.append(float(data.get('%s_consumption' % out, 0)))
|
||||
cumulated_consumptions.append(
|
||||
float(data.get('%s_cumulatedConsumption' % out, 0)) / 1000)
|
||||
start_dates.append(data.get('%s_consumptionStart' % out, ""))
|
||||
|
||||
_LOGGER.debug('%s: %s, %s, %s since %s', host, states,
|
||||
consumptions, cumulated_consumptions, start_dates)
|
||||
|
||||
ndev = DEVICES[host].netio
|
||||
ndev.consumptions = consumptions
|
||||
ndev.cumulated_consumptions = cumulated_consumptions
|
||||
ndev.states = states
|
||||
ndev.start_dates = start_dates
|
||||
|
||||
for dev in DEVICES[host].entities:
|
||||
dev.update_ha_state()
|
||||
|
||||
return self.json(True)
|
||||
|
||||
|
||||
class NetioSwitch(SwitchDevice):
|
||||
"""Provide a netio linked switch."""
|
||||
|
||||
def __init__(self, netio, outlet, name):
|
||||
"""Defined to handle throttle."""
|
||||
self._name = name
|
||||
self.outlet = outlet
|
||||
self.netio = netio
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Netio device's name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return not hasattr(self, 'telnet')
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn switch on."""
|
||||
self._set(True)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn switch off."""
|
||||
self._set(False)
|
||||
|
||||
def _set(self, value):
|
||||
val = list('uuuu')
|
||||
val[self.outlet - 1] = "1" if value else "0"
|
||||
self.netio.get('port list %s' % ''.join(val))
|
||||
self.netio.states[self.outlet - 1] = value
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return switch's status."""
|
||||
return self.netio.states[self.outlet - 1]
|
||||
|
||||
def update(self):
|
||||
"""Called by HA."""
|
||||
self.netio.update()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return optional state attributes."""
|
||||
return {ATTR_CURRENT_POWER_W: self.current_power_w,
|
||||
ATTR_TOTAL_CONSUMPTION_KWH: self.cumulated_consumption_kwh,
|
||||
ATTR_START_DATE: self.start_date.split('|')[0]}
|
||||
|
||||
@property
|
||||
def current_power_w(self):
|
||||
"""Return actual power."""
|
||||
return self.netio.consumptions[self.outlet - 1]
|
||||
|
||||
@property
|
||||
def cumulated_consumption_kwh(self):
|
||||
"""Total enerygy consumption since start_date."""
|
||||
return self.netio.cumulated_consumptions[self.outlet - 1]
|
||||
|
||||
@property
|
||||
def start_date(self):
|
||||
"""Point in time when the energy accumulation started."""
|
||||
return self.netio.start_dates[self.outlet - 1]
|
@ -0,0 +1,37 @@
|
||||
# Describes the format for available switch services
|
||||
|
||||
turn_on:
|
||||
description: Turn a switch on
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn on
|
||||
example: 'switch.living_room'
|
||||
|
||||
turn_off:
|
||||
description: Turn a switch off
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn off
|
||||
example: 'switch.living_room'
|
||||
|
||||
toggle:
|
||||
description: Toggles a switch state
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to toggle
|
||||
example: 'switch.living_room'
|
||||
|
||||
mysensors_send_ir_code:
|
||||
description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that should have the IR code set and be turned on. Platform dependent.
|
||||
example: 'switch.living_room_1_1'
|
||||
|
||||
V_IR_SEND:
|
||||
description: IR code to send
|
||||
example: '0xC284'
|
@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -7,11 +7,7 @@ https://home-assistant.io/components/tellduslive/
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "tellduslive"
|
||||
@ -20,12 +16,6 @@ REQUIREMENTS = ['tellive-py==0.5.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVER_SENSORS = "tellduslive.sensors"
|
||||
DISCOVER_SWITCHES = "tellduslive.switches"
|
||||
DISCOVERY_TYPES = {"sensor": DISCOVER_SENSORS,
|
||||
"switch": DISCOVER_SWITCHES}
|
||||
|
||||
|
||||
CONF_PUBLIC_KEY = "public_key"
|
||||
CONF_PRIVATE_KEY = "private_key"
|
||||
CONF_TOKEN = "token"
|
||||
@ -101,16 +91,8 @@ class TelldusLiveData(object):
|
||||
_LOGGER.info("discovered %d new %s devices",
|
||||
len(found_devices), component_name)
|
||||
|
||||
component = get_component(component_name)
|
||||
bootstrap.setup_component(self._hass,
|
||||
component.DOMAIN,
|
||||
self._config)
|
||||
|
||||
discovery_type = DISCOVERY_TYPES[component_name]
|
||||
|
||||
self._hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery_type,
|
||||
ATTR_DISCOVERED: found_devices})
|
||||
discovery.load_platform(self._hass, component_name, DOMAIN,
|
||||
found_devices, self._config)
|
||||
|
||||
def request(self, what, **params):
|
||||
"""Send a request to the Tellstick Live API."""
|
||||
|
@ -8,11 +8,8 @@ import logging
|
||||
import threading
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE,
|
||||
EVENT_PLATFORM_DISCOVERED, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = "tellstick"
|
||||
@ -24,11 +21,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_SIGNAL_REPETITIONS = "signal_repetitions"
|
||||
DEFAULT_SIGNAL_REPETITIONS = 1
|
||||
|
||||
DISCOVER_SWITCHES = "tellstick.switches"
|
||||
DISCOVER_LIGHTS = "tellstick.lights"
|
||||
DISCOVERY_TYPES = {"switch": DISCOVER_SWITCHES,
|
||||
"light": DISCOVER_LIGHTS}
|
||||
|
||||
ATTR_DISCOVER_DEVICES = "devices"
|
||||
ATTR_DISCOVER_CONFIG = "config"
|
||||
|
||||
@ -57,17 +49,11 @@ def _discover(hass, config, found_devices, component_name):
|
||||
_LOGGER.info("discovered %d new %s devices",
|
||||
len(found_devices), component_name)
|
||||
|
||||
component = get_component(component_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN,
|
||||
config)
|
||||
|
||||
signal_repetitions = config[DOMAIN].get(ATTR_SIGNAL_REPETITIONS)
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: DISCOVERY_TYPES[component_name],
|
||||
ATTR_DISCOVERED: {ATTR_DISCOVER_DEVICES: found_devices,
|
||||
ATTR_DISCOVER_CONFIG:
|
||||
signal_repetitions}})
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices,
|
||||
ATTR_DISCOVER_CONFIG: signal_repetitions}, config)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -15,7 +15,6 @@ from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components import (ecobee, zwave)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
@ -29,6 +28,7 @@ SCAN_INTERVAL = 60
|
||||
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
SERVICE_SET_FAN_MODE = "set_fan_mode"
|
||||
SERVICE_SET_HVAC_MODE = "set_hvac_mode"
|
||||
|
||||
STATE_HEAT = "heat"
|
||||
STATE_COOL = "cool"
|
||||
@ -37,6 +37,7 @@ STATE_IDLE = "idle"
|
||||
ATTR_CURRENT_TEMPERATURE = "current_temperature"
|
||||
ATTR_AWAY_MODE = "away_mode"
|
||||
ATTR_FAN = "fan"
|
||||
ATTR_HVAC_MODE = "hvac_mode"
|
||||
ATTR_MAX_TEMP = "max_temp"
|
||||
ATTR_MIN_TEMP = "min_temp"
|
||||
ATTR_TEMPERATURE_LOW = "target_temp_low"
|
||||
@ -45,11 +46,6 @@ ATTR_OPERATION = "current_operation"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
ecobee.DISCOVER_THERMOSTAT: 'ecobee',
|
||||
zwave.DISCOVER_THERMOSTATS: 'zwave'
|
||||
}
|
||||
|
||||
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_AWAY_MODE): cv.boolean,
|
||||
@ -62,6 +58,10 @@ SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_FAN): cv.boolean,
|
||||
})
|
||||
SET_HVAC_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_HVAC_MODE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def set_away_mode(hass, away_mode, entity_id=None):
|
||||
@ -98,11 +98,22 @@ def set_fan_mode(hass, fan_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
|
||||
|
||||
|
||||
def set_hvac_mode(hass, hvac_mode, entity_id=None):
|
||||
"""Set specified thermostat hvac mode."""
|
||||
data = {
|
||||
ATTR_HVAC_MODE: hvac_mode
|
||||
}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Setup thermostats."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
@ -164,6 +175,22 @@ def setup(hass, config):
|
||||
descriptions.get(SERVICE_SET_FAN_MODE),
|
||||
schema=SET_FAN_MODE_SCHEMA)
|
||||
|
||||
def hvac_mode_set_service(service):
|
||||
"""Set hvac mode on target thermostats."""
|
||||
target_thermostats = component.extract_from_service(service)
|
||||
|
||||
hvac_mode = service.data[ATTR_HVAC_MODE]
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.set_hvac_mode(hvac_mode)
|
||||
|
||||
thermostat.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_HVAC_MODE, hvac_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_HVAC_MODE),
|
||||
schema=SET_HVAC_MODE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -250,6 +277,10 @@ class ThermostatDevice(Entity):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set hvac mode."""
|
||||
pass
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
|
@ -5,17 +5,29 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/thermostat.ecobee/
|
||||
"""
|
||||
import logging
|
||||
from os import path
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.thermostat import (
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, TEMP_FAHRENHEIT
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
_CONFIGURING = {}
|
||||
|
||||
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
|
||||
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
|
||||
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Ecobee Thermostat Platform."""
|
||||
@ -26,10 +38,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.info(
|
||||
"Loading ecobee thermostat component with hold_temp set to %s",
|
||||
hold_temp)
|
||||
add_devices(Thermostat(data, index, hold_temp)
|
||||
for index in range(len(data.ecobee.thermostats)))
|
||||
devices = [Thermostat(data, index, hold_temp)
|
||||
for index in range(len(data.ecobee.thermostats))]
|
||||
add_devices(devices)
|
||||
|
||||
def fan_min_on_time_set_service(service):
|
||||
"""Set the minimum fan on time on the target thermostats."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
target_thermostats = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
target_thermostats = devices
|
||||
|
||||
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.set_fan_min_on_time(str(fan_min_on_time))
|
||||
|
||||
thermostat.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
|
||||
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class Thermostat(ThermostatDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
|
||||
@ -127,6 +166,11 @@ class Thermostat(ThermostatDevice):
|
||||
"""Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off."""
|
||||
return self.thermostat['settings']['hvacMode']
|
||||
|
||||
@property
|
||||
def fan_min_on_time(self):
|
||||
"""Return current fan minimum on time."""
|
||||
return self.thermostat['settings']['fanMinOnTime']
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
@ -135,7 +179,8 @@ class Thermostat(ThermostatDevice):
|
||||
"humidity": self.humidity,
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"hvac_mode": self.hvac_mode
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
}
|
||||
|
||||
@property
|
||||
@ -177,6 +222,11 @@ class Thermostat(ThermostatDevice):
|
||||
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, mode)
|
||||
|
||||
def set_fan_min_on_time(self, fan_min_on_time):
|
||||
"""Set the minimum fan on time."""
|
||||
self.data.ecobee.set_fan_min_on_time(self.thermostat_index,
|
||||
fan_min_on_time)
|
||||
|
||||
# Home and Sleep mode aren't used in UI yet:
|
||||
|
||||
# def turn_home_mode_on(self):
|
||||
|
@ -34,3 +34,15 @@ set_fan_mode:
|
||||
fan:
|
||||
description: New value of fan mode
|
||||
example: true
|
||||
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum time, in minutes, to run the fan each hour
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
descriptions: Name(s) of entities to change
|
||||
example: 'thermostat.ecobee'
|
||||
|
||||
fan_min_on_time:
|
||||
description: New value of fan minimum on time
|
||||
example: 5
|
||||
|
@ -25,6 +25,12 @@ DEVICE_MAPPINGS = {
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_IGNORE
|
||||
}
|
||||
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_STATE = 69 # 0x45
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 49 # 0x31
|
||||
COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 66 # 0x42
|
||||
COMMAND_CLASS_THERMOSTAT_MODE = 64 # 0x40
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZWave thermostats."""
|
||||
@ -51,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
"""Represents a HeatControl thermostat."""
|
||||
|
||||
@ -61,11 +67,12 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
from pydispatch import dispatcher
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
self._target_temperature = None
|
||||
self._index = value.index
|
||||
self._current_temperature = None
|
||||
self._current_operation = STATE_IDLE
|
||||
self._current_operation_state = STATE_IDLE
|
||||
self._unit = None
|
||||
self._current_operation_state = STATE_IDLE
|
||||
self._target_temperature = None
|
||||
self._current_fan_state = STATE_IDLE
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
@ -79,31 +86,37 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# set point
|
||||
for _, value in self._node.get_values(class_id=0x43).items():
|
||||
if int(value.data) != 0:
|
||||
self._target_temperature = int(value.data)
|
||||
# Operation
|
||||
for _, value in self._node.get_values(class_id=0x40).items():
|
||||
self._current_operation = value.data_as_string
|
||||
# Current Temp
|
||||
for _, value in self._node.get_values_for_command_class(0x31).items():
|
||||
# current Temp
|
||||
for _, value in self._node.get_values_for_command_class(
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL).items():
|
||||
self._current_temperature = int(value.data)
|
||||
self._unit = value.units
|
||||
# COMMAND_CLASS_THERMOSTAT_OPERATING_STATE
|
||||
for _, value in self._node.get_values(class_id=0x42).items():
|
||||
|
||||
# operation state
|
||||
for _, value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).items():
|
||||
self._current_operation_state = value.data_as_string
|
||||
|
||||
# target temperature
|
||||
temps = []
|
||||
for _, value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).items():
|
||||
temps.append(int(value.data))
|
||||
if value.index == self._index:
|
||||
self._target_temperature = value.data
|
||||
self._target_temperature_high = max(temps)
|
||||
self._target_temperature_low = min(temps)
|
||||
|
||||
# fan state
|
||||
for _, value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_STATE).items():
|
||||
self._current_fan_state = value.data_as_string
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on ZWave."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
"""Return if the fan is not idle."""
|
||||
return self._current_operation_state != 'Idle'
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
@ -114,7 +127,6 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return unit
|
||||
return self.hass.config.temperature_unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@ -123,17 +135,24 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
"""Return the operation mode."""
|
||||
return self._current_operation
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation_state
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
"""Return true if the fan is on."""
|
||||
return not (self._current_fan_state == 'Idle' or
|
||||
self._current_fan_state == STATE_IDLE)
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
# set point
|
||||
for _, value in self._node.get_values_for_command_class(0x43).items():
|
||||
if int(value.data) != 0:
|
||||
for _, value in self._node.get_values_for_command_class(
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT).items():
|
||||
if int(value.data) != 0 and value.index == self._index:
|
||||
value.data = temperature
|
||||
|
@ -5,16 +5,13 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_SERVICE, ATTR_DISCOVERED,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.10']
|
||||
|
||||
@ -27,28 +24,18 @@ VERA_CONTROLLER = None
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
CONF_LIGHTS = 'lights'
|
||||
|
||||
BINARY_SENSOR = 'binary_sensor'
|
||||
SENSOR = 'sensor'
|
||||
LIGHT = 'light'
|
||||
SWITCH = 'switch'
|
||||
|
||||
DEVICE_CATEGORIES = {
|
||||
'Sensor': BINARY_SENSOR,
|
||||
'Temperature Sensor': SENSOR,
|
||||
'Light Sensor': SENSOR,
|
||||
'Humidity Sensor': SENSOR,
|
||||
'Dimmable Switch': LIGHT,
|
||||
'Switch': SWITCH,
|
||||
'Armable Sensor': SWITCH,
|
||||
'On/Off Switch': SWITCH,
|
||||
'Sensor': 'binary_sensor',
|
||||
'Temperature Sensor': 'sensor',
|
||||
'Light Sensor': 'sensor',
|
||||
'Humidity Sensor': 'sensor',
|
||||
'Dimmable Switch': 'light',
|
||||
'Switch': 'switch',
|
||||
'Armable Sensor': 'switch',
|
||||
'On/Off Switch': 'switch',
|
||||
# 'Window Covering': NOT SUPPORTED YET
|
||||
}
|
||||
|
||||
DISCOVER_BINARY_SENSORS = 'vera.binary_sensors'
|
||||
DISCOVER_SENSORS = 'vera.sensors'
|
||||
DISCOVER_LIGHTS = 'vera.lights'
|
||||
DISCOVER_SWITCHES = 'vera.switchs'
|
||||
|
||||
VERA_DEVICES = defaultdict(list)
|
||||
|
||||
|
||||
@ -100,19 +87,13 @@ def setup(hass, base_config):
|
||||
dev_type = DEVICE_CATEGORIES.get(device.category)
|
||||
if dev_type is None:
|
||||
continue
|
||||
if dev_type == SWITCH and device.device_id in lights_ids:
|
||||
dev_type = LIGHT
|
||||
if dev_type == 'switch' and device.device_id in lights_ids:
|
||||
dev_type = 'light'
|
||||
VERA_DEVICES[dev_type].append(device)
|
||||
|
||||
for comp_name, discovery in (((BINARY_SENSOR, DISCOVER_BINARY_SENSORS),
|
||||
(SENSOR, DISCOVER_SENSORS),
|
||||
(LIGHT, DISCOVER_LIGHTS),
|
||||
(SWITCH, DISCOVER_SWITCHES))):
|
||||
component = get_component(comp_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN, base_config)
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery,
|
||||
ATTR_DISCOVERED: {}})
|
||||
for component in 'binary_sensor', 'sensor', 'light', 'switch':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, base_config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -9,19 +9,11 @@ import threading
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "verisure"
|
||||
DISCOVER_SENSORS = 'verisure.sensors'
|
||||
DISCOVER_SWITCHES = 'verisure.switches'
|
||||
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
||||
DISCOVER_LOCKS = 'verisure.lock'
|
||||
|
||||
REQUIREMENTS = ['vsure==0.8.1']
|
||||
|
||||
@ -43,15 +35,8 @@ def setup(hass, config):
|
||||
if not HUB.login():
|
||||
return False
|
||||
|
||||
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('alarm_control_panel', DISCOVER_ALARMS),
|
||||
('lock', DISCOVER_LOCKS))):
|
||||
component = get_component(comp_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery,
|
||||
ATTR_DISCOVERED: {}})
|
||||
for component in ('sensor', 'switch', 'alarm_control_panel', 'lock'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
@ -142,7 +127,7 @@ class VerisureHub(object):
|
||||
except AttributeError:
|
||||
status[overview.deviceLabel] = overview
|
||||
except self._verisure.Error as ex:
|
||||
_LOGGER.error('Caught connection error %s, tries to reconnect', ex)
|
||||
_LOGGER.info('Caught connection error %s, tries to reconnect', ex)
|
||||
self.reconnect()
|
||||
|
||||
def reconnect(self):
|
||||
|
@ -6,29 +6,22 @@ https://home-assistant.io/components/wemo/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.components.discovery import SERVICE_WEMO
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.4.3']
|
||||
|
||||
DOMAIN = 'wemo'
|
||||
DISCOVER_LIGHTS = 'wemo.light'
|
||||
DISCOVER_BINARY_SENSORS = 'wemo.binary_sensor'
|
||||
DISCOVER_SWITCHES = 'wemo.switch'
|
||||
|
||||
# Mapping from Wemo model_name to service.
|
||||
# Mapping from Wemo model_name to component.
|
||||
WEMO_MODEL_DISPATCH = {
|
||||
'Bridge': DISCOVER_LIGHTS,
|
||||
'Insight': DISCOVER_SWITCHES,
|
||||
'Maker': DISCOVER_SWITCHES,
|
||||
'Sensor': DISCOVER_BINARY_SENSORS,
|
||||
'Socket': DISCOVER_SWITCHES,
|
||||
'LightSwitch': DISCOVER_SWITCHES
|
||||
}
|
||||
WEMO_SERVICE_DISPATCH = {
|
||||
DISCOVER_LIGHTS: 'light',
|
||||
DISCOVER_BINARY_SENSORS: 'binary_sensor',
|
||||
DISCOVER_SWITCHES: 'switch',
|
||||
'Bridge': 'light',
|
||||
'Insight': 'switch',
|
||||
'Maker': 'switch',
|
||||
'Sensor': 'binary_sensor',
|
||||
'Socket': 'switch',
|
||||
'LightSwitch': 'switch'
|
||||
}
|
||||
|
||||
SUBSCRIPTION_REGISTRY = None
|
||||
@ -64,13 +57,12 @@ def setup(hass, config):
|
||||
_LOGGER.debug('Discovered unique device %s', serial)
|
||||
KNOWN_DEVICES.append(serial)
|
||||
|
||||
service = WEMO_MODEL_DISPATCH.get(model_name) or DISCOVER_SWITCHES
|
||||
component = WEMO_SERVICE_DISPATCH.get(service)
|
||||
component = WEMO_MODEL_DISPATCH.get(model_name, 'switch')
|
||||
|
||||
discovery.discover(hass, service, discovery_info,
|
||||
component, config)
|
||||
discovery.load_platform(hass, component, DOMAIN, discovery_info,
|
||||
config)
|
||||
|
||||
discovery.listen(hass, discovery.SERVICE_WEMO, discovery_dispatch)
|
||||
discovery.listen(hass, SERVICE_WEMO, discovery_dispatch)
|
||||
|
||||
_LOGGER.info("Scanning for WeMo devices.")
|
||||
devices = [(device.host, device) for device in pywemo.discover_devices()]
|
||||
@ -92,5 +84,5 @@ def setup(hass, config):
|
||||
|
||||
discovery_info = (device.name, device.model_name, url, device.mac,
|
||||
device.serialnumber)
|
||||
discovery.discover(hass, discovery.SERVICE_WEMO, discovery_info)
|
||||
discovery.discover(hass, SERVICE_WEMO, discovery_info)
|
||||
return True
|
||||
|
@ -6,23 +6,12 @@ https://home-assistant.io/components/wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_ACCESS_TOKEN,
|
||||
EVENT_PLATFORM_DISCOVERED, ATTR_BATTERY_LEVEL)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "wink"
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
|
||||
DISCOVER_LIGHTS = "wink.lights"
|
||||
DISCOVER_SWITCHES = "wink.switches"
|
||||
DISCOVER_SENSORS = "wink.sensors"
|
||||
DISCOVER_BINARY_SENSORS = "wink.binary_sensors"
|
||||
DISCOVER_LOCKS = "wink.locks"
|
||||
DISCOVER_GARAGE_DOORS = "wink.garage_doors"
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@ -36,28 +25,18 @@ def setup(hass, config):
|
||||
pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN])
|
||||
|
||||
# Load components for the devices in the Wink that we support
|
||||
for component_name, func_exists, discovery_type in (
|
||||
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
|
||||
('switch', lambda: pywink.get_switches or
|
||||
pywink.get_sirens or
|
||||
pywink.get_powerstrip_outlets, DISCOVER_SWITCHES),
|
||||
('binary_sensor', pywink.get_sensors, DISCOVER_BINARY_SENSORS),
|
||||
('sensor', lambda: pywink.get_sensors or
|
||||
pywink.get_eggtrays, DISCOVER_SENSORS),
|
||||
('lock', pywink.get_locks, DISCOVER_LOCKS),
|
||||
('garage_door', pywink.get_garage_doors, DISCOVER_GARAGE_DOORS)):
|
||||
for component_name, func_exists in (
|
||||
('light', pywink.get_bulbs),
|
||||
('switch', lambda: pywink.get_switches or pywink.get_sirens or
|
||||
pywink.get_powerstrip_outlets),
|
||||
('binary_sensor', pywink.get_sensors),
|
||||
('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays),
|
||||
('lock', pywink.get_locks),
|
||||
('rollershutter', pywink.get_shades),
|
||||
('garage_door', pywink.get_garage_doors)):
|
||||
|
||||
if func_exists():
|
||||
component = get_component(component_name)
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
|
||||
# Fire discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: discovery_type,
|
||||
ATTR_DISCOVERED: {}
|
||||
})
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -9,11 +9,11 @@ import os.path
|
||||
import time
|
||||
from pprint import pprint
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_DISCOVERED, ATTR_ENTITY_ID, ATTR_LOCATION,
|
||||
ATTR_SERVICE, CONF_CUSTOMIZE, EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
|
||||
ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_LOCATION,
|
||||
CONF_CUSTOMIZE, EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
from homeassistant.util import convert, slugify
|
||||
|
||||
@ -37,14 +37,6 @@ SERVICE_HEAL_NETWORK = "heal_network"
|
||||
SERVICE_SOFT_RESET = "soft_reset"
|
||||
SERVICE_TEST_NETWORK = "test_network"
|
||||
|
||||
DISCOVER_SENSORS = "zwave.sensors"
|
||||
DISCOVER_SWITCHES = "zwave.switch"
|
||||
DISCOVER_LIGHTS = "zwave.light"
|
||||
DISCOVER_BINARY_SENSORS = 'zwave.binary_sensor'
|
||||
DISCOVER_THERMOSTATS = 'zwave.thermostat'
|
||||
DISCOVER_HVAC = 'zwave.hvac'
|
||||
DISCOVER_LOCKS = 'zwave.lock'
|
||||
|
||||
EVENT_SCENE_ACTIVATED = "zwave.scene_activated"
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 38
|
||||
@ -71,39 +63,32 @@ TYPE_DECIMAL = "Decimal"
|
||||
# value type).
|
||||
DISCOVERY_COMPONENTS = [
|
||||
('sensor',
|
||||
DISCOVER_SENSORS,
|
||||
[COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
COMMAND_CLASS_METER,
|
||||
COMMAND_CLASS_ALARM],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER),
|
||||
('light',
|
||||
DISCOVER_LIGHTS,
|
||||
[COMMAND_CLASS_SWITCH_MULTILEVEL],
|
||||
TYPE_BYTE,
|
||||
GENRE_USER),
|
||||
('switch',
|
||||
DISCOVER_SWITCHES,
|
||||
[COMMAND_CLASS_SWITCH_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER),
|
||||
('binary_sensor',
|
||||
DISCOVER_BINARY_SENSORS,
|
||||
[COMMAND_CLASS_SENSOR_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER),
|
||||
('thermostat',
|
||||
DISCOVER_THERMOSTATS,
|
||||
[COMMAND_CLASS_THERMOSTAT_SETPOINT],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_WHATEVER),
|
||||
('hvac',
|
||||
DISCOVER_HVAC,
|
||||
[COMMAND_CLASS_THERMOSTAT_FAN_MODE],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_WHATEVER),
|
||||
('lock',
|
||||
DISCOVER_LOCKS,
|
||||
[COMMAND_CLASS_DOOR_LOCK],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER),
|
||||
@ -235,7 +220,6 @@ def setup(hass, config):
|
||||
def value_added(node, value):
|
||||
"""Called when a value is added to a node on the network."""
|
||||
for (component,
|
||||
discovery_service,
|
||||
command_ids,
|
||||
value_type,
|
||||
value_genre) in DISCOVERY_COMPONENTS:
|
||||
@ -247,9 +231,6 @@ def setup(hass, config):
|
||||
if value_genre is not None and value_genre != value.genre:
|
||||
continue
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, component, config)
|
||||
|
||||
# Configure node
|
||||
name = "{}.{}".format(component, _object_id(value))
|
||||
|
||||
@ -261,14 +242,10 @@ def setup(hass, config):
|
||||
else:
|
||||
value.disable_poll()
|
||||
|
||||
# Fire discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: discovery_service,
|
||||
ATTR_DISCOVERED: {
|
||||
ATTR_NODE_ID: node.node_id,
|
||||
ATTR_VALUE_ID: value.value_id,
|
||||
}
|
||||
})
|
||||
discovery.load_platform(hass, component, DOMAIN, {
|
||||
ATTR_NODE_ID: node.node_id,
|
||||
ATTR_VALUE_ID: value.value_id,
|
||||
}, config)
|
||||
|
||||
def scene_activated(node, scene_id):
|
||||
"""Called when a scene is activated on any node in the network."""
|
||||
|
@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
"""Constants used by Home Assistant components."""
|
||||
|
||||
__version__ = "0.21.2"
|
||||
__version__ = "0.22.0"
|
||||
REQUIRED_PYTHON_VER = (3, 4)
|
||||
|
||||
PLATFORM_FORMAT = '{}.{}'
|
||||
|
@ -73,12 +73,13 @@ def entity_id(value):
|
||||
value = string(value).lower()
|
||||
if valid_entity_id(value):
|
||||
return value
|
||||
raise vol.Invalid('Entity ID {} does not match format <domain>.<object_id>'
|
||||
.format(value))
|
||||
raise vol.Invalid('Entity ID {} is an invalid entity id'.format(value))
|
||||
|
||||
|
||||
def entity_ids(value):
|
||||
"""Validate Entity IDs."""
|
||||
if value is None:
|
||||
raise vol.Invalid('Entity IDs can not be None')
|
||||
if isinstance(value, str):
|
||||
value = [ent_id.strip() for ent_id in value.split(',')]
|
||||
|
||||
|
86
homeassistant/helpers/discovery.py
Normal file
86
homeassistant/helpers/discovery.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""Helper methods to help with platform discovery."""
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
|
||||
|
||||
EVENT_LOAD_PLATFORM = 'load_platform.{}'
|
||||
ATTR_PLATFORM = 'platform'
|
||||
|
||||
|
||||
def listen(hass, service, callback):
|
||||
"""Setup listener for discovery of specific service.
|
||||
|
||||
Service can be a string or a list/tuple.
|
||||
"""
|
||||
if isinstance(service, str):
|
||||
service = (service,)
|
||||
else:
|
||||
service = tuple(service)
|
||||
|
||||
def discovery_event_listener(event):
|
||||
"""Listen for discovery events."""
|
||||
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
|
||||
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
|
||||
|
||||
|
||||
def discover(hass, service, discovered=None, component=None, hass_config=None):
|
||||
"""Fire discovery event. Can ensure a component is loaded."""
|
||||
if component is not None:
|
||||
bootstrap.setup_component(hass, component, hass_config)
|
||||
|
||||
data = {
|
||||
ATTR_SERVICE: service
|
||||
}
|
||||
|
||||
if discovered is not None:
|
||||
data[ATTR_DISCOVERED] = discovered
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
|
||||
|
||||
|
||||
def listen_platform(hass, component, callback):
|
||||
"""Register a platform loader listener."""
|
||||
service = EVENT_LOAD_PLATFORM.format(component)
|
||||
|
||||
def discovery_platform_listener(event):
|
||||
"""Listen for platform discovery events."""
|
||||
if event.data.get(ATTR_SERVICE) != service:
|
||||
return
|
||||
|
||||
platform = event.data.get(ATTR_PLATFORM)
|
||||
|
||||
if not platform:
|
||||
return
|
||||
|
||||
callback(platform, event.data.get(ATTR_DISCOVERED))
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener)
|
||||
|
||||
|
||||
def load_platform(hass, component, platform, discovered=None,
|
||||
hass_config=None):
|
||||
"""Load a component and platform dynamically.
|
||||
|
||||
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
|
||||
fired to load the platform. The event will contain:
|
||||
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
|
||||
ATTR_PLATFORM = <<platform>>
|
||||
ATTR_DISCOVERED = <<discovery info>> }
|
||||
|
||||
Use `listen_platform` to register a callback for these events.
|
||||
"""
|
||||
if component is not None:
|
||||
bootstrap.setup_component(hass, component, hass_config)
|
||||
|
||||
data = {
|
||||
ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component),
|
||||
ATTR_PLATFORM: platform,
|
||||
}
|
||||
|
||||
if discovered is not None:
|
||||
data[ATTR_DISCOVERED] = discovered
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
|
@ -2,11 +2,11 @@
|
||||
from threading import Lock
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import discovery, group
|
||||
from homeassistant.components import group
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE,
|
||||
DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
@ -20,8 +20,7 @@ class EntityComponent(object):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, logger, domain, hass,
|
||||
scan_interval=DEFAULT_SCAN_INTERVAL,
|
||||
discovery_platforms=None, group_name=None):
|
||||
scan_interval=DEFAULT_SCAN_INTERVAL, group_name=None):
|
||||
"""Initialize an entity component."""
|
||||
self.logger = logger
|
||||
self.hass = hass
|
||||
@ -29,7 +28,6 @@ class EntityComponent(object):
|
||||
self.domain = domain
|
||||
self.entity_id_format = domain + '.{}'
|
||||
self.scan_interval = scan_interval
|
||||
self.discovery_platforms = discovery_platforms
|
||||
self.group_name = group_name
|
||||
|
||||
self.entities = {}
|
||||
@ -54,23 +52,14 @@ class EntityComponent(object):
|
||||
for p_type, p_config in config_per_platform(config, self.domain):
|
||||
self._setup_platform(p_type, p_config)
|
||||
|
||||
if self.discovery_platforms:
|
||||
# Discovery listener for all items in discovery_platforms array
|
||||
# passed from a component's setup method (e.g. light/__init__.py)
|
||||
discovery.listen(
|
||||
self.hass, self.discovery_platforms.keys(),
|
||||
lambda service, info:
|
||||
self._setup_platform(self.discovery_platforms[service], {},
|
||||
info))
|
||||
|
||||
# Generic discovery listener for loading platform dynamically
|
||||
# Refer to: homeassistant.components.discovery.load_platform()
|
||||
def load_platform_callback(service, info):
|
||||
def component_platform_discovered(platform, info):
|
||||
"""Callback to load a platform."""
|
||||
platform = info.pop(discovery.LOAD_PLATFORM)
|
||||
self._setup_platform(platform, {}, info if info else None)
|
||||
discovery.listen(self.hass, discovery.LOAD_PLATFORM + '.' +
|
||||
self.domain, load_platform_callback)
|
||||
self._setup_platform(platform, {}, info)
|
||||
|
||||
discovery.listen_platform(self.hass, self.domain,
|
||||
component_platform_discovered)
|
||||
|
||||
def extract_from_service(self, service):
|
||||
"""Extract all known entities from a service call.
|
||||
|
@ -17,8 +17,8 @@ def track_state_change(hass, entity_ids, action, from_state=None,
|
||||
Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
|
||||
Pass the return value into hass.bus.remove_listener to remove it.
|
||||
"""
|
||||
from_state = _process_match_param(from_state)
|
||||
to_state = _process_match_param(to_state)
|
||||
from_state = _process_state_match(from_state)
|
||||
to_state = _process_state_match(to_state)
|
||||
|
||||
# Ensure it is a lowercase list with entity ids we want to match on
|
||||
if entity_ids == MATCH_ALL:
|
||||
@ -155,7 +155,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
|
||||
hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener)
|
||||
return time_change_listener
|
||||
|
||||
pmp = _process_match_param
|
||||
pmp = _process_time_match
|
||||
year, month, day = pmp(year), pmp(month), pmp(day)
|
||||
hour, minute, second = pmp(hour), pmp(minute), pmp(second)
|
||||
|
||||
@ -190,7 +190,17 @@ def track_time_change(hass, action, year=None, month=None, day=None,
|
||||
second, local=True)
|
||||
|
||||
|
||||
def _process_match_param(parameter):
|
||||
def _process_state_match(parameter):
|
||||
"""Wrap parameter in a tuple if it is not one and returns it."""
|
||||
if parameter is None or parameter == MATCH_ALL:
|
||||
return MATCH_ALL
|
||||
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
|
||||
return (parameter,)
|
||||
else:
|
||||
return tuple(parameter)
|
||||
|
||||
|
||||
def _process_time_match(parameter):
|
||||
"""Wrap parameter in a tuple if it is not one and returns it."""
|
||||
if parameter is None or parameter == MATCH_ALL:
|
||||
return MATCH_ALL
|
||||
|
@ -12,9 +12,13 @@ from homeassistant.components.notify import (
|
||||
ATTR_MESSAGE, SERVICE_NOTIFY)
|
||||
from homeassistant.components.sun import (
|
||||
STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON)
|
||||
from homeassistant.components.switch.mysensors import (
|
||||
ATTR_IR_CODE, SERVICE_SEND_IR_CODE)
|
||||
from homeassistant.components.thermostat import (
|
||||
ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_TEMPERATURE)
|
||||
from homeassistant.components.thermostat.ecobee import (
|
||||
ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME)
|
||||
from homeassistant.components.hvac import (
|
||||
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION_MODE, ATTR_AUX_HEAT,
|
||||
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING_MODE,
|
||||
@ -46,12 +50,14 @@ SERVICE_ATTRIBUTES = {
|
||||
SERVICE_NOTIFY: [ATTR_MESSAGE],
|
||||
SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE],
|
||||
SERVICE_SET_FAN_MODE: [ATTR_FAN],
|
||||
SERVICE_SET_FAN_MIN_ON_TIME: [ATTR_FAN_MIN_ON_TIME],
|
||||
SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE],
|
||||
SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY],
|
||||
SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE],
|
||||
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
|
||||
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
|
||||
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
|
||||
SERVICE_SEND_IR_CODE: [ATTR_IR_CODE]
|
||||
}
|
||||
|
||||
# Update this dict when new services are added to HA.
|
||||
|
@ -1,10 +1,35 @@
|
||||
"""Color util methods."""
|
||||
import logging
|
||||
import math
|
||||
# pylint: disable=unused-import
|
||||
from webcolors import html5_parse_legacy_color as color_name_to_rgb # noqa
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HASS_COLOR_MAX = 500 # mireds (inverted)
|
||||
HASS_COLOR_MIN = 154
|
||||
COLORS = {
|
||||
'white': (255, 255, 255), 'beige': (245, 245, 220),
|
||||
'tan': (210, 180, 140), 'gray': (128, 128, 128),
|
||||
'navy blue': (0, 0, 128), 'royal blue': (8, 76, 158),
|
||||
'blue': (0, 0, 255), 'azure': (0, 127, 255), 'aqua': (127, 255, 212),
|
||||
'teal': (0, 128, 128), 'green': (0, 255, 0),
|
||||
'forest green': (34, 139, 34), 'olive': (128, 128, 0),
|
||||
'chartreuse': (127, 255, 0), 'lime': (191, 255, 0),
|
||||
'golden': (255, 215, 0), 'red': (255, 0, 0), 'coral': (0, 63, 72),
|
||||
'hot pink': (252, 15, 192), 'fuchsia': (255, 119, 255),
|
||||
'lavender': (181, 126, 220), 'indigo': (75, 0, 130),
|
||||
'maroon': (128, 0, 0), 'crimson': (220, 20, 60)}
|
||||
|
||||
|
||||
def color_name_to_rgb(color_name):
|
||||
"""Convert color name to RGB hex value."""
|
||||
hex_value = COLORS.get(color_name.lower())
|
||||
|
||||
if not hex_value:
|
||||
_LOGGER.error('unknown color supplied %s default to white', color_name)
|
||||
hex_value = COLORS['white']
|
||||
|
||||
return hex_value
|
||||
|
||||
|
||||
# Taken from:
|
||||
|
@ -5,7 +5,6 @@ pytz>=2016.4
|
||||
pip>=7.0.0
|
||||
jinja2>=2.8
|
||||
voluptuous==0.8.9
|
||||
webcolors==1.5
|
||||
eventlet==0.19.0
|
||||
|
||||
# homeassistant.components.isy994
|
||||
@ -30,7 +29,10 @@ Werkzeug==0.11.5
|
||||
apcaccess==0.0.4
|
||||
|
||||
# homeassistant.components.sun
|
||||
astral==1.1
|
||||
astral==1.2
|
||||
|
||||
# homeassistant.components.sensor.swiss_hydrological_data
|
||||
beautifulsoup4==4.4.1
|
||||
|
||||
# homeassistant.components.light.blinksticklight
|
||||
blinkstick==1.1.7
|
||||
@ -95,9 +97,6 @@ hikvision==0.4
|
||||
# homeassistant.components.sensor.dht
|
||||
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
|
||||
|
||||
# homeassistant.components.sensor.netatmo
|
||||
https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip#lnetatmo==0.4.0
|
||||
|
||||
# homeassistant.components.switch.dlink
|
||||
https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1
|
||||
|
||||
@ -124,6 +123,9 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.
|
||||
# homeassistant.components.device_tracker.fritz
|
||||
# https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
https://github.com/jabesq/netatmo-api-python/archive/v0.5.0.zip#lnetatmo==0.5.0
|
||||
|
||||
# homeassistant.components.sensor.sabnzbd
|
||||
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
|
||||
|
||||
@ -131,7 +133,7 @@ https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad591540925
|
||||
https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip#pyqwikswitch==0.4
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
https://github.com/nkgilley/python-ecobee-api/archive/4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5
|
||||
https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6
|
||||
|
||||
# homeassistant.components.switch.edimax
|
||||
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
|
||||
@ -172,6 +174,9 @@ lightify==1.0.3
|
||||
# homeassistant.components.light.limitlessled
|
||||
limitlessled==1.0.0
|
||||
|
||||
# homeassistant.components.sensor.swiss_hydrological_data
|
||||
lxml==3.6.0
|
||||
|
||||
# homeassistant.components.notify.message_bird
|
||||
messagebird==1.2.0
|
||||
|
||||
@ -196,12 +201,14 @@ panasonic_viera==0.2
|
||||
|
||||
# homeassistant.components.device_tracker.aruba
|
||||
# homeassistant.components.device_tracker.asuswrt
|
||||
# homeassistant.components.media_player.pandora
|
||||
pexpect==4.0.1
|
||||
|
||||
# homeassistant.components.light.hue
|
||||
phue==0.8
|
||||
|
||||
# homeassistant.components.media_player.plex
|
||||
# homeassistant.components.sensor.plex
|
||||
plexapi==1.1.0
|
||||
|
||||
# homeassistant.components.thermostat.proliphix
|
||||
@ -252,6 +259,9 @@ pyloopenergy==0.0.13
|
||||
# homeassistant.components.device_tracker.netgear
|
||||
pynetgear==0.3.3
|
||||
|
||||
# homeassistant.components.switch.netio
|
||||
pynetio==0.1.6
|
||||
|
||||
# homeassistant.components.alarm_control_panel.nx584
|
||||
# homeassistant.components.binary_sensor.nx584
|
||||
pynx584==0.2
|
||||
@ -263,6 +273,7 @@ pyowm==2.3.1
|
||||
pyserial<=3.0
|
||||
|
||||
# homeassistant.components.device_tracker.snmp
|
||||
# homeassistant.components.sensor.snmp
|
||||
pysnmp==4.3.2
|
||||
|
||||
# homeassistant.components.sensor.forecast
|
||||
@ -287,7 +298,7 @@ python-pushover==0.2
|
||||
python-statsd==1.7.2
|
||||
|
||||
# homeassistant.components.notify.telegram
|
||||
python-telegram-bot==4.2.0
|
||||
python-telegram-bot==4.2.1
|
||||
|
||||
# homeassistant.components.sensor.twitch
|
||||
python-twitch==1.2.0
|
||||
@ -297,9 +308,10 @@ python-twitch==1.2.0
|
||||
# homeassistant.components.garage_door.wink
|
||||
# homeassistant.components.light.wink
|
||||
# homeassistant.components.lock.wink
|
||||
# homeassistant.components.rollershutter.wink
|
||||
# homeassistant.components.sensor.wink
|
||||
# homeassistant.components.switch.wink
|
||||
python-wink==0.7.6
|
||||
python-wink==0.7.7
|
||||
|
||||
# homeassistant.components.keyboard
|
||||
pyuserinput==0.1.9
|
||||
|
@ -5,5 +5,6 @@ pytest>=2.9.1
|
||||
pytest-cov>=2.2.0
|
||||
pytest-timeout>=1.0.0
|
||||
pytest-capturelog>=0.7
|
||||
betamax==0.5.1
|
||||
betamax==0.7.0
|
||||
pydocstyle>=1.0.0
|
||||
httpretty==0.8.14
|
||||
|
1
setup.py
1
setup.py
@ -17,7 +17,6 @@ REQUIRES = [
|
||||
'pip>=7.0.0',
|
||||
'jinja2>=2.8',
|
||||
'voluptuous==0.8.9',
|
||||
'webcolors==1.5',
|
||||
'eventlet==0.19.0',
|
||||
]
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
69
tests/components/device_tracker/test_asuswrt.py
Normal file
69
tests/components/device_tracker/test_asuswrt.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""The tests for the ASUSWRT device tracker platform."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from homeassistant.components import device_tracker
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME,
|
||||
CONF_HOST)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
|
||||
"""Tests for the ASUSWRT device tracker platform."""
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
"""Stop everything that was started."""
|
||||
try:
|
||||
os.remove(self.hass.config.path(device_tracker.YAML_DEVICES))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def test_password_or_pub_key_required(self):
|
||||
"""Test creating an AsusWRT scanner without a pass or pubkey."""
|
||||
self.assertIsNone(device_tracker.asuswrt.get_scanner(
|
||||
self.hass, {device_tracker.DOMAIN: {
|
||||
CONF_PLATFORM: 'asuswrt',
|
||||
CONF_HOST: 'fake_host',
|
||||
CONF_USERNAME: 'fake_user'
|
||||
}}))
|
||||
|
||||
@mock.patch(
|
||||
'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner',
|
||||
return_value=mock.MagicMock())
|
||||
def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock):
|
||||
"""Test creating an AsusWRT scanner with a password and no pubkey."""
|
||||
conf_dict = {
|
||||
device_tracker.DOMAIN: {
|
||||
CONF_PLATFORM: 'asuswrt',
|
||||
CONF_HOST: 'fake_host',
|
||||
CONF_USERNAME: 'fake_user',
|
||||
CONF_PASSWORD: 'fake_pass'
|
||||
}
|
||||
}
|
||||
self.assertIsNotNone(device_tracker.asuswrt.get_scanner(
|
||||
self.hass, conf_dict))
|
||||
asuswrt_mock.assert_called_once_with(conf_dict[device_tracker.DOMAIN])
|
||||
|
||||
@mock.patch(
|
||||
'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner',
|
||||
return_value=mock.MagicMock())
|
||||
def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock):
|
||||
"""Test creating an AsusWRT scanner with a pubkey and no password."""
|
||||
conf_dict = {
|
||||
device_tracker.DOMAIN: {
|
||||
CONF_PLATFORM: 'asuswrt',
|
||||
CONF_HOST: 'fake_host',
|
||||
CONF_USERNAME: 'fake_user',
|
||||
'pub_key': '/fake_path'
|
||||
}
|
||||
}
|
||||
self.assertIsNotNone(device_tracker.asuswrt.get_scanner(
|
||||
self.hass, conf_dict))
|
||||
asuswrt_mock.assert_called_once_with(conf_dict[device_tracker.DOMAIN])
|
53
tests/components/device_tracker/test_bt_home_hub_5.py
Normal file
53
tests/components/device_tracker/test_bt_home_hub_5.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""The tests for the BT Home Hub 5 device tracker platform."""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.device_tracker import bt_home_hub_5
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
patch_file = 'homeassistant.components.device_tracker.bt_home_hub_5'
|
||||
|
||||
|
||||
def _get_homehub_data(url):
|
||||
"""Return mock homehub data."""
|
||||
return '''
|
||||
[
|
||||
{
|
||||
"mac": "AA:BB:CC:DD:EE:FF,
|
||||
"hostname": "hostname",
|
||||
"ip": "192.168.1.43",
|
||||
"ipv6": "",
|
||||
"name": "hostname",
|
||||
"activity": "1",
|
||||
"os": "Unknown",
|
||||
"device": "Unknown",
|
||||
"time_first_seen": "2016/06/05 11:14:45",
|
||||
"time_last_active": "2016/06/06 11:33:08",
|
||||
"dhcp_option": "39043T90430T9TGK0EKGE5KGE3K904390K45GK054",
|
||||
"port": "wl0",
|
||||
"ipv6_ll": "fe80::gd67:ghrr:fuud:4332",
|
||||
"activity_ip": "1",
|
||||
"activity_ipv6_ll": "0",
|
||||
"activity_ipv6": "0",
|
||||
"device_oui": "NA",
|
||||
"device_serial": "NA",
|
||||
"device_class": "NA"
|
||||
}
|
||||
]
|
||||
'''
|
||||
|
||||
|
||||
class TestBTHomeHub5DeviceTracker(unittest.TestCase):
|
||||
"""Test BT Home Hub 5 device tracker platform."""
|
||||
|
||||
@patch('{}._get_homehub_data'.format(patch_file), new=_get_homehub_data)
|
||||
def test_config_minimal(self):
|
||||
"""Test the setup with minimal configuration."""
|
||||
config = {
|
||||
'device_tracker': {
|
||||
CONF_HOST: 'foo'
|
||||
}
|
||||
}
|
||||
result = bt_home_hub_5.get_scanner(None, config)
|
||||
|
||||
self.assertIsNotNone(result)
|
@ -25,7 +25,7 @@ class TestSensorYr:
|
||||
|
||||
def test_default_setup(self, betamax_session):
|
||||
"""Test the default setup."""
|
||||
now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC)
|
||||
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
|
||||
|
||||
with patch('homeassistant.components.sensor.yr.requests.Session',
|
||||
return_value=betamax_session):
|
||||
@ -37,13 +37,13 @@ class TestSensorYr:
|
||||
|
||||
state = self.hass.states.get('sensor.yr_symbol')
|
||||
|
||||
assert '46' == state.state
|
||||
assert '3' == state.state
|
||||
assert state.state.isnumeric()
|
||||
assert state.attributes.get('unit_of_measurement') is None
|
||||
|
||||
def test_custom_setup(self, betamax_session):
|
||||
"""Test a custom setup."""
|
||||
now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC)
|
||||
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
|
||||
|
||||
with patch('homeassistant.components.sensor.yr.requests.Session',
|
||||
return_value=betamax_session):
|
||||
@ -61,15 +61,15 @@ class TestSensorYr:
|
||||
|
||||
state = self.hass.states.get('sensor.yr_pressure')
|
||||
assert 'hPa' == state.attributes.get('unit_of_measurement')
|
||||
assert '1025.1' == state.state
|
||||
assert '1009.3' == state.state
|
||||
|
||||
state = self.hass.states.get('sensor.yr_wind_direction')
|
||||
assert '°' == state.attributes.get('unit_of_measurement')
|
||||
assert '81.8' == state.state
|
||||
assert '103.6' == state.state
|
||||
|
||||
state = self.hass.states.get('sensor.yr_humidity')
|
||||
assert '%' == state.attributes.get('unit_of_measurement')
|
||||
assert '79.6' == state.state
|
||||
assert '55.5' == state.state
|
||||
|
||||
state = self.hass.states.get('sensor.yr_fog')
|
||||
assert '%' == state.attributes.get('unit_of_measurement')
|
||||
@ -77,4 +77,4 @@ class TestSensorYr:
|
||||
|
||||
state = self.hass.states.get('sensor.yr_wind_speed')
|
||||
assert 'm/s', state.attributes.get('unit_of_measurement')
|
||||
assert '4.3' == state.state
|
||||
assert '3.5' == state.state
|
||||
|
@ -3,7 +3,7 @@
|
||||
import unittest
|
||||
|
||||
import homeassistant.components.configurator as configurator
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
@ -40,26 +40,25 @@ class TestConfigurator(unittest.TestCase):
|
||||
|
||||
def test_request_all_info(self):
|
||||
"""Test request config with all possible info."""
|
||||
values = [
|
||||
"config_description", "config image url",
|
||||
"config submit caption", []]
|
||||
|
||||
keys = [
|
||||
configurator.ATTR_DESCRIPTION, configurator.ATTR_DESCRIPTION_IMAGE,
|
||||
configurator.ATTR_SUBMIT_CAPTION, configurator.ATTR_FIELDS]
|
||||
|
||||
exp_attr = dict(zip(keys, values))
|
||||
|
||||
exp_attr[configurator.ATTR_CONFIGURE_ID] = configurator.request_config(
|
||||
self.hass, "Test Request", lambda _: None,
|
||||
*values)
|
||||
exp_attr = {
|
||||
ATTR_FRIENDLY_NAME: "Test Request",
|
||||
configurator.ATTR_DESCRIPTION: "config description",
|
||||
configurator.ATTR_DESCRIPTION_IMAGE: "config image url",
|
||||
configurator.ATTR_SUBMIT_CAPTION: "config submit caption",
|
||||
configurator.ATTR_FIELDS: [],
|
||||
configurator.ATTR_CONFIGURE_ID: configurator.request_config(
|
||||
self.hass, "Test Request", lambda _: None,
|
||||
"config description", "config image url",
|
||||
"config submit caption"
|
||||
)
|
||||
}
|
||||
|
||||
states = self.hass.states.all()
|
||||
self.assertEqual(1, len(states))
|
||||
state = states[0]
|
||||
|
||||
self.assertEqual(configurator.STATE_CONFIGURE, state.state)
|
||||
self.assertEqual(exp_attr, state.attributes)
|
||||
assert exp_attr == dict(state.attributes)
|
||||
|
||||
def test_callback_called_on_configure(self):
|
||||
"""Test if our callback gets called when configure service called."""
|
||||
|
76
tests/components/test_forecast.py
Normal file
76
tests/components/test_forecast.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""The tests for the forecast.io platform."""
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import forecastio
|
||||
import httpretty
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from homeassistant.components.sensor import forecast
|
||||
from homeassistant import core as ha
|
||||
|
||||
|
||||
class TestForecastSetup(unittest.TestCase):
|
||||
"""Test the forecast.io platform."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize values for this testcase class."""
|
||||
self.hass = ha.HomeAssistant()
|
||||
self.key = 'foo'
|
||||
self.config = {
|
||||
'api_key': 'foo',
|
||||
'monitored_conditions': ['summary', 'icon']
|
||||
}
|
||||
self.lat = 37.8267
|
||||
self.lon = -122.423
|
||||
self.hass.config.latitude = self.lat
|
||||
self.hass.config.longitude = self.lon
|
||||
|
||||
def test_setup_no_latitude(self):
|
||||
"""Test that the component is not loaded without required config."""
|
||||
self.hass.config.latitude = None
|
||||
self.assertFalse(forecast.setup_platform(self.hass, {}, MagicMock()))
|
||||
|
||||
@patch('forecastio.api.get_forecast')
|
||||
def test_setup_bad_api_key(self, mock_get_forecast):
|
||||
"""Test for handling a bad API key."""
|
||||
# The forecast API wrapper that we use raises an HTTP error
|
||||
# when you try to use a bad (or no) API key.
|
||||
url = 'https://api.forecast.io/forecast/{}/{},{}?units=auto'.format(
|
||||
self.key, str(self.lat), str(self.lon)
|
||||
)
|
||||
msg = '400 Client Error: Bad Request for url: {}'.format(url)
|
||||
mock_get_forecast.side_effect = HTTPError(msg,)
|
||||
|
||||
response = forecast.setup_platform(self.hass, self.config, MagicMock())
|
||||
self.assertFalse(response)
|
||||
|
||||
@httpretty.activate
|
||||
@patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast)
|
||||
def test_setup(self, mock_get_forecast):
|
||||
"""Test for successfully setting up the forecast.io platform."""
|
||||
def load_fixture_from_json():
|
||||
cwd = os.path.dirname(__file__)
|
||||
fixture_path = os.path.join(cwd, '..', 'fixtures', 'forecast.json')
|
||||
with open(fixture_path) as file:
|
||||
content = json.load(file)
|
||||
return json.dumps(content)
|
||||
|
||||
# Mock out any calls to the actual API and
|
||||
# return the fixture json instead
|
||||
uri = 'api.forecast.io\/forecast\/(\w+)\/(-?\d+\.?\d*),(-?\d+\.?\d*)'
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
re.compile(uri),
|
||||
body=load_fixture_from_json(),
|
||||
)
|
||||
# The following will raise an error if the regex for the mock was
|
||||
# incorrect and we actually try to go out to the internet.
|
||||
httpretty.HTTPretty.allow_net_connect = False
|
||||
|
||||
forecast.setup_platform(self.hass, self.config, MagicMock())
|
||||
self.assertTrue(mock_get_forecast.called)
|
||||
self.assertEqual(mock_get_forecast.call_count, 1)
|
@ -51,6 +51,30 @@ class TestShellCommand(unittest.TestCase):
|
||||
}
|
||||
})
|
||||
|
||||
def test_template_render_no_template(self):
|
||||
"""Ensure shell_commands without templates get rendered properly."""
|
||||
cmd, shell = shell_command._parse_command(self.hass, 'ls /bin', {})
|
||||
self.assertTrue(shell)
|
||||
self.assertEqual(cmd, 'ls /bin')
|
||||
|
||||
def test_template_render(self):
|
||||
"""Ensure shell_commands with templates get rendered properly."""
|
||||
self.hass.states.set('sensor.test_state', 'Works')
|
||||
cmd, shell = shell_command._parse_command(
|
||||
self.hass,
|
||||
'ls /bin {{ states.sensor.test_state.state }}', {}
|
||||
)
|
||||
self.assertFalse(shell, False)
|
||||
self.assertEqual(cmd[-1], 'Works')
|
||||
|
||||
def test_invalid_template_fails(self):
|
||||
"""Test that shell_commands with invalid templates fail."""
|
||||
cmd, _shell = shell_command._parse_command(
|
||||
self.hass,
|
||||
'ls /bin {{ states. .test_state.state }}', {}
|
||||
)
|
||||
self.assertEqual(cmd, None)
|
||||
|
||||
@patch('homeassistant.components.shell_command.subprocess.call',
|
||||
side_effect=SubprocessError)
|
||||
@patch('homeassistant.components.shell_command._LOGGER.error')
|
||||
|
1462
tests/fixtures/forecast.json
vendored
Normal file
1462
tests/fixtures/forecast.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user