Merge pull request #6756 from home-assistant/release-0-41

0.41
This commit is contained in:
Fabian Affolter 2017-03-26 00:01:49 +01:00 committed by GitHub
commit 1f046972d9
171 changed files with 6269 additions and 1899 deletions

View file

@ -59,6 +59,9 @@ omit =
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
homeassistant/components/lutron_caseta.py
homeassistant/components/*/lutron_caseta.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
@ -115,9 +118,6 @@ omit =
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/zwave/*
homeassistant/components/*/zwave.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
@ -145,6 +145,9 @@ omit =
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py
@ -171,6 +174,7 @@ omit =
homeassistant/components/climate/oem.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/config/zwave.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/myq.py
@ -269,6 +273,7 @@ omit =
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py
@ -327,7 +332,6 @@ omit =
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/ebox.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/emoncms.py
homeassistant/components/sensor/fastdotcom.py
@ -352,6 +356,7 @@ omit =
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/lyft.py
homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mqtt_room.py
@ -429,6 +434,9 @@ omit =
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py
homeassistant/components/zwave/__init__.py
homeassistant/components/zwave/util.py
homeassistant/components/zwave/workaround.py
[report]

View file

@ -1,5 +1,5 @@
include README.rst
include LICENSE
include LICENSE.md
graft homeassistant
prune homeassistant/components/frontend/www_static/home-assistant-polymer
recursive-exclude * *.py[co]

View file

@ -255,10 +255,13 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'):
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
return [sys.executable] + [arg for arg in sys.argv if
arg != '--daemon']
else:
return [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir: str,

View file

@ -21,7 +21,6 @@ import homeassistant.loader as loader
from homeassistant.util.logging import AsyncHandler
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import event_decorators, service
from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__)
@ -127,10 +126,6 @@ def async_from_config_dict(config: Dict[str, Any],
_LOGGER.info('Home Assistant core initialized')
# Give event decorators access to HASS
event_decorators.HASS = hass
service.HASS = hass
# stage 1
for component in components:
if component not in FIRST_INIT_COMPONENT:

View file

@ -113,7 +113,7 @@ def async_setup(hass, config):
if not alarm.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
alarm.async_update_ha_state(True))
if hasattr(alarm, 'async_update'):
update_tasks.append(update_coro)

View file

@ -27,7 +27,7 @@ from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
DOMAIN = 'android_ip_webcam'
REQUIREMENTS = ["pydroid-ipcam==0.4"]
REQUIREMENTS = ["pydroid-ipcam==0.6"]
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
@ -125,11 +125,8 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
CONF_AUTO_DISCOVERY = 'auto_discovery'
CONF_MOTION_SENSOR = 'motion_sensor'
DEFAULT_AUTO_DISCOVERY = True
DEFAULT_MOTION_SENSOR = False
DEFAULT_NAME = 'IP Webcam'
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 10
@ -145,14 +142,11 @@ CONFIG_SCHEMA = vol.Schema({
cv.time_period,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_AUTO_DISCOVERY, default=DEFAULT_AUTO_DISCOVERY):
cv.boolean,
vol.Optional(CONF_SWITCHES, default=[]):
vol.Optional(CONF_SWITCHES, default=None):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
vol.Optional(CONF_SENSORS, default=[]):
vol.Optional(CONF_SENSORS, default=None):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR):
cv.boolean,
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA)
@ -184,6 +178,18 @@ def async_setup(hass, config):
timeout=cam_config[CONF_TIMEOUT]
)
if switches is None:
switches = [setting for setting in cam.enabled_settings
if setting in SWITCHES]
if sensors is None:
sensors = [sensor for sensor in cam.enabled_sensors
if sensor in SENSORS]
sensors.extend(['audio_connections', 'video_connections'])
if motion is None:
motion = 'motion_active' in cam.enabled_sensors
@asyncio.coroutine
def async_update_data(now):
"""Update data from ipcam in SCAN_INTERVAL."""
@ -195,20 +201,6 @@ def async_setup(hass, config):
yield from async_update_data(None)
# use autodiscovery to detect sensors/configs
if cam_config[CONF_AUTO_DISCOVERY]:
if not cam.available:
_LOGGER.error(
"Android webcam %s not found for discovery!", cam.base_url)
return
sensors = [sensor for sensor in cam.enabled_sensors
if sensor in SENSORS]
switches = [setting for setting in cam.enabled_settings
if setting in SWITCHES]
motion = True if 'motion_active' in cam.enabled_sensors else False
sensors.extend(['audio_connections', 'video_connections'])
# load platforms
webcams[host] = cam

View file

@ -327,6 +327,8 @@ class APIEventForwardingView(HomeAssistantView):
@asyncio.coroutine
def post(self, request):
"""Setup an event forwarder."""
_LOGGER.warning('Event forwarding is deprecated. '
'Will be removed by 0.43')
hass = request.app['hass']
try:
data = yield from request.json()

View file

@ -36,6 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for droplet in droplets:
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
if droplet_id is None:
_LOGGER.error("Droplet %s is not available", droplet)
return False
dev.append(DigitalOceanBinarySensor(
digital_ocean.DIGITAL_OCEAN, droplet_id))

View file

@ -0,0 +1,157 @@
"""Sensor to indicate whether the current day is a workday."""
import asyncio
import logging
import datetime
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
CONF_NAME, WEEKDAYS)
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.8.1']
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
# By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
CONF_EXCLUDES = 'excludes'
# By default, public holidays, Saturdays and Sundays are excluded from workdays
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor'
ALLOWED_DAYS = WEEKDAYS + ['holiday']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
vol.Optional(CONF_PROVINCE, default=None): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Workday sensor."""
import holidays
# Get the Sensor name from the config
sensor_name = config.get(CONF_NAME)
# Get the country code from the config
country = config.get(CONF_COUNTRY)
# Get the province from the config
province = config.get(CONF_PROVINCE)
# Get the list of workdays from the config
workdays = config.get(CONF_WORKDAYS)
# Get the list of excludes from the config
excludes = config.get(CONF_EXCLUDES)
# Instantiate the holidays module for the current year
year = datetime.datetime.now().year
obj_holidays = getattr(holidays, country)(years=year)
# Also apply the provience, if available for the configured country
if province:
if province not in obj_holidays.PROVINCES:
_LOGGER.error('There is no province/state %s in country %s',
province, country)
return False
else:
year = datetime.datetime.now().year
obj_holidays = getattr(holidays, country)(prov=province,
years=year)
# Output found public holidays via the debug channel
_LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()):
_LOGGER.debug("%s %s", date, name)
# Add ourselves as device
add_devices([IsWorkdaySensor(obj_holidays, workdays,
excludes, sensor_name)], True)
def day_to_string(day):
"""Convert day index 0 - 7 to string."""
try:
return ALLOWED_DAYS[day]
except IndexError:
return None
class IsWorkdaySensor(Entity):
"""Implementation of a Workday sensor."""
def __init__(self, obj_holidays, workdays, excludes, name):
"""Initialize the Workday sensor."""
self._name = name
self._obj_holidays = obj_holidays
self._workdays = workdays
self._excludes = excludes
self._state = STATE_UNKNOWN
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
def is_include(self, day, now):
"""Check if given day is in the includes list."""
# Check includes
if day in self._workdays:
return True
elif 'holiday' in self._workdays and now in self._obj_holidays:
return True
return False
def is_exclude(self, day, now):
"""Check if given day is in the excludes list."""
# Check excludes
if day in self._excludes:
return True
elif 'holiday' in self._excludes and now in self._obj_holidays:
return True
return False
@asyncio.coroutine
def async_update(self):
"""Get date and look whether it is a holiday."""
# Default is no workday
self._state = STATE_OFF
# Get iso day of the week (1 = Monday, 7 = Sunday)
day = datetime.datetime.today().isoweekday() - 1
day_of_week = day_to_string(day)
if self.is_include(day_of_week, dt_util.now()):
self._state = STATE_ON
if self.is_exclude(day_of_week, dt_util.now()):
self._state = STATE_OFF

View file

@ -19,34 +19,34 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
def get_device(value, **kwargs):
def get_device(values, **kwargs):
"""Create zwave entity device."""
device_mapping = workaround.get_device_mapping(value)
device_mapping = workaround.get_device_mapping(values.primary)
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
# Default the multiplier to 4
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8)
re_arm_multiplier = zwave.get_config_value(values.primary.node, 9) or 4
return ZWaveTriggerSensor(values, "motion", re_arm_multiplier * 8)
if workaround.get_device_component_mapping(value) == DOMAIN:
return ZWaveBinarySensor(value, None)
if workaround.get_device_component_mapping(values.primary) == DOMAIN:
return ZWaveBinarySensor(values, None)
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
return ZWaveBinarySensor(value, None)
if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
return ZWaveBinarySensor(values, None)
return None
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
"""Representation of a binary sensor within Z-Wave."""
def __init__(self, value, device_class):
def __init__(self, values, device_class):
"""Initialize the sensor."""
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self._sensor_type = device_class
self._state = self._value.data
self._state = self.values.primary.data
def update_properties(self):
"""Callback on data changes for node values."""
self._state = self._value.data
self._state = self.values.primary.data
@property
def is_on(self):
@ -58,24 +58,19 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._sensor_type
@property
def should_poll(self):
"""No polling needed."""
return False
class ZWaveTriggerSensor(ZWaveBinarySensor):
"""Representation of a stateless sensor within Z-Wave."""
def __init__(self, value, device_class, re_arm_sec=60):
def __init__(self, values, device_class, re_arm_sec=60):
"""Initialize the sensor."""
super(ZWaveTriggerSensor, self).__init__(value, device_class)
super(ZWaveTriggerSensor, self).__init__(values, device_class)
self.re_arm_sec = re_arm_sec
self.invalidate_after = None
def update_properties(self):
"""Called when a value for this entity's node has changed."""
self._state = self._value.data
self._state = self.values.primary.data
# only allow this value to be true for re_arm secs
if not self.hass:
return

View file

@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'blink'
REQUIREMENTS = ['blinkpy==0.4.4']
REQUIREMENTS = ['blinkpy==0.5.2']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({

View file

@ -15,7 +15,7 @@ from homeassistant.util import Throttle
DEPENDENCIES = ['blink']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
_LOGGER = logging.getLogger(__name__)

View file

@ -1,67 +0,0 @@
"""
Support for internal dispatcher image push to Camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.dispatcher/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
CONF_SIGNAL = 'signal'
DEFAULT_NAME = 'Dispatcher Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SIGNAL): cv.slugify,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a dispatcher camera."""
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices(
[DispatcherCamera(config[CONF_NAME], config[CONF_SIGNAL])])
class DispatcherCamera(Camera):
"""A dispatcher implementation of an camera."""
def __init__(self, name, signal):
"""Initialize a dispatcher camera."""
super().__init__()
self._name = name
self._signal = signal
self._image = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Register dispatcher and callbacks."""
@callback
def async_update_image(image):
"""Update image from dispatcher call."""
self._image = image
async_dispatcher_connect(self.hass, self._signal, async_update_image)
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
return self._image
@property
def name(self):
"""Return the name of this device."""
return self._name

View file

@ -14,7 +14,7 @@ import async_timeout
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import (
@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
TIMEOUT = 5
DEFAULT_TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
@ -51,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@ -60,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a Synology IP Camera."""
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
websession_init = async_get_clientsession(hass, verify_ssl)
# Determine API to use for authentication
@ -74,7 +76,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}
query_req = None
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
with async_timeout.timeout(timeout, loop=hass.loop):
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload
@ -103,7 +105,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url
syno_auth_url,
timeout
)
# init websession
@ -120,7 +123,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
'version': '1'
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
with async_timeout.timeout(timeout, loop=hass.loop):
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload
@ -149,7 +152,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
snapshot_path,
streaming_path,
camera_path,
auth_path
auth_path,
timeout
)
devices.append(device)
@ -157,7 +161,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@asyncio.coroutine
def get_session_id(hass, websession, username, password, login_url):
def get_session_id(hass, websession, username, password, login_url, timeout):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
@ -170,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url):
}
auth_req = None
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
with async_timeout.timeout(timeout, loop=hass.loop):
auth_req = yield from websession.get(
login_url,
params=auth_payload
@ -192,7 +196,7 @@ class SynologyCamera(Camera):
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path):
auth_path, timeout):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
@ -206,6 +210,7 @@ class SynologyCamera(Camera):
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._timeout = timeout
def camera_image(self):
"""Return bytes of camera image."""
@ -225,7 +230,7 @@ class SynologyCamera(Camera):
'cameraId': self._camera_id
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
response = yield from self._websession.get(
image_url,
params=image_payload

View file

@ -19,6 +19,9 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zoneminder']
DOMAIN = 'zoneminder'
# From ZoneMinder's web/includes/config.php.in
ZM_STATE_ALARM = "2"
def _get_image_url(hass, monitor, mode):
zm_data = hass.data[DOMAIN]
@ -69,10 +72,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
}
cameras.append(MjpegCamera(hass, device_info))
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
if not cameras:
_LOGGER.warning('No active cameras found')
return
async_add_devices(cameras)
class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream."""
def __init__(self, hass, device_info, monitor):
"""Initialize as a subclass of MjpegCamera."""
super().__init__(hass, device_info)
self._monitor_id = int(monitor['Id'])
self._is_recording = None
@property
def should_poll(self):
"""Update the recording state periodically."""
return True
def update(self):
"""Update our recording state from the ZM API."""
_LOGGER.debug('Updating camera state for monitor %i', self._monitor_id)
status_response = zoneminder.get_state(
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
)
if not status_response:
_LOGGER.warning('Could not get status for monitor %i',
self._monitor_id)
return
self._is_recording = status_response['status'] == ZM_STATE_ALARM
@property
def is_recording(self):
"""Return whether the monitor is in alarm mode."""
return self._is_recording

View file

@ -224,7 +224,7 @@ def async_setup(hass, config):
if not climate.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
climate.async_update_ha_state(True))
if hasattr(climate, 'async_update'):
update_tasks.append(update_coro)

View file

@ -0,0 +1,296 @@
"""tado component to create a climate device for each zone."""
import logging
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.climate import (
ClimateDevice)
from homeassistant.const import (
ATTR_TEMPERATURE)
from homeassistant.components.tado import (
DATA_TADO)
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
# When we change the temperature setting, we need an overlay mode
# wait until tado changes the mode automatic
CONST_OVERLAY_TADO_MODE = "TADO_MODE"
# the user has change the temperature or mode manually
CONST_OVERLAY_MANUAL = "MANUAL"
# the temperature will be reset after a timespan
CONST_OVERLAY_TIMER = "TIMER"
OPERATION_LIST = {
CONST_OVERLAY_MANUAL: "Manual",
CONST_OVERLAY_TIMER: "Timer",
CONST_OVERLAY_TADO_MODE: "Tado mode",
CONST_MODE_SMART_SCHEDULE: "Smart schedule",
CONST_MODE_OFF: "Off"}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the climate platform."""
# get the PyTado object from the hub component
tado = hass.data[DATA_TADO]
try:
zones = tado.get_zones()
except RuntimeError:
_LOGGER.error("Unable to get zone info from mytado")
return False
climate_devices = []
for zone in zones:
climate_devices.append(create_climate_device(tado, hass,
zone,
zone['name'],
zone['id']))
if len(climate_devices) > 0:
add_devices(climate_devices, True)
return True
else:
return False
def create_climate_device(tado, hass, zone, name, zone_id):
"""Create a climate device."""
capabilities = tado.get_capabilities(zone_id)
unit = TEMP_CELSIUS
min_temp = float(capabilities["temperatures"]["celsius"]["min"])
max_temp = float(capabilities["temperatures"]["celsius"]["max"])
ac_mode = capabilities["type"] != "HEATING"
data_id = 'zone {} {}'.format(name, zone_id)
device = TadoClimate(tado,
name, zone_id, data_id,
hass.config.units.temperature(min_temp, unit),
hass.config.units.temperature(max_temp, unit),
ac_mode)
tado.add_sensor(data_id, {
"id": zone_id,
"zone": zone,
"name": name,
"climate": device
})
return device
class TadoClimate(ClimateDevice):
"""Representation of a tado climate device."""
def __init__(self, store, zone_name, zone_id, data_id,
min_temp, max_temp, ac_mode,
tolerance=0.3):
"""Initialization of TadoClimate device."""
self._store = store
self._data_id = data_id
self.zone_name = zone_name
self.zone_id = zone_id
self.ac_mode = ac_mode
self._active = False
self._device_is_active = False
self._unit = TEMP_CELSIUS
self._cur_temp = None
self._cur_humidity = None
self._is_away = False
self._min_temp = min_temp
self._max_temp = max_temp
self._target_temp = None
self._tolerance = tolerance
self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
@property
def name(self):
"""Return the name of the sensor."""
return self.zone_name
@property
def current_humidity(self):
"""Return the current humidity."""
return self._cur_humidity
@property
def current_temperature(self):
"""Return the sensor temperature."""
return self._cur_temp
@property
def current_operation(self):
"""Return current readable operation mode."""
return OPERATION_LIST.get(self._current_operation)
@property
def operation_list(self):
"""List of available operation modes (readable)."""
return list(OPERATION_LIST.values())
@property
def temperature_unit(self):
"""The unit of measurement used by the platform."""
return self._unit
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._is_away
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
def set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
self._current_operation = CONST_OVERLAY_TADO_MODE
self._overlay_mode = None
self._target_temp = temperature
self._control_heating()
def set_operation_mode(self, readable_operation_mode):
"""Set new operation mode."""
operation_mode = CONST_MODE_SMART_SCHEDULE
for mode, readable in OPERATION_LIST.items():
if readable == readable_operation_mode:
operation_mode = mode
break
self._current_operation = operation_mode
self._overlay_mode = None
self._control_heating()
@property
def min_temp(self):
"""Return the minimum temperature."""
if self._min_temp:
return self._min_temp
else:
# get default temp from super class
return super().min_temp
@property
def max_temp(self):
"""Return the maximum temperature."""
if self._max_temp:
return self._max_temp
else:
# Get default temp from super class
return super().max_temp
def update(self):
"""Update the state of this climate device."""
self._store.update()
data = self._store.get_data(self._data_id)
if data is None:
_LOGGER.debug('Recieved no data for zone %s',
self.zone_name)
return
if 'sensorDataPoints' in data:
sensor_data = data['sensorDataPoints']
temperature = float(
sensor_data['insideTemperature']['celsius'])
humidity = float(
sensor_data['humidity']['percentage'])
setting = 0
# temperature setting will not exist when device is off
if 'temperature' in data['setting'] and \
data['setting']['temperature'] is not None:
setting = float(
data['setting']['temperature']['celsius'])
unit = TEMP_CELSIUS
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
self._target_temp = self.hass.config.units.temperature(
setting, unit)
self._cur_humidity = humidity
if 'tadoMode' in data:
mode = data['tadoMode']
self._is_away = mode == "AWAY"
if 'setting' in data:
power = data['setting']['power']
if power == "OFF":
self._current_operation = CONST_MODE_OFF
self._device_is_active = False
else:
self._device_is_active = True
if 'overlay' in data and data['overlay'] is not None:
overlay = True
termination = data['overlay']['termination']['type']
else:
overlay = False
termination = ""
# if you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
if overlay and self._device_is_active:
# there is an overlay the device is on
self._overlay_mode = termination
self._current_operation = termination
else:
# there is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._current_operation = CONST_MODE_SMART_SCHEDULE
def _control_heating(self):
"""Send new target temperature to mytado."""
if not self._active and None not in (self._cur_temp,
self._target_temp):
self._active = True
_LOGGER.info('Obtained current and target temperature. '
'tado thermostat active.')
if not self._active or self._current_operation == self._overlay_mode:
return
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
_LOGGER.info('Switching mytado.com to SCHEDULE (default) '
'for zone %s', self.zone_name)
self._store.reset_zone_overlay(self.zone_id)
self._overlay_mode = self._current_operation
return
if self._current_operation == CONST_MODE_OFF:
_LOGGER.info('Switching mytado.com to OFF for zone %s',
self.zone_name)
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
self._overlay_mode = self._current_operation
return
_LOGGER.info('Switching mytado.com to %s mode for zone %s',
self._current_operation, self.zone_name)
self._store.set_zone_overlay(self.zone_id,
self._current_operation,
self._target_temp)
self._overlay_mode = self._current_operation

View file

@ -85,11 +85,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
return self.vera_device.fan_cycle()
@property
def current_power_mwh(self):
"""Current power usage in mWh."""
def current_power_w(self):
"""Current power usage in W."""
power = self.vera_device.power
if power:
return convert(power, float, 0.0) * 1000
return convert(power, float, 0.0)
def update(self):
"""Called by the vera device callback to update state."""

View file

@ -10,7 +10,6 @@ import logging
from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
@ -33,20 +32,18 @@ DEVICE_MAPPINGS = {
}
def get_device(hass, value, **kwargs):
def get_device(hass, values, **kwargs):
"""Create zwave entity device."""
temp_unit = hass.config.units.temperature_unit
return ZWaveClimate(value, temp_unit)
return ZWaveClimate(values, temp_unit)
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Representation of a Z-Wave Climate device."""
def __init__(self, value, temp_unit):
def __init__(self, values, temp_unit):
"""Initialize the Z-Wave climate device."""
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._index = value.index
self._node = value.node
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self._target_temperature = None
self._current_temperature = None
self._current_operation = None
@ -61,10 +58,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("temp_unit is %s", self._unit)
self._zxt_120 = None
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if (self.node.manufacturer_id.strip() and
self.node.product_id.strip()):
specific_sensor_key = (
int(self.node.manufacturer_id, 16),
int(self.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
@ -75,86 +73,58 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
def update_properties(self):
"""Callback on data changes for node values."""
# Operation Mode
self._current_operation = self.get_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
operation_list = self.get_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
member='data_items')
if operation_list:
self._operation_list = list(operation_list)
if self.values.mode:
self._current_operation = self.values.mode.data
operation_list = self.values.mode.data_items
if operation_list:
self._operation_list = list(operation_list)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
_LOGGER.debug("self._current_operation=%s", self._current_operation)
# Current Temp
self._current_temperature = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
label=['Temperature'], member='data')
device_unit = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
label=['Temperature'], member='units')
if device_unit is not None:
self._unit = device_unit
if self.values.temperature:
self._current_temperature = self.values.temperature.data
device_unit = self.values.temperature.units
if device_unit is not None:
self._unit = device_unit
# Fan Mode
self._current_fan_mode = self.get_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
member='data')
fan_list = self.get_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
member='data_items')
if fan_list:
self._fan_list = list(fan_list)
if self.values.fan_mode:
self._current_fan_mode = self.values.fan_mode.data
fan_list = self.values.fan_mode.data_items
if fan_list:
self._fan_list = list(fan_list)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
_LOGGER.debug("self._current_fan_mode=%s",
self._current_fan_mode)
# Swing mode
if self._zxt_120 == 1:
self._current_swing_mode = (
self.get_value(
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
index=33,
member='data'))
swing_list = self.get_value(class_id=zwave.const
.COMMAND_CLASS_CONFIGURATION,
index=33,
member='data_items')
if swing_list:
self._swing_list = list(swing_list)
if self.values.zxt_120_swing_mode:
self._current_swing_mode = self.values.zxt_120_swing_mode.data
swing_list = self.values.zxt_120_swing_mode.data_items
if swing_list:
self._swing_list = list(swing_list)
_LOGGER.debug("self._swing_list=%s", self._swing_list)
_LOGGER.debug("self._current_swing_mode=%s",
self._current_swing_mode)
# Set point
temps = []
for value in (
self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
temps.append((round(float(value.data)), 1))
if value.index == self._index:
if value.data == 0:
_LOGGER.debug("Setpoint is 0, setting default to "
"current_temperature=%s",
self._current_temperature)
self._target_temperature = (
round((float(self._current_temperature)), 1))
break
else:
self._target_temperature = round((float(value.data)), 1)
if self.values.primary.data == 0:
_LOGGER.debug("Setpoint is 0, setting default to "
"current_temperature=%s",
self._current_temperature)
self._target_temperature = (
round((float(self._current_temperature)), 1))
else:
self._target_temperature = round(
(float(self.values.primary.data)), 1)
# Operating state
self._operating_state = self.get_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
member='data')
if self.values.operating_state:
self._operating_state = self.values.operating_state.data
# Fan operating state
self._fan_state = self.get_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
member='data')
@property
def should_poll(self):
"""No polling on Z-Wave."""
return False
if self.values.fan_state:
self._fan_state = self.values.fan_state.data
@property
def current_fan_mode(self):
@ -213,41 +183,31 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
else:
return
self.set_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
index=self._index, data=temperature)
self.schedule_update_ha_state()
self.values.primary.data = temperature
def set_fan_mode(self, fan):
"""Set new target fan mode."""
self.set_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
index=0, data=bytes(fan, 'utf-8'))
if self.values.fan_mode:
self.values.fan_mode.data = bytes(fan, 'utf-8')
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
self.set_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
index=0, data=bytes(operation_mode, 'utf-8'))
if self.values.mode:
self.values.mode.data = bytes(operation_mode, 'utf-8')
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
if self._zxt_120 == 1:
self.set_value(
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
index=33, data=bytes(swing_mode, 'utf-8'))
if self.values.zxt_120_swing_mode:
self.values.zxt_120_swing_mode.data = bytes(
swing_mode, 'utf-8')
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
data = super().device_state_attributes
if self._operating_state:
data[ATTR_OPERATING_STATE] = self._operating_state,
data[ATTR_OPERATING_STATE] = self._operating_state
if self._fan_state:
data[ATTR_FAN_STATE] = self._fan_state
return data
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
return None

View file

@ -166,7 +166,7 @@ def async_setup(hass, config):
if not cover.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
cover.async_update_ha_state(True))
if hasattr(cover, 'async_update'):
update_tasks.append(update_coro)

View file

@ -14,8 +14,8 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/arraylabs/pymyq/archive/v0.0.7.zip'
'#pymyq==0.0.7']
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
'#pymyq==0.0.8']
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): cv.string,

View file

@ -20,64 +20,49 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
def get_device(value, **kwargs):
def get_device(values, **kwargs):
"""Create zwave entity device."""
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and value.index == 0):
return ZwaveRollershutter(value)
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
return ZwaveGarageDoor(value)
if (values.primary.command_class ==
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and values.primary.index == 0):
return ZwaveRollershutter(values)
elif (values.primary.command_class in [
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
return ZwaveGarageDoor(values)
return None
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Representation of an Zwave roller shutter."""
def __init__(self, value):
def __init__(self, values):
"""Initialize the zwave rollershutter."""
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
# pylint: disable=no-member
self._node = value.node
self._open_id = None
self._close_id = None
self._current_position_id = None
self._current_position = None
self._workaround = workaround.get_device_mapping(value)
self._workaround = workaround.get_device_mapping(values.primary)
if self._workaround:
_LOGGER.debug("Using workaround %s", self._workaround)
self.update_properties()
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
if not self._node.is_ready:
return None
return [self._current_position_id]
def update_properties(self):
"""Callback on data changes for node values."""
# Position value
if not self._node.is_ready:
if self._current_position_id is None:
self._current_position_id = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
label=['Level'], member='value_id')
if self._open_id is None:
self._open_id = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
label=['Open', 'Up'], member='value_id')
if self._close_id is None:
self._close_id = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
label=['Close', 'Down'], member='value_id')
if self._open_id and self._close_id and \
self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
self._open_id, self._close_id = self._close_id, self._open_id
self._workaround = None
self._current_position = self._node.get_dimmer_level(
self._current_position_id)
self._current_position = self.values.primary.data
if self.values.open and self.values.close and \
self._open_id is None and self._close_id is None:
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
self._open_id = self.values.close.value_id
self._close_id = self.values.open.value_id
self._workaround = None
else:
self._open_id = self.values.open.value_id
self._close_id = self.values.close.value_id
@property
def is_closed(self):
@ -112,7 +97,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
self._node.set_dimmer(self._value.value_id, position)
self.node.set_dimmer(self.values.primary.value_id, position)
def stop_cover(self, **kwargs):
"""Stop the roller shutter."""
@ -122,14 +107,14 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
"""Representation of an Zwave garage door device."""
def __init__(self, value):
def __init__(self, values):
"""Initialize the zwave garage door."""
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self.update_properties()
def update_properties(self):
"""Callback on data changes for node values."""
self._state = self._value.data
self._state = self.values.primary.data
@property
def is_closed(self):
@ -138,11 +123,11 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
def close_cover(self):
"""Close the garage door."""
self._value.data = False
self.values.primary.data = False
def open_cover(self):
"""Open the garage door."""
self._value.data = True
self.values.primary.data = True
@property
def device_class(self):

View file

@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
REQUIREMENTS = ['fritzconnection==0.6']
REQUIREMENTS = ['fritzconnection==0.6.3']
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)

View file

@ -14,6 +14,7 @@ import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
@ -31,6 +32,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
class InvalidLuciTokenError(HomeAssistantError):
"""When an invalid token is detected."""
pass
def get_scanner(hass, config):
"""Validate the configuration and return a Luci scanner."""
scanner = LuciDeviceScanner(config[DOMAIN])
@ -46,8 +53,9 @@ class LuciDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
@ -55,12 +63,15 @@ class LuciDeviceScanner(DeviceScanner):
self.last_results = {}
self.token = _get_token(host, username, password)
self.host = host
self.refresh_token()
self.mac2name = None
self.success_init = self.token is not None
def refresh_token(self):
"""Get a new token."""
self.token = _get_token(self.host, self.username, self.password)
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
@ -98,8 +109,15 @@ class LuciDeviceScanner(DeviceScanner):
_LOGGER.info('Checking ARP')
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
try:
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
except InvalidLuciTokenError:
_LOGGER.info('Refreshing token')
self.refresh_token()
return False
if result:
self.last_results = []
for device_entry in result:
@ -116,6 +134,7 @@ class LuciDeviceScanner(DeviceScanner):
def _req_json_rpc(url, method, *args, **kwargs):
"""Perform one JSON RPC operation."""
data = json.dumps({'method': method, 'params': args})
try:
res = requests.post(url, data=data, timeout=5, **kwargs)
except requests.exceptions.Timeout:
@ -139,6 +158,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
"Failed to authenticate, "
"please check your username and password")
return
elif res.status_code == 403:
_LOGGER.error('Luci responded with a 403 Invalid token')
raise InvalidLuciTokenError
else:
_LOGGER.error('Invalid response from luci: %s', res)

View file

@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.10.1']
REQUIREMENTS = ['python-digitalocean==1.11']
_LOGGER = logging.getLogger(__name__)

View file

@ -218,7 +218,7 @@ def async_setup(hass, config: dict):
if not fan.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
fan.async_update_ha_state(True))
if hasattr(fan, 'async_update'):
update_tasks.append(update_coro)

View file

@ -63,7 +63,7 @@
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: 'shady',
dom: 'shadow',
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};

View file

@ -2,19 +2,19 @@
FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
"frontend.html": "418f6ef8354ce71f1b9594ee2068ebef",
"mdi.html": "65413cdf82f822bd6480e577852f0292",
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f",
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "153aad076f98bbd626466bac50986874",
"panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
"panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
"panels/ha-panel-history.html": "bfd5f929d5aa9cefdd799ec37624efa1",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2",
"panels/ha-panel-map.html": "e10704a3469e44d1714eac9ed8e4b6a0",
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit de1b20b70a16aeb7c48a1b4867c97864c88adb1c
Subproject commit f4c59e1eff3223262c198a29cf70c62572de019b

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -85,7 +85,7 @@ def setup(hass, config):
try:
influx = InfluxDBClient(**kwargs)
influx.query("SHOW DIAGNOSTICS;")
influx.query("SHOW DIAGNOSTICS;", database=conf[CONF_DB_NAME])
except exceptions.InfluxDBClientError as exc:
_LOGGER.error("Database host is not accessible due to '%s', please "
"check your entries in the configuration file and that "

View file

@ -255,7 +255,7 @@ def async_setup(hass, config):
if not light.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
light.async_update_ha_state(True))
if hasattr(light, 'async_update'):
update_tasks.append(update_coro)

View file

@ -6,6 +6,9 @@ https://home-assistant.io/components/light.lifx/
"""
import colorsys
import logging
import asyncio
from functools import partial
from datetime import timedelta
import voluptuous as vol
@ -13,117 +16,91 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
from homeassistant.helpers.event import track_time_change
from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
from homeassistant import util
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['liffylights==0.9.4']
REQUIREMENTS = ['aiolifx==0.4.2']
BYTE_MAX = 255
UDP_BROADCAST_PORT = 56700
# Delay (in ms) expected for changes to take effect in the physical bulb
BULB_LATENCY = 500
CONF_BROADCAST = 'broadcast'
CONF_SERVER = 'server'
BYTE_MAX = 255
SHORT_MAX = 65535
TEMP_MAX = 9000
TEMP_MAX_HASS = 500
TEMP_MIN = 2500
TEMP_MIN_HASS = 154
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default=None): cv.string,
vol.Optional(CONF_BROADCAST, default=None): cv.string,
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the LIFX platform."""
import aiolifx
server_addr = config.get(CONF_SERVER)
broadcast_addr = config.get(CONF_BROADCAST)
lifx_library = LIFX(add_devices, server_addr, broadcast_addr)
lifx_manager = LIFXManager(hass, async_add_devices)
# Register our poll service
track_time_change(hass, lifx_library.poll, second=[10, 40])
coro = hass.loop.create_datagram_endpoint(
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager),
local_addr=(server_addr, UDP_BROADCAST_PORT))
lifx_library.probe()
hass.async_add_job(coro)
return True
class LIFX(object):
"""Representation of a LIFX light."""
class LIFXManager(object):
"""Representation of all known LIFX entities."""
def __init__(self, add_devices_callback, server_addr=None,
broadcast_addr=None):
def __init__(self, hass, async_add_devices):
"""Initialize the light."""
import liffylights
self.entities = {}
self.hass = hass
self.async_add_devices = async_add_devices
self._devices = []
self._add_devices_callback = add_devices_callback
self._liffylights = liffylights.LiffyLights(
self.on_device, self.on_power, self.on_color, server_addr,
broadcast_addr)
def find_bulb(self, ipaddr):
"""Search for bulbs."""
bulb = None
for device in self._devices:
if device.ipaddr == ipaddr:
bulb = device
break
return bulb
def on_device(self, ipaddr, name, power, hue, sat, bri, kel):
"""Initialize the light."""
bulb = self.find_bulb(ipaddr)
if bulb is None:
_LOGGER.debug("new bulb %s %s %d %d %d %d %d",
ipaddr, name, power, hue, sat, bri, kel)
bulb = LIFXLight(
self._liffylights, ipaddr, name, power, hue, sat, bri, kel)
self._devices.append(bulb)
self._add_devices_callback([bulb])
@callback
def register(self, device):
"""Callback for newly detected bulb."""
if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr]
_LOGGER.debug("%s register AGAIN", entity.ipaddr)
entity.available = True
self.hass.async_add_job(entity.async_update_ha_state())
else:
_LOGGER.debug("update bulb %s %s %d %d %d %d %d",
ipaddr, name, power, hue, sat, bri, kel)
bulb.set_power(power)
bulb.set_color(hue, sat, bri, kel)
bulb.schedule_update_ha_state()
_LOGGER.debug("%s register NEW", device.ip_addr)
device.get_color(self.ready)
def on_color(self, ipaddr, hue, sat, bri, kel):
"""Initialize the light."""
bulb = self.find_bulb(ipaddr)
@callback
def ready(self, device, msg):
"""Callback that adds the device once all data is retrieved."""
entity = LIFXLight(device)
_LOGGER.debug("%s register READY", entity.ipaddr)
self.entities[device.mac_addr] = entity
self.hass.async_add_job(self.async_add_devices, [entity])
if bulb is not None:
bulb.set_color(hue, sat, bri, kel)
bulb.schedule_update_ha_state()
def on_power(self, ipaddr, power):
"""Initialize the light."""
bulb = self.find_bulb(ipaddr)
if bulb is not None:
bulb.set_power(power)
bulb.schedule_update_ha_state()
# pylint: disable=unused-argument
def poll(self, now):
"""Polling for the light."""
self.probe()
def probe(self, address=None):
"""Probe the light."""
self._liffylights.probe(address)
@callback
def unregister(self, device):
"""Callback for disappearing bulb."""
if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr]
_LOGGER.debug("%s unregister", entity.ipaddr)
entity.available = False
entity.updated_event.set()
self.hass.async_add_job(entity.async_update_ha_state())
def convert_rgb_to_hsv(rgb):
@ -140,31 +117,35 @@ def convert_rgb_to_hsv(rgb):
class LIFXLight(Light):
"""Representation of a LIFX light."""
def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness,
kelvin):
def __init__(self, device):
"""Initialize the light."""
_LOGGER.debug("LIFXLight: %s %s", ipaddr, name)
self._liffylights = liffy
self._ip = ipaddr
self.set_name(name)
self.set_power(power)
self.set_color(hue, saturation, brightness, kelvin)
self.device = device
self.updated_event = asyncio.Event()
self.blocker = None
self.postponed_update = None
self._available = True
self.set_power(device.power_level)
self.set_color(*device.color)
@property
def should_poll(self):
"""No polling needed for LIFX light."""
return False
def available(self):
"""Return the availability of the device."""
return self._available
@available.setter
def available(self, value):
"""Set the availability of the device."""
self._available = value
@property
def name(self):
"""Return the name of the device."""
return self._name
return self.device.label
@property
def ipaddr(self):
"""Return the IP address of the device."""
return self._ip
return self.device.ip_addr[0]
@property
def rgb_color(self):
@ -199,16 +180,47 @@ class LIFXLight(Light):
"""Flag supported features."""
return SUPPORT_LIFX
def turn_on(self, **kwargs):
@callback
def update_after_transition(self, now):
"""Request new status after completion of the last transition."""
self.postponed_update = None
self.hass.async_add_job(self.async_update_ha_state(force_refresh=True))
@callback
def unblock_updates(self, now):
"""Allow async_update after the new state has settled on the bulb."""
self.blocker = None
self.hass.async_add_job(self.async_update_ha_state(force_refresh=True))
def update_later(self, when):
"""Block immediate update requests and schedule one for later."""
if self.blocker:
self.blocker()
self.blocker = async_track_point_in_utc_time(
self.hass, self.unblock_updates,
util.dt.utcnow() + timedelta(milliseconds=BULB_LATENCY))
if self.postponed_update:
self.postponed_update()
if when > BULB_LATENCY:
self.postponed_update = async_track_point_in_utc_time(
self.hass, self.update_after_transition,
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the device on."""
if ATTR_TRANSITION in kwargs:
fade = int(kwargs[ATTR_TRANSITION] * 1000)
else:
fade = 0
changed_color = False
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
changed_color = True
else:
hue = self._hue
saturation = self._sat
@ -216,40 +228,64 @@ class LIFXLight(Light):
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
changed_color = True
else:
brightness = self._bri
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
changed_color = True
else:
kelvin = self._kel
hsbk = [hue, saturation, brightness, kelvin]
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
self._ip, self._power,
hue, saturation, brightness, kelvin, fade)
self.ipaddr, self._power, fade, *hsbk)
if self._power == 0:
self._liffylights.set_color(self._ip, hue, saturation,
brightness, kelvin, 0)
self._liffylights.set_power(self._ip, 65535, fade)
if changed_color:
self.device.set_color(hsbk, None, 0)
self.device.set_power(True, None, fade)
else:
self._liffylights.set_color(self._ip, hue, saturation,
brightness, kelvin, fade)
self.device.set_power(True, None, 0) # racing for power status
if changed_color:
self.device.set_color(hsbk, None, fade)
def turn_off(self, **kwargs):
self.update_later(0)
if fade < BULB_LATENCY:
self.set_power(1)
self.set_color(*hsbk)
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the device off."""
if ATTR_TRANSITION in kwargs:
fade = int(kwargs[ATTR_TRANSITION] * 1000)
else:
fade = 0
_LOGGER.debug("turn_off: %s %d", self._ip, fade)
self._liffylights.set_power(self._ip, 0, fade)
self.device.set_power(False, None, fade)
def set_name(self, name):
"""Set name of the light."""
self._name = name
self.update_later(fade)
if fade < BULB_LATENCY:
self.set_power(0)
@callback
def got_color(self, device, msg):
"""Callback that gets current power/color status."""
self.set_power(device.power_level)
self.set_color(*device.color)
self.updated_event.set()
@asyncio.coroutine
def async_update(self):
"""Update bulb status (if it is available)."""
_LOGGER.debug("%s async_update", self.ipaddr)
if self.available and self.blocker is None:
self.updated_event.clear()
self.device.get_color(self.got_color)
yield from self.updated_event.wait()
def set_power(self, power):
"""Set power state value."""

View file

@ -0,0 +1,64 @@
"""Support for Lutron Caseta lights."""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.light.lutron import (
to_hass_level, to_lutron_level)
from homeassistant.components.lutron_caseta import (
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Lutron Caseta lights."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
light_devices = bridge.get_devices_by_type("WallDimmer")
for light_device in light_devices:
dev = LutronCasetaLight(light_device, bridge)
devs.append(dev)
add_devices(devs, True)
class LutronCasetaLight(LutronCasetaDevice, Light):
"""Representation of a Lutron Light, including dimmable."""
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
@property
def brightness(self):
"""Return the brightness of the light."""
return to_hass_level(self._state["current_state"])
def turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self._device_type == "WallDimmer":
brightness = kwargs[ATTR_BRIGHTNESS]
else:
brightness = 255
self._smartbridge.set_value(self._device_id,
to_lutron_level(brightness))
def turn_off(self, **kwargs):
"""Turn the light off."""
self._smartbridge.set_value(self._device_id, 0)
@property
def is_on(self):
"""Return true if device is on."""
return self._state["current_state"] > 0
def update(self):
"""Called when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)

View file

@ -14,8 +14,8 @@ from homeassistant.components.rflink import (
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN,
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_TYPE
from homeassistant.const import (
CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
DEPENDENCIES = ['rflink']
_LOGGER = logging.getLogger(__name__)
@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__)
TYPE_DIMMABLE = 'dimmable'
TYPE_SWITCHABLE = 'switchable'
TYPE_HYBRID = 'hybrid'
TYPE_TOGGLE = 'toggle'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): DOMAIN,
@ -33,7 +34,8 @@ PLATFORM_SCHEMA = vol.Schema({
cv.string: {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE):
vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE, TYPE_HYBRID),
vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE,
TYPE_HYBRID, TYPE_TOGGLE),
vol.Optional(CONF_ALIASSES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
@ -71,6 +73,9 @@ def entity_class_for_type(entity_type):
# sends 'dim' and 'on' command to support both dimmers and on/off
# switches. Not compatible with signal repetition.
TYPE_HYBRID: HybridRflinkLight,
# sends only 'on' commands for switches which turn on and off
# using the same 'on' command for both.
TYPE_TOGGLE: ToggleRflinkLight,
}
return entity_device_mapping.get(entity_type, RflinkLight)
@ -213,3 +218,38 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light):
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
class ToggleRflinkLight(SwitchableRflinkDevice, Light):
"""Rflink light device which sends out only 'on' commands.
Some switches like for example Livolo light switches use the
same 'on' command to switch on and switch off the lights.
If the light is on and 'on' gets sent, the light will turn off
and if the light is off and 'on' gets sent, the light will turn on.
"""
@property
def entity_id(self):
"""Return entity id."""
return "light.{}".format(self.name)
def _handle_event(self, event):
"""Adjust state if Rflink picks up a remote command for this device."""
self.cancel_queued_send_commands()
command = event['command']
if command == 'on':
# if the state is unknown or false, it gets set as true
# if the state is true, it gets set as false
self._state = self._state in [STATE_UNKNOWN, False]
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the device on."""
yield from self._async_handle_command('toggle')
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the device off."""
yield from self._async_handle_command('toggle')

View file

@ -18,6 +18,8 @@ DEPENDENCIES = ['wink']
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
RGB_MODES = ['hsb', 'rgb']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink lights."""
@ -54,6 +56,8 @@ class WinkLight(WinkDevice, Light):
"""Current bulb color in RGB."""
if not self.wink.supports_hue_saturation():
return None
elif self.wink.color_model() not in RGB_MODES:
return False
else:
hue = self.wink.color_hue()
saturation = self.wink.color_saturation()

View file

@ -2,7 +2,7 @@
Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.yeelightsunflower
https://home-assistant.io/components/light.yeelightsunflower/
"""
import logging
import voluptuous as vol

View file

@ -49,9 +49,9 @@ SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
| SUPPORT_COLOR_TEMP)
def get_device(node, value, node_config, **kwargs):
def get_device(node, values, node_config, **kwargs):
"""Create zwave entity device."""
name = '{}.{}'.format(DOMAIN, zwave.object_id(value))
name = '{}.{}'.format(DOMAIN, zwave.object_id(values.primary))
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
@ -59,9 +59,9 @@ def get_device(node, value, node_config, **kwargs):
refresh, delay)
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
return ZwaveColorLight(value, refresh, delay)
return ZwaveColorLight(values, refresh, delay)
else:
return ZwaveDimmer(value, refresh, delay)
return ZwaveDimmer(values, refresh, delay)
def brightness_state(value):
@ -75,9 +75,9 @@ def brightness_state(value):
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Representation of a Z-Wave dimmer."""
def __init__(self, value, refresh, delay):
def __init__(self, values, refresh, delay):
"""Initialize the light."""
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self._brightness = None
self._state = None
self._delay = delay
@ -86,10 +86,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
# Enable appropriate workaround flags for our device
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if (self.node.manufacturer_id.strip() and
self.node.product_id.strip()):
specific_sensor_key = (int(self.node.manufacturer_id, 16),
int(self.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
@ -105,7 +105,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
def update_properties(self):
"""Update internal properties based on zwave values."""
# Brightness
self._brightness, self._state = brightness_state(self._value)
self._brightness, self._state = brightness_state(self.values.primary)
def value_changed(self):
"""Called when a value for this entity's node has changed."""
@ -116,7 +116,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
self.values.primary.refresh()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
@ -151,12 +151,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
else:
brightness = 255
if self._value.node.set_dimmer(self._value.value_id, brightness):
if self.node.set_dimmer(self.values.primary.value_id, brightness):
self._state = STATE_ON
def turn_off(self, **kwargs):
"""Turn the device off."""
if self._value.node.set_dimmer(self._value.value_id, 0):
if self.node.set_dimmer(self.values.primary.value_id, 0):
self._state = STATE_OFF
@ -170,73 +170,28 @@ def ct_to_rgb(temp):
class ZwaveColorLight(ZwaveDimmer):
"""Representation of a Z-Wave color changing light."""
def __init__(self, value, refresh, delay):
def __init__(self, values, refresh, delay):
"""Initialize the light."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
self._value_color = None
self._value_color_channels = None
self._color_channels = None
self._rgb = None
self._ct = None
super().__init__(value, refresh, delay)
# Create a listener so the color values can be linked to this entity
dispatcher.connect(
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
self._get_color_values()
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
return [val.value_id for val in [
self._value_color, self._value_color_channels] if val]
def _get_color_values(self):
"""Search for color values available on this node."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
_LOGGER.debug("Searching for zwave color values")
# Currently zwave nodes only exist with one color element per node.
if self._value_color is None:
for value_color in self._value.node.get_rgbbulbs().values():
self._value_color = value_color
if self._value_color_channels is None:
self._value_color_channels = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
genre=zwave.const.GENRE_SYSTEM, type=zwave.const.TYPE_INT)
if self._value_color and self._value_color_channels:
_LOGGER.debug("Zwave node color values found.")
dispatcher.disconnect(
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
self.update_properties()
def _value_added(self, value):
"""Called when a value has been added to the network."""
if self._value.node != value.node:
return
# Check for the missing color values
self._get_color_values()
super().__init__(values, refresh, delay)
def update_properties(self):
"""Update internal properties based on zwave values."""
super().update_properties()
if self._value_color is None:
if self.values.color is None:
return
if self._value_color_channels is None:
if self.values.color_channels is None:
return
# Color Channels
self._color_channels = self._value_color_channels.data
self._color_channels = self.values.color_channels.data
# Color Data String
data = self._value_color.data
data = self.values.color.data
# RGB is always present in the openzwave color data string.
self._rgb = [
@ -309,10 +264,10 @@ class ZwaveColorLight(ZwaveDimmer):
if self._zw098:
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
self._ct = TEMP_WARM_HASS
rgbw = b'#000000FF00'
rgbw = b'#000000ff00'
else:
self._ct = TEMP_COLD_HASS
rgbw = b'#00000000FF'
rgbw = b'#00000000ff'
elif ATTR_RGB_COLOR in kwargs:
self._rgb = kwargs[ATTR_RGB_COLOR]
@ -329,8 +284,8 @@ class ZwaveColorLight(ZwaveDimmer):
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'0000'
if rgbw and self._value_color:
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
if rgbw and self.values.color:
self.values.color.data = rgbw
super().turn_on(**kwargs)

View file

@ -96,7 +96,7 @@ def async_setup(hass, config):
if not entity.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
entity.async_update_ha_state(True))
if hasattr(entity, 'async_update'):
update_tasks.append(update_coro)

View file

@ -6,6 +6,7 @@ https://home-assistant.io/components/lock.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import asyncio
import logging
from os import path
@ -13,7 +14,6 @@ import voluptuous as vol
from homeassistant.components.lock import DOMAIN, LockDevice
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
@ -53,7 +53,7 @@ LOCK_ALARM_TYPE = {
'9': 'Deadbolt Jammed',
'18': 'Locked with Keypad by user ',
'19': 'Unlocked with Keypad by user ',
'21': 'Manually Locked by',
'21': 'Manually Locked by ',
'22': 'Manually Unlocked by Key or Inside thumb turn',
'24': 'Locked by RF',
'25': 'Unlocked by RF',
@ -120,8 +120,12 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({
})
def get_device(hass, node, value, **kwargs):
"""Create zwave entity device."""
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Generic Z-Wave platform setup."""
yield from zwave.async_setup_platform(
hass, config, async_add_devices, discovery_info)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
@ -140,6 +144,7 @@ def get_device(hass, node, value, **kwargs):
_LOGGER.error('Invalid code provided: (%s)'
' usercode must %s or less digits',
usercode, len(value.data))
break
value.data = str(usercode)
break
@ -175,32 +180,34 @@ def get_device(hass, node, value, **kwargs):
_LOGGER.info('Usercode at slot %s is cleared', value.index)
break
if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
hass.services.register(DOMAIN,
SERVICE_SET_USERCODE,
set_usercode,
descriptions.get(SERVICE_SET_USERCODE),
schema=SET_USERCODE_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_GET_USERCODE,
get_usercode,
descriptions.get(SERVICE_GET_USERCODE),
schema=GET_USERCODE_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_CLEAR_USERCODE,
clear_usercode,
descriptions.get(SERVICE_CLEAR_USERCODE),
schema=CLEAR_USERCODE_SCHEMA)
return ZwaveLock(value)
hass.services.async_register(DOMAIN,
SERVICE_SET_USERCODE,
set_usercode,
descriptions.get(SERVICE_SET_USERCODE),
schema=SET_USERCODE_SCHEMA)
hass.services.async_register(DOMAIN,
SERVICE_GET_USERCODE,
get_usercode,
descriptions.get(SERVICE_GET_USERCODE),
schema=GET_USERCODE_SCHEMA)
hass.services.async_register(DOMAIN,
SERVICE_CLEAR_USERCODE,
clear_usercode,
descriptions.get(SERVICE_CLEAR_USERCODE),
schema=CLEAR_USERCODE_SCHEMA)
def get_device(node, values, **kwargs):
"""Create zwave entity device."""
return ZwaveLock(values)
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
"""Representation of a Z-Wave Lock."""
def __init__(self, value):
def __init__(self, values):
"""Initialize the Z-Wave lock device."""
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self._state = None
self._notification = None
self._lock_status = None
@ -208,10 +215,10 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
# Enable appropriate workaround flags for our device
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if (self.node.manufacturer_id.strip() and
self.node.product_id.strip()):
specific_sensor_key = (int(self.node.manufacturer_id, 16),
int(self.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE:
self._v2btze = 1
@ -221,43 +228,41 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
def update_properties(self):
"""Callback on data changes for node values."""
self._state = self._value.data
self._state = self.values.primary.data
_LOGGER.debug('Lock state set from Bool value and'
' is %s', self._state)
notification_data = self.get_value(class_id=zwave.const
.COMMAND_CLASS_ALARM,
label=['Access Control'],
member='data')
if notification_data:
if self.values.access_control:
notification_data = self.values.access_control.data
self._notification = LOCK_NOTIFICATION.get(str(notification_data))
if self._v2btze:
advanced_config = self.get_value(class_id=zwave.const
.COMMAND_CLASS_CONFIGURATION,
index=12,
data=CONFIG_ADVANCED,
member='data')
if advanced_config:
self._state = LOCK_STATUS.get(str(notification_data))
_LOGGER.debug('Lock state set from Access Control '
'value and is %s, get=%s',
str(notification_data),
self.state)
alarm_type = self.get_value(class_id=zwave.const
.COMMAND_CLASS_ALARM,
label=['Alarm Type'], member='data')
if self._v2btze:
if self.values.v2btze_advanced and \
self.values.v2btze_advanced.data == CONFIG_ADVANCED:
self._state = LOCK_STATUS.get(str(notification_data))
_LOGGER.debug('Lock state set from Access Control '
'value and is %s, get=%s',
str(notification_data),
self.state)
if not self.values.alarm_type:
return
alarm_type = self.values.alarm_type.data
_LOGGER.debug('Lock alarm_type is %s', str(alarm_type))
alarm_level = self.get_value(class_id=zwave.const
.COMMAND_CLASS_ALARM,
label=['Alarm Level'], member='data')
if self.values.alarm_level:
alarm_level = self.values.alarm_level.data
else:
alarm_level = None
_LOGGER.debug('Lock alarm_level is %s', str(alarm_level))
if not alarm_type:
return
if alarm_type is 21:
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(str(alarm_type)),
MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level)))
if alarm_type in ALARM_TYPE_STD:
return
if str(alarm_type) in ALARM_TYPE_STD:
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level))
return
@ -277,11 +282,11 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
def lock(self, **kwargs):
"""Lock the device."""
self._value.data = True
self.values.primary.data = True
def unlock(self, **kwargs):
"""Unlock the device."""
self._value.data = False
self.values.primary.data = False
@property
def device_state_attributes(self):
@ -292,8 +297,3 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
if self._lock_status:
data[ATTR_LOCK_STATUS] = self._lock_status
return data
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
return None

View file

@ -0,0 +1,100 @@
"""
Component for interacting with a Lutron Caseta system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/lutron_caseta/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_HOST,
CONF_USERNAME,
CONF_PASSWORD)
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['https://github.com/gurumitts/'
'pylutron-caseta/archive/v0.2.4.zip#'
'pylutron-caseta==v0.2.4']
_LOGGER = logging.getLogger(__name__)
LUTRON_CASETA_SMARTBRIDGE = 'lutron_smartbridge'
DOMAIN = 'lutron_caseta'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, base_config):
"""Setup the Lutron component."""
from pylutron_caseta.smartbridge import Smartbridge
config = base_config.get(DOMAIN)
hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge(
hostname=config[CONF_HOST],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD]
)
if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected():
_LOGGER.error("Unable to connect to Lutron smartbridge at %s",
config[CONF_HOST])
return False
_LOGGER.info("Connected to Lutron smartbridge at %s",
config[CONF_HOST])
for component in ('light', 'switch'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
class LutronCasetaDevice(Entity):
"""Common base class for all Lutron Caseta devices."""
def __init__(self, device, bridge):
"""Set up the base class.
[:param]device the device metadata
[:param]bridge the smartbridge object
"""
self._device_id = device["device_id"]
self._device_type = device["type"]
self._device_name = device["name"]
self._state = None
self._smartbridge = bridge
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
self._smartbridge.add_subscriber(self._device_id,
self._update_callback)
def _update_callback(self):
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of the device."""
return self._device_name
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {'Lutron Integration ID': self._device_id}
return attr
@property
def should_poll(self):
"""No polling needed."""
return False

View file

@ -4,32 +4,37 @@ Support to interface with the Emby API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.emby/
"""
import asyncio
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice,
SUPPORT_PLAY, PLATFORM_SCHEMA)
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_HOST, CONF_API_KEY, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME,
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
from homeassistant.helpers.event import (track_utc_time_change)
from homeassistant.util import Throttle
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==0.2']
REQUIREMENTS = ['pyemby==1.1']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
_LOGGER = logging.getLogger(__name__)
CONF_AUTO_HIDE = 'auto_hide'
MEDIA_TYPE_TRAILER = 'trailer'
MEDIA_TYPE_GENERIC_VIDEO = 'video'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8096
DEFAULT_SSL_PORT = 8920
DEFAULT_SSL = False
DEFAULT_AUTO_HIDE = False
_LOGGER = logging.getLogger(__name__)
@ -37,219 +42,210 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default='localhost'): cv.string,
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PORT, default=None): cv.port,
vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean,
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Emby platform."""
from pyemby.emby import EmbyRemote
from pyemby import EmbyServer
_host = config.get(CONF_HOST)
_key = config.get(CONF_API_KEY)
_port = config.get(CONF_PORT)
host = config.get(CONF_HOST)
key = config.get(CONF_API_KEY)
port = config.get(CONF_PORT)
ssl = config.get(CONF_SSL)
auto_hide = config.get(CONF_AUTO_HIDE)
if config.get(CONF_SSL):
_protocol = "https"
else:
_protocol = "http"
if port is None:
port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT
_url = '{}://{}:{}'.format(_protocol, _host, _port)
_LOGGER.debug('Setting up Emby server at: %s:%s', host, port)
_LOGGER.debug('Setting up Emby server at: %s', _url)
emby = EmbyServer(host, key, port, ssl, hass.loop)
embyserver = EmbyRemote(_key, _url)
active_emby_devices = {}
inactive_emby_devices = {}
emby_clients = {}
emby_sessions = {}
track_utc_time_change(hass, lambda now: update_devices(), second=30)
@callback
def device_update_callback(data):
"""Callback for when devices are added to emby."""
new_devices = []
active_devices = []
for dev_id in emby.devices:
active_devices.append(dev_id)
if dev_id not in active_emby_devices and \
dev_id not in inactive_emby_devices:
new = EmbyDevice(emby, dev_id)
active_emby_devices[dev_id] = new
new_devices.append(new)
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_devices():
"""Update the devices objects."""
devices = embyserver.get_sessions()
if devices is None:
_LOGGER.error('Error listing Emby devices.')
return
elif dev_id in inactive_emby_devices:
if emby.devices[dev_id].state != 'Off':
add = inactive_emby_devices.pop(dev_id)
active_emby_devices[dev_id] = add
_LOGGER.debug("Showing %s, item: %s", dev_id, add)
add.set_available(True)
add.set_hidden(False)
new_emby_clients = []
for device in devices:
if device['DeviceId'] == embyserver.unique_id:
break
if new_devices:
_LOGGER.debug("Adding new devices to HASS: %s", new_devices)
async_add_devices(new_devices, update_before_add=True)
if device['DeviceId'] not in emby_clients:
_LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.',
device['DeviceId'])
new_client = EmbyClient(embyserver, device, emby_sessions,
update_devices, update_sessions)
emby_clients[device['DeviceId']] = new_client
new_emby_clients.append(new_client)
else:
emby_clients[device['DeviceId']].set_device(device)
@callback
def device_removal_callback(data):
"""Callback for when devices are removed from emby."""
if data in active_emby_devices:
rem = active_emby_devices.pop(data)
inactive_emby_devices[data] = rem
_LOGGER.debug("Inactive %s, item: %s", data, rem)
rem.set_available(False)
if auto_hide:
rem.set_hidden(True)
if new_emby_clients:
add_devices_callback(new_emby_clients)
@callback
def start_emby(event):
"""Start emby connection."""
emby.start()
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_sessions():
"""Update the sessions objects."""
sessions = embyserver.get_sessions()
if sessions is None:
_LOGGER.error('Error listing Emby sessions')
return
@asyncio.coroutine
def stop_emby(event):
"""Stop emby connection."""
yield from emby.stop()
emby_sessions.clear()
for session in sessions:
emby_sessions[session['DeviceId']] = session
emby.add_new_devices_callback(device_update_callback)
emby.add_stale_devices_callback(device_removal_callback)
update_devices()
update_sessions()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emby)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby)
class EmbyClient(MediaPlayerDevice):
"""Representation of a Emby device."""
class EmbyDevice(MediaPlayerDevice):
"""Representation of an Emby device."""
# pylint: disable=too-many-arguments, too-many-public-methods,
def __init__(self, client, device, emby_sessions, update_devices,
update_sessions):
def __init__(self, emby, device_id):
"""Initialize the Emby device."""
self.emby_sessions = emby_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
self.client = client
self.set_device(device)
_LOGGER.debug('New Emby Device initialized with ID: %s', device_id)
self.emby = emby
self.device_id = device_id
self.device = self.emby.devices[self.device_id]
self._hidden = False
self._available = True
self.media_status_last_position = None
self.media_status_received = None
def set_device(self, device):
"""Set the device property."""
self.device = device
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callback."""
self.emby.add_update_callback(self.async_update_callback,
self.device_id)
@callback
def async_update_callback(self, msg):
"""Callback for device updates."""
# Check if we should update progress
if self.device.media_position:
if self.device.media_position != self.media_status_last_position:
self.media_status_last_position = self.device.media_position
self.media_status_received = dt_util.utcnow()
elif not self.device.is_nowplaying:
# No position, but we have an old value and are still playing
self.media_status_last_position = None
self.media_status_received = None
self.hass.async_add_job(self.async_update_ha_state())
@property
def hidden(self):
"""Return True if entity should be hidden from UI."""
return self._hidden
def set_hidden(self, value):
"""Set hidden property."""
self._hidden = value
@property
def available(self):
"""Return True if entity is available."""
return self._available
def set_available(self, value):
"""Set available property."""
self._available = value
@property
def unique_id(self):
"""Return the id of this emby client."""
return '{}.{}'.format(
self.__class__, self.device['DeviceId'])
return '{}.{}'.format(self.__class__, self.device_id)
@property
def supports_remote_control(self):
"""Return control ability."""
return self.device['SupportsRemoteControl']
return self.device.supports_remote_control
@property
def name(self):
"""Return the name of the device."""
return 'emby_{}'.format(self.device['DeviceName']) or \
DEVICE_DEFAULT_NAME
return 'Emby - {} - {}'.format(self.device.client, self.device.name) \
or DEVICE_DEFAULT_NAME
@property
def session(self):
"""Return the session, if any."""
if self.device['DeviceId'] not in self.emby_sessions:
return None
return self.emby_sessions[self.device['DeviceId']]
@property
def now_playing_item(self):
"""Return the currently playing item, if any."""
session = self.session
if session is not None and 'NowPlayingItem' in session:
return session['NowPlayingItem']
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property
def state(self):
"""Return the state of the device."""
session = self.session
if session:
if 'NowPlayingItem' in session:
if session['PlayState']['IsPaused']:
return STATE_PAUSED
else:
return STATE_PLAYING
else:
return STATE_IDLE
# This is nasty. Need to find a way to determine alive
else:
state = self.device.state
if state == 'Paused':
return STATE_PAUSED
elif state == 'Playing':
return STATE_PLAYING
elif state == 'Idle':
return STATE_IDLE
elif state == 'Off':
return STATE_OFF
return STATE_UNKNOWN
def update(self):
"""Get the latest details."""
self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True)
# Check if we should update progress
try:
position = self.session['PlayState']['PositionTicks']
except (KeyError, TypeError):
self.media_status_last_position = None
self.media_status_received = None
else:
position = int(position) / 10000000
if position != self.media_status_last_position:
self.media_status_last_position = position
self.media_status_received = dt_util.utcnow()
def play_percent(self):
"""Return current media percent complete."""
if self.now_playing_item['RunTimeTicks'] and \
self.session['PlayState']['PositionTicks']:
try:
return int(self.session['PlayState']['PositionTicks']) / \
int(self.now_playing_item['RunTimeTicks']) * 100
except KeyError:
return 0
else:
return 0
@property
def app_name(self):
"""Return current user as app_name."""
# Ideally the media_player object would have a user property.
try:
return self.device['UserName']
except KeyError:
return None
return self.device.username
@property
def media_content_id(self):
"""Content ID of current playing media."""
if self.now_playing_item is not None:
try:
return self.now_playing_item['Id']
except KeyError:
return None
return self.device.media_id
@property
def media_content_type(self):
"""Content type of current playing media."""
if self.now_playing_item is None:
return None
try:
media_type = self.now_playing_item['Type']
if media_type == 'Episode':
return MEDIA_TYPE_TVSHOW
elif media_type == 'Movie':
return MEDIA_TYPE_VIDEO
elif media_type == 'Trailer':
return MEDIA_TYPE_TRAILER
return None
except KeyError:
return None
media_type = self.device.media_type
if media_type == 'Episode':
return MEDIA_TYPE_TVSHOW
elif media_type == 'Movie':
return MEDIA_TYPE_VIDEO
elif media_type == 'Trailer':
return MEDIA_TYPE_TRAILER
elif media_type == 'Music':
return MEDIA_TYPE_MUSIC
elif media_type == 'Video':
return MEDIA_TYPE_GENERIC_VIDEO
elif media_type == 'Audio':
return MEDIA_TYPE_MUSIC
return None
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self.now_playing_item and self.media_content_type:
try:
return int(self.now_playing_item['RunTimeTicks']) / 10000000
except KeyError:
return None
return self.device.media_runtime
@property
def media_position(self):
@ -268,45 +264,42 @@ class EmbyClient(MediaPlayerDevice):
@property
def media_image_url(self):
"""Image url of current playing media."""
if self.now_playing_item is not None:
try:
return self.client.get_image(
self.now_playing_item['ThumbItemId'], 'Thumb', 0)
except KeyError:
try:
return self.client.get_image(
self.now_playing_item[
'PrimaryImageItemId'], 'Primary', 0)
except KeyError:
return None
return self.device.media_image_url
@property
def media_title(self):
"""Title of current playing media."""
# find a string we can use as a title
if self.now_playing_item is not None:
return self.now_playing_item['Name']
return self.device.media_title
@property
def media_season(self):
"""Season of curent playing media (TV Show only)."""
if self.now_playing_item is not None and \
'ParentIndexNumber' in self.now_playing_item:
return self.now_playing_item['ParentIndexNumber']
return self.device.media_season
@property
def media_series_title(self):
"""The title of the series of current playing media (TV Show only)."""
if self.now_playing_item is not None and \
'SeriesName' in self.now_playing_item:
return self.now_playing_item['SeriesName']
return self.device.media_series_title
@property
def media_episode(self):
"""Episode of current playing media (TV Show only)."""
if self.now_playing_item is not None and \
'IndexNumber' in self.now_playing_item:
return self.now_playing_item['IndexNumber']
return self.device.media_episode
@property
def media_album_name(self):
"""Album name of current playing media (Music track only)."""
return self.device.media_album_name
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
return self.device.media_artist
@property
def media_album_artist(self):
"""Album artist of current playing media (Music track only)."""
return self.device.media_album_artist
@property
def supported_features(self):
@ -316,20 +309,44 @@ class EmbyClient(MediaPlayerDevice):
else:
return None
def media_play(self):
"""Send play command."""
if self.supports_remote_control:
self.client.play(self.session)
def async_media_play(self):
"""Play media.
def media_pause(self):
"""Send pause command."""
if self.supports_remote_control:
self.client.pause(self.session)
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_play()
def media_next_track(self):
"""Send next track command."""
self.client.next_track(self.session)
def async_media_pause(self):
"""Pause the media player.
def media_previous_track(self):
"""Send previous track command."""
self.client.previous_track(self.session)
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_pause()
def async_media_stop(self):
"""Stop the media player.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_stop()
def async_media_next_track(self):
"""Send next track command.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_next()
def async_media_previous_track(self):
"""Send next track command.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_previous()
def async_media_seek(self, position):
"""Send seek command.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_seek(position)

View file

@ -16,16 +16,18 @@ from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
PLATFORM_SCHEMA)
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
MEDIA_TYPE_PLAYLIST)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_PASSWORD,
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.2']
REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.3']
_LOGGER = logging.getLogger(__name__)
@ -37,11 +39,26 @@ DEFAULT_NAME = 'Kodi'
DEFAULT_PORT = 8080
DEFAULT_TCP_PORT = 9090
DEFAULT_TIMEOUT = 5
DEFAULT_SSL = False
DEFAULT_PROXY_SSL = False
DEFAULT_ENABLE_WEBSOCKET = True
TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown']
# https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h
MEDIA_TYPES = {
"music": MEDIA_TYPE_MUSIC,
"artist": MEDIA_TYPE_MUSIC,
"album": MEDIA_TYPE_MUSIC,
"song": MEDIA_TYPE_MUSIC,
"video": MEDIA_TYPE_VIDEO,
"set": MEDIA_TYPE_PLAYLIST,
"musicvideo": MEDIA_TYPE_VIDEO,
"movie": MEDIA_TYPE_VIDEO,
"tvshow": MEDIA_TYPE_TVSHOW,
"season": MEDIA_TYPE_TVSHOW,
"episode": MEDIA_TYPE_TVSHOW,
}
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP
@ -51,7 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION),
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
@ -66,7 +83,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
encryption = config.get(CONF_SSL)
encryption = get_deprecated(config, CONF_PROXY_SSL, CONF_SSL)
websocket = config.get(CONF_ENABLE_WEBSOCKET)
if host.startswith('http://') or host.startswith('https://'):
@ -169,7 +186,6 @@ class KodiDevice(MediaPlayerDevice):
self._properties = {}
self._item = {}
self._app_properties = {}
self._ws_connected = False
@callback
def async_on_speed_event(self, sender, data):
@ -244,29 +260,26 @@ class KodiDevice(MediaPlayerDevice):
"""Connect to Kodi via websocket protocol."""
import jsonrpc_base
try:
yield from self._ws_server.ws_connect()
ws_loop_future = yield from self._ws_server.ws_connect()
except jsonrpc_base.jsonrpc.TransportError:
_LOGGER.info("Unable to connect to Kodi via websocket")
_LOGGER.debug(
"Unable to connect to Kodi via websocket", exc_info=True)
# Websocket connection is not required. Just return.
return
self.hass.loop.create_task(self.async_ws_loop())
self._ws_connected = True
@asyncio.coroutine
def async_ws_loop(self):
"""Run the websocket asyncio message loop."""
import jsonrpc_base
try:
yield from self._ws_server.ws_loop()
except jsonrpc_base.jsonrpc.TransportError:
# Kodi abruptly ends ws connection when exiting. We only need to
# know that it was closed.
pass
finally:
yield from self._ws_server.close()
self._ws_connected = False
@asyncio.coroutine
def ws_loop_wrapper():
"""Catch exceptions from the websocket loop task."""
try:
yield from ws_loop_future
except jsonrpc_base.TransportError:
# Kodi abruptly ends ws connection when exiting. We will try
# to reconnect on the next poll.
pass
# Create a task instead of adding a tracking job, since this task will
# run until the websocket connection is closed.
self.hass.loop.create_task(ws_loop_wrapper())
@asyncio.coroutine
def async_update(self):
@ -279,8 +292,8 @@ class KodiDevice(MediaPlayerDevice):
self._app_properties = {}
return
if self._enable_websocket and not self._ws_connected:
self.hass.loop.create_task(self.async_ws_connect())
if self._enable_websocket and not self._ws_server.connected:
self.hass.async_add_job(self.async_ws_connect())
self._app_properties = \
yield from self.server.Application.GetProperties(
@ -299,7 +312,8 @@ class KodiDevice(MediaPlayerDevice):
self._item = (yield from self.server.Player.GetItem(
player_id,
['title', 'file', 'uniqueid', 'thumbnail', 'artist']
['title', 'file', 'uniqueid', 'thumbnail', 'artist',
'albumartist', 'showtitle', 'album', 'season', 'episode']
))['item']
else:
self._properties = {}
@ -309,7 +323,7 @@ class KodiDevice(MediaPlayerDevice):
@property
def server(self):
"""Active server for json-rpc requests."""
if self._ws_connected:
if self._enable_websocket and self._ws_server.connected:
return self._ws_server
else:
return self._http_server
@ -322,7 +336,7 @@ class KodiDevice(MediaPlayerDevice):
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return not self._ws_connected
return not (self._enable_websocket and self._ws_server.connected)
@property
def volume_level(self):
@ -343,8 +357,7 @@ class KodiDevice(MediaPlayerDevice):
@property
def media_content_type(self):
"""Content type of current playing media."""
if self._players is not None and len(self._players) > 0:
return self._players[0]['type']
return MEDIA_TYPES.get(self._item.get('type'))
@property
def media_duration(self):
@ -382,6 +395,44 @@ class KodiDevice(MediaPlayerDevice):
return self._item.get(
'title', self._item.get('label', self._item.get('file')))
@property
def media_series_title(self):
"""Title of series of current playing media, TV show only."""
return self._item.get('showtitle')
@property
def media_season(self):
"""Season of current playing media, TV show only."""
return self._item.get('season')
@property
def media_episode(self):
"""Episode of current playing media, TV show only."""
return self._item.get('episode')
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self._item.get('album')
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
artists = self._item.get('artist', [])
if len(artists) > 0:
return artists[0]
else:
return None
@property
def media_album_artist(self):
"""Album artist of current playing media, music track only."""
artists = self._item.get('albumartist', [])
if len(artists) > 0:
return artists[0]
else:
return None
@property
def supported_features(self):
"""Flag media player features that are supported."""

View file

@ -17,7 +17,7 @@ from homeassistant.components.media_player import (
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
CONF_HOST)
CONF_HOST, CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
@ -25,9 +25,7 @@ REQUIREMENTS = ['python-mpd2==0.5.5']
_LOGGER = logging.getLogger(__name__)
CONF_LOCATION = 'location'
DEFAULT_LOCATION = 'MPD'
DEFAULT_NAME = 'MPD'
DEFAULT_PORT = 6600
PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120)
@ -38,7 +36,7 @@ SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
@ -49,9 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MPD platform."""
daemon = config.get(CONF_HOST)
port = config.get(CONF_PORT)
location = config.get(CONF_LOCATION)
name = config.get(CONF_NAME)
password = config.get(CONF_PASSWORD)
import mpd
# pylint: disable=no-member
@ -75,20 +72,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
else:
raise
add_devices([MpdDevice(daemon, port, location, password)])
add_devices([MpdDevice(daemon, port, password, name)])
class MpdDevice(MediaPlayerDevice):
"""Representation of a MPD server."""
# pylint: disable=no-member
def __init__(self, server, port, location, password):
def __init__(self, server, port, password, name):
"""Initialize the MPD device."""
import mpd
self.server = server
self.port = port
self._name = location
self._name = name
self.password = password
self.status = None
self.currentsong = None

View file

@ -10,16 +10,35 @@ import os
from datetime import timedelta
from urllib.parse import urlparse
import homeassistant.util as util
import requests
import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
SUPPORT_PLAY, MediaPlayerDevice)
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO,
PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
MediaPlayerDevice,
)
from homeassistant.const import (
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN)
DEVICE_DEFAULT_NAME,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.loader import get_component
from homeassistant.helpers.event import (track_utc_time_change)
REQUIREMENTS = ['plexapi==2.0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -27,13 +46,24 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PLEX_CONFIG_FILE = 'plex.conf'
CONF_INCLUDE_NON_CLIENTS = 'include_non_clients'
CONF_USE_EPISODE_ART = 'use_episode_art'
CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids'
CONF_SHOW_ALL_CONTROLS = 'show_all_controls'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False):
cv.boolean,
vol.Optional(CONF_USE_EPISODE_ART, default=False):
cv.boolean,
vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False):
cv.boolean,
})
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY
def config_from_file(filename, config=None):
"""Small configuration file management function."""
@ -62,10 +92,12 @@ def config_from_file(filename, config=None):
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Plex platform."""
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
if len(config):
# get config from plex.conf
file_config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
if len(file_config):
# Setup a configured PlexServer
host, token = config.popitem()
host, token = file_config.popitem()
token = token['token']
# Via discovery
elif discovery_info is not None:
@ -79,22 +111,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
else:
return
setup_plexserver(host, token, hass, add_devices_callback)
setup_plexserver(host, token, hass, config, add_devices_callback)
def setup_plexserver(host, token, hass, add_devices_callback):
def setup_plexserver(host, token, hass, config, add_devices_callback):
"""Setup a plexserver based on host parameter."""
import plexapi.server
import plexapi.exceptions
try:
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
except (plexapi.exceptions.BadRequest,
plexapi.exceptions.Unauthorized,
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error:
_LOGGER.info(error)
# No token or wrong token
request_configuration(host, hass, add_devices_callback)
request_configuration(host, hass, config,
add_devices_callback)
return
# If we came here and configuring this host, mark as done
@ -106,8 +138,9 @@ def setup_plexserver(host, token, hass, add_devices_callback):
# Save config
if not config_from_file(
hass.config.path(PLEX_CONFIG_FILE),
{host: {'token': token}}):
hass.config.path(PLEX_CONFIG_FILE), {host: {
'token': token
}}):
_LOGGER.error('failed to save config file')
_LOGGER.info('Connected to: http://%s', host)
@ -117,6 +150,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
track_utc_time_change(hass, lambda now: update_devices(), second=30)
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
# pylint: disable=too-many-branches
def update_devices():
"""Update the devices objects."""
try:
@ -125,8 +159,8 @@ def setup_plexserver(host, token, hass, add_devices_callback):
_LOGGER.exception('Error listing plex devices')
return
except OSError:
_LOGGER.error(
'Could not connect to plex server at http://%s', host)
_LOGGER.error('Could not connect to plex server at http://%s',
host)
return
new_plex_clients = []
@ -136,12 +170,31 @@ def setup_plexserver(host, token, hass, add_devices_callback):
continue
if device.machineIdentifier not in plex_clients:
new_client = PlexClient(device, plex_sessions, update_devices,
new_client = PlexClient(config, device, None,
plex_sessions, update_devices,
update_sessions)
plex_clients[device.machineIdentifier] = new_client
new_plex_clients.append(new_client)
else:
plex_clients[device.machineIdentifier].set_device(device)
plex_clients[device.machineIdentifier].refresh(device, None)
# add devices with a session and no client (ex. PlexConnect Apple TV's)
if config.get(CONF_INCLUDE_NON_CLIENTS):
for machine_identifier, session in plex_sessions.items():
if (machine_identifier not in plex_clients
and machine_identifier is not None):
new_client = PlexClient(config, None, session,
plex_sessions, update_devices,
update_sessions)
plex_clients[machine_identifier] = new_client
new_plex_clients.append(new_client)
else:
plex_clients[machine_identifier].refresh(None, session)
for machine_identifier, client in plex_clients.items():
# force devices to idle that do not have a valid session
if client.session is None:
client.force_idle()
if new_plex_clients:
add_devices_callback(new_plex_clients)
@ -157,99 +210,333 @@ def setup_plexserver(host, token, hass, add_devices_callback):
plex_sessions.clear()
for session in sessions:
plex_sessions[session.player.machineIdentifier] = session
if (session.player is not None and
session.player.machineIdentifier is not None):
plex_sessions[session.player.machineIdentifier] = session
update_devices()
update_sessions()
update_devices()
def request_configuration(host, hass, add_devices_callback):
def request_configuration(host, hass, config, add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], 'Failed to register, please try again.')
configurator.notify_errors(_CONFIGURING[host],
'Failed to register, please try again.')
return
def plex_configuration_callback(data):
"""The actions to do when our configuration callback is called."""
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
setup_plexserver(host,
data.get('token'), hass, config,
add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, 'Plex Media Server', plex_configuration_callback,
hass,
'Plex Media Server',
plex_configuration_callback,
description=('Enter the X-Plex-Token'),
entity_picture='/static/images/logo_plex_mediaserver.png',
submit_caption='Confirm',
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
)
fields=[{
'id': 'token',
'name': 'X-Plex-Token',
'type': ''
}])
# pylint: disable=too-many-instance-attributes, too-many-public-methods
class PlexClient(MediaPlayerDevice):
"""Representation of a Plex device."""
# pylint: disable=attribute-defined-outside-init
def __init__(self, device, plex_sessions, update_devices, update_sessions):
# pylint: disable=too-many-arguments
def __init__(self, config, device, session, plex_sessions,
update_devices, update_sessions):
"""Initialize the Plex device."""
from plexapi.utils import NA
self._app_name = ''
self._device = None
self._device_protocol_capabilities = None
self._is_player_active = False
self._is_player_available = False
self._machine_identifier = None
self._make = ''
self._media_content_id = None
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
self._media_title = None
self._name = None
self._player_state = 'idle'
self._previous_volume_level = 1 # Used in fake muting
self._session = None
self._session_type = None
self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # since we can't retrieve remotely
self.na_type = NA
self.config = config
self.plex_sessions = plex_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
self.set_device(device)
self._season = None
def set_device(self, device):
"""Set the device property."""
self.device = device
# Music
self._media_album_artist = None
self._media_album_name = None
self._media_artist = None
self._media_track = None
# TV Show
self._media_episode = None
self._media_season = None
self._media_series_title = None
self.refresh(device, session)
# Assign custom entity ID if desired
if self.config.get(CONF_USE_CUSTOM_ENTITY_IDS):
prefix = ''
# allow for namespace prefixing when using custom entity names
if config.get("entity_namespace"):
prefix = config.get("entity_namespace") + '_'
# rename the entity id
if self.machine_identifier:
self.entity_id = "%s.%s%s" % (
'media_player', prefix,
self.machine_identifier.lower().replace('-', '_'))
else:
if self.name:
self.entity_id = "%s.%s%s" % (
'media_player', prefix,
self.name.lower().replace('-', '_'))
# pylint: disable=too-many-branches, too-many-statements
def refresh(self, device, session):
"""Refresh key device data."""
# new data refresh
if session:
self._session = session
if device:
self._device = device
self._session = None
if self._device:
self._machine_identifier = self._convert_na_to_none(
self._device.machineIdentifier)
self._name = self._convert_na_to_none(
self._device.title) or DEVICE_DEFAULT_NAME
self._device_protocol_capabilities = (
self._device.protocolCapabilities)
# set valid session, preferring device session
if self._device and self.plex_sessions.get(
self._device.machineIdentifier, None):
self._session = self._convert_na_to_none(self.plex_sessions.get(
self._device.machineIdentifier, None))
if self._session:
self._media_position = self._convert_na_to_none(
self._session.viewOffset)
self._media_content_id = self._convert_na_to_none(
self._session.ratingKey)
else:
self._media_position = None
self._media_content_id = None
# player dependent data
if self._session and self._session.player:
self._is_player_available = True
self._machine_identifier = self._convert_na_to_none(
self._session.player.machineIdentifier)
self._name = self._convert_na_to_none(self._session.player.title)
self._player_state = self._session.player.state
self._make = self._convert_na_to_none(self._session.player.device)
else:
self._is_player_available = False
if self._player_state == 'playing':
self._is_player_active = True
self._state = STATE_PLAYING
elif self._player_state == 'paused':
self._is_player_active = True
self._state = STATE_PAUSED
elif self.device:
self._is_player_active = False
self._state = STATE_IDLE
else:
self._is_player_active = False
self._state = STATE_OFF
if self._is_player_active and self._session is not None:
self._session_type = self._session.type
self._media_duration = self._convert_na_to_none(
self._session.duration)
else:
self._session_type = None
self._media_duration = None
# media type
if self._session_type == 'clip':
_LOGGER.debug('Clip content type detected, '
'compatibility may vary: %s',
self.entity_id)
self._media_content_type = MEDIA_TYPE_TVSHOW
elif self._session_type == 'episode':
self._media_content_type = MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
self._media_content_type = MEDIA_TYPE_VIDEO
elif self._session_type == 'track':
self._media_content_type = MEDIA_TYPE_MUSIC
else:
self._media_content_type = None
# title (movie name, tv episode name, music song name)
if self._session:
self._media_title = self._convert_na_to_none(self._session.title)
# Movies
if (self.media_content_type == MEDIA_TYPE_VIDEO and
self._convert_na_to_none(self._session.year) is not None):
self._media_title += ' (' + str(self._session.year) + ')'
# TV Show
if (self._is_player_active and
self._media_content_type is MEDIA_TYPE_TVSHOW):
# season number (00)
if callable(self._convert_na_to_none(self._session.seasons)):
self._media_season = self._convert_na_to_none(
self._session.seasons()[0].index).zfill(2)
elif self._convert_na_to_none(
self._session.parentIndex) is not None:
self._media_season = self._session.parentIndex.zfill(2)
else:
self._media_season = None
# show name
self._media_series_title = self._convert_na_to_none(
self._session.grandparentTitle)
# episode number (00)
if self._convert_na_to_none(
self._session.index) is not None:
self._media_episode = str(self._session.index).zfill(2)
else:
self._media_season = None
self._media_series_title = None
self._media_episode = None
# Music
if (self._is_player_active and
self._media_content_type == MEDIA_TYPE_MUSIC):
self._media_album_name = self._convert_na_to_none(
self._session.parentTitle)
self._media_album_artist = self._convert_na_to_none(
self._session.grandparentTitle)
self._media_track = self._convert_na_to_none(self._session.index)
self._media_artist = self._convert_na_to_none(
self._session.originalTitle)
# use album artist if track artist is missing
if self._media_artist is None:
_LOGGER.debug(
'Using album artist because track artist '
'was not found: %s', self.entity_id)
self._media_artist = self._media_album_artist
else:
self._media_album_name = None
self._media_album_artist = None
self._media_track = None
self._media_artist = None
# set app name to library name
if (self._session is not None
and self._session.librarySectionID is not None):
self._app_name = self._convert_na_to_none(
self._session.server.library.sectionByID(
self._session.librarySectionID).title)
else:
self._app_name = ''
# media image url
if self._session is not None:
thumb_url = self._get_thumbnail_url(self._session.thumb)
if (self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.config.get(CONF_USE_EPISODE_ART)):
thumb_url = self._get_thumbnail_url(
self._session.grandparentThumb)
if thumb_url is None:
_LOGGER.debug('Using media art because media thumb '
'was not found: %s', self.entity_id)
thumb_url = self._get_thumbnail_url(self._session.art)
self._media_image_url = thumb_url
else:
self._media_image_url = None
def _get_thumbnail_url(self, property_value):
"""Return full URL (if exists) for a thumbnail property."""
if self._convert_na_to_none(property_value) is None:
return None
if self._session is None or self._session.server is None:
return None
url = self._session.server.url(property_value)
response = requests.get(url, verify=False)
if response and response.status_code == 200:
return url
def force_idle(self):
"""Force client to idle."""
self._state = STATE_IDLE
self._session = None
@property
def unique_id(self):
"""Return the id of this plex client."""
return '{}.{}'.format(
self.__class__, self.device.machineIdentifier or self.device.name)
return '{}.{}'.format(self.__class__, self.machine_identifier or
self.name)
@property
def name(self):
"""Return the name of the device."""
return self.device.title or DEVICE_DEFAULT_NAME
return self._name
@property
def machine_identifier(self):
"""Return the machine identifier of the device."""
return self._machine_identifier
@property
def app_name(self):
"""Return the library name of playing media."""
return self._app_name
@property
def device(self):
"""Return the device, if any."""
return self._device
@property
def session(self):
"""Return the session, if any."""
return self.plex_sessions.get(self.device.machineIdentifier, None)
return self._session
@property
def state(self):
"""Return the state of the device."""
if self.session and self.session.player:
state = self.session.player.state
if state == 'playing':
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
# This is nasty. Need to find a way to determine alive
elif self.device:
return STATE_IDLE
else:
return STATE_OFF
return STATE_UNKNOWN
return self._state
def update(self):
"""Get the latest details."""
from plexapi.video import Show
self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True)
if isinstance(self.session, Show):
self._season = self._convert_na_to_none(
self.session.seasons()[0].index)
# pylint: disable=no-self-use, singleton-comparison
def _convert_na_to_none(self, value):
"""Convert PlexAPI _NA() instances to None."""
@ -272,93 +559,298 @@ class PlexClient(MediaPlayerDevice):
@property
def media_content_id(self):
"""Content ID of current playing media."""
if self.session is not None:
return self._convert_na_to_none(self.session.ratingKey)
return self._media_content_id
@property
def media_content_type(self):
"""Content type of current playing media."""
if self.session is None:
return None
media_type = self.session.type
if media_type == 'episode':
if self._session_type == 'clip':
_LOGGER.debug('Clip content type detected, '
'compatibility may vary: %s',
self.entity_id)
return MEDIA_TYPE_TVSHOW
elif media_type == 'movie':
elif self._session_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
return MEDIA_TYPE_VIDEO
elif media_type == 'track':
elif self._session_type == 'track':
return MEDIA_TYPE_MUSIC
return None
else:
return None
@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_name
@property
def media_album_artist(self):
"""Album artist of current playing media, music track only."""
return self._media_album_artist
@property
def media_track(self):
"""Track number of current playing media, music track only."""
return self._media_track
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self.session is not None:
return self._convert_na_to_none(self.session.duration)
return self._media_duration
@property
def media_image_url(self):
"""Image url of current playing media."""
if self.session is not None:
thumb_url = self._convert_na_to_none(self.session.thumbUrl)
if str(self.na_type) in thumb_url:
# Audio tracks build their thumb urls internally before passing
# back a URL with the PlexAPI _NA type already converted to a
# string and embedded into a malformed URL
thumb_url = None
return thumb_url
return self._media_image_url
@property
def media_title(self):
"""Title of current playing media."""
# find a string we can use as a title
if self.session is not None:
return self._convert_na_to_none(self.session.title)
return self._media_title
@property
def media_season(self):
"""Season of curent playing media (TV Show only)."""
return self._season
return self._media_season
@property
def media_series_title(self):
"""The title of the series of current playing media (TV Show only)."""
from plexapi.video import Show
if isinstance(self.session, Show):
return self._convert_na_to_none(self.session.grandparentTitle)
return self._media_series_title
@property
def media_episode(self):
"""Episode of current playing media (TV Show only)."""
from plexapi.video import Show
if isinstance(self.session, Show):
return self._convert_na_to_none(self.session.index)
return self._media_episode
@property
def make(self):
"""The make of the device (ex. SHIELD Android TV)."""
return self._make
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_PLEX
if not self._is_player_active:
return None
# force show all controls
if self.config.get(CONF_SHOW_ALL_CONTROLS):
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
# only show controls when we know what device is connecting
if not self._make:
return None
# no mute support
elif self.make.lower() == "shield android tv":
_LOGGER.debug(
'Shield Android TV client detected, disabling mute '
'controls: %s', self.entity_id)
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF)
# Only supports play,pause,stop (and off which really is stop)
elif self.make.lower().startswith("tivo"):
_LOGGER.debug(
'Tivo client detected, only enabling pause, play, '
'stop, and off controls: %s', self.entity_id)
return (SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP |
SUPPORT_TURN_OFF)
# Not all devices support playback functionality
# Playback includes volume, stop/play/pause, etc.
elif self.device and 'playback' in self._device_protocol_capabilities:
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
else:
return None
def _local_client_control_fix(self):
"""Detect if local client and adjust url to allow control."""
if self.device is None:
return
# if this device's machineIdentifier matches an active client
# with a loopback address, the device must be local or casting
for client in self.device.server.clients():
if ("127.0.0.1" in client.baseurl and
client.machineIdentifier == self.device.machineIdentifier):
# point controls to server since that's where the
# playback is occuring
_LOGGER.debug(
'Local client detected, redirecting controls to '
'Plex server: %s', self.entity_id)
server_url = self.device.server.baseurl
client_url = self.device.baseurl
self.device.baseurl = "{}://{}:{}".format(
urlparse(client_url).scheme,
urlparse(server_url).hostname,
str(urlparse(client_url).port))
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self.device.setVolume(int(volume * 100),
self._active_media_plexapi_type)
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.setVolume(
int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve
@property
def volume_level(self):
"""Return the volume level of the client (0..1)."""
if (self._is_player_active and self.device and
'playback' in self._device_protocol_capabilities):
return self._volume_level
@property
def is_volume_muted(self):
"""Return boolean if volume is currently muted."""
if self._is_player_active and self.device:
return self._volume_muted
def mute_volume(self, mute):
"""Mute the volume.
Since we can't actually mute, we'll:
- On mute, store volume and set volume to 0
- On unmute, set volume to previously stored volume
"""
if not (self.device and
'playback' in self._device_protocol_capabilities):
return
self._volume_muted = mute
if mute:
self._previous_volume_level = self._volume_level
self.set_volume_level(0)
else:
self.set_volume_level(self._previous_volume_level)
def media_play(self):
"""Send play command."""
self.device.play(self._active_media_plexapi_type)
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.play(self._active_media_plexapi_type)
def media_pause(self):
"""Send pause command."""
self.device.pause(self._active_media_plexapi_type)
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.pause(self._active_media_plexapi_type)
def media_stop(self):
"""Send stop command."""
self.device.stop(self._active_media_plexapi_type)
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.stop(self._active_media_plexapi_type)
def turn_off(self):
"""Turn the client off."""
# Fake it since we can't turn the client off
self.media_stop()
def media_next_track(self):
"""Send next track command."""
self.device.skipNext(self._active_media_plexapi_type)
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.skipNext(self._active_media_plexapi_type)
def media_previous_track(self):
"""Send previous track command."""
self.device.skipPrevious(self._active_media_plexapi_type)
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.skipPrevious(self._active_media_plexapi_type)
# pylint: disable=W0613
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
if not (self.device and
'playback' in self._device_protocol_capabilities):
return
src = json.loads(media_id)
media = None
if media_type == 'MUSIC':
media = self.device.server.library.section(
src['library_name']).get(src['artist_name']).album(
src['album_name']).get(src['track_name'])
elif media_type == 'EPISODE':
media = self._get_episode(
src['library_name'], src['show_name'],
src['season_number'], src['episode_number'])
elif media_type == 'PLAYLIST':
media = self.device.server.playlist(src['playlist_name'])
elif media_type == 'VIDEO':
media = self.device.server.library.section(
src['library_name']).get(src['video_name'])
if media:
self._client_play_media(media, shuffle=src['shuffle'])
def _get_episode(self, library_name, show_name, season_number,
episode_number):
"""Find TV episode and return a Plex media object."""
target_season = None
target_episode = None
seasons = self.device.server.library.section(library_name).get(
show_name).seasons()
for season in seasons:
if int(season.seasonNumber) == int(season_number):
target_season = season
break
if target_season is None:
_LOGGER.error('Season not found: %s\\%s - S%sE%s', library_name,
show_name,
str(season_number).zfill(2),
str(episode_number).zfill(2))
else:
for episode in target_season.episodes():
if int(episode.index) == int(episode_number):
target_episode = episode
break
if target_episode is None:
_LOGGER.error('Episode not found: %s\\%s - S%sE%s',
library_name, show_name,
str(season_number).zfill(2),
str(episode_number).zfill(2))
return target_episode
def _client_play_media(self, media, **params):
"""Instruct Plex client to play a piece of media."""
if not (self.device and
'playback' in self._device_protocol_capabilities):
_LOGGER.error('Client cannot play media: %s', self.entity_id)
return
import plexapi.playqueue
server_url = media.server.baseurl.split(':')
playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,
media, **params)
self._local_client_control_fix()
self.device.sendCommand('playback/playMedia', **dict({
'machineIdentifier':
self.device.server.machineIdentifier,
'address':
server_url[1].strip('/'),
'port':
server_url[-1],
'key':
media.key,
'containerKey':
'/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params))

View file

@ -0,0 +1,237 @@
"""
Volumio Platform.
The volumio platform allows you to control a Volumio media player
from Home Assistant.
To add a Volumio player to your installation, add the following to
your configuration.yaml file.
# Example configuration.yaml entry
media_player:
- platform: volumio
name: 'Volumio Home Audio'
host: homeaudio.local
port: 3000
Configuration variables:
- **name** (*Optional*): Name of the device
- **host** (*Required*): IP address or hostname of the device
- **port** (*Required*): Port number of Volumio service
"""
import logging
import asyncio
import aiohttp
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_PLAY, MediaPlayerDevice,
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC)
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Volumio'
DEFAULT_PORT = 3000
TIMEOUT = 10
SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Volumio platform."""
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name = config.get(CONF_NAME)
async_add_devices([Volumio(name, host, port, hass)])
class Volumio(MediaPlayerDevice):
"""Volumio Player Object."""
def __init__(self, name, host, port, hass):
"""Initialize the media player."""
self.host = host
self.port = port
self.hass = hass
self._url = host + ":" + str(port)
self._name = name
self._state = {}
self.async_update()
self._lastvol = self._state.get('volume', 0)
@asyncio.coroutine
def send_volumio_msg(self, method, params=None):
"""Send message."""
url = "http://{}:{}/api/v1/{}/".format(
self.host, self.port, method)
response = None
_LOGGER.debug("URL: %s params: %s", url, params)
try:
websession = async_get_clientsession(self.hass)
response = yield from websession.get(url, params=params)
if response.status == 200:
data = yield from response.json()
else:
_LOGGER.error(
"Query failed, response code: %s Full message: %s",
response.status, response)
return False
except (asyncio.TimeoutError,
aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as error:
_LOGGER.error("Failed communicating with Volumio: %s", type(error))
return False
finally:
if response is not None:
yield from response.release()
try:
return data
except AttributeError:
_LOGGER.error("Received invalid response: %s", data)
return False
@asyncio.coroutine
def async_update(self):
"""Update state."""
resp = yield from self.send_volumio_msg('getState')
if resp is False:
return
self._state = resp.copy()
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def state(self):
"""Return the state of the device."""
status = self._state.get('status', None)
if status == 'pause':
return STATE_PAUSED
elif status == 'play':
return STATE_PLAYING
else:
return STATE_IDLE
@property
def media_title(self):
"""Title of current playing media."""
return self._state.get('title', None)
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
return self._state.get('artist', None)
@property
def media_album_name(self):
"""Artist of current playing media (Music track only)."""
return self._state.get('album', None)
@property
def media_image_url(self):
"""Image url of current playing media."""
url = self._state.get('albumart', None)
if url is None:
return
if str(url[0:2]).lower() == 'ht':
mediaurl = url
else:
mediaurl = "http://" + self.host + ":" + str(self.port) + url
return mediaurl
@property
def media_seek_position(self):
"""Time in seconds of current seek position."""
return self._state.get('seek', None)
@property
def media_duration(self):
"""Time in seconds of current song duration."""
return self._state.get('duration', None)
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
volume = self._state.get('volume', None)
if volume is not None:
volume = volume / 100
return volume
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._state.get('mute', None)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def supported_features(self):
"""Flag of media commands that are supported."""
return SUPPORT_VOLUMIO
def async_media_next_track(self):
"""Send media_next command to media player."""
return self.send_volumio_msg('commands', params={'cmd': 'next'})
def async_media_previous_track(self):
"""Send media_previous command to media player."""
return self.send_volumio_msg('commands', params={'cmd': 'prev'})
def async_media_play(self):
"""Send media_play command to media player."""
return self.send_volumio_msg('commands', params={'cmd': 'play'})
def async_media_pause(self):
"""Send media_pause command to media player."""
return self.send_volumio_msg('commands', params={'cmd': 'pause'})
def async_set_volume_level(self, volume):
"""Send volume_up command to media player."""
return self.send_volumio_msg('commands',
params={'cmd': 'volume',
'volume': int(volume * 100)})
def async_mute_volume(self, mute):
"""Send mute command to media player."""
mutecmd = 'mute' if mute else 'unmute'
if mute:
# mute is implemenhted as 0 volume, do save last volume level
self._lastvol = self._state['volume']
return self.send_volumio_msg('commands',
params={'cmd': 'volume',
'volume': mutecmd})
else:
return self.send_volumio_msg('commands',
params={'cmd': 'volume',
'volume': self._lastvol})

View file

@ -9,6 +9,7 @@ import logging
import os
import socket
import time
import ssl
import requests.certs
import voluptuous as vol
@ -48,6 +49,7 @@ CONF_CERTIFICATE = 'certificate'
CONF_CLIENT_KEY = 'client_key'
CONF_CLIENT_CERT = 'client_cert'
CONF_TLS_INSECURE = 'tls_insecure'
CONF_TLS_VERSION = 'tls_version'
CONF_BIRTH_MESSAGE = 'birth_message'
CONF_WILL_MESSAGE = 'will_message'
@ -67,6 +69,7 @@ DEFAULT_RETAIN = False
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_DISCOVERY = False
DEFAULT_DISCOVERY_PREFIX = 'homeassistant'
DEFAULT_TLS_PROTOCOL = 'auto'
ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload'
@ -116,12 +119,15 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): cv.isfile,
vol.Optional(CONF_CERTIFICATE): vol.Any('auto', cv.isfile),
vol.Inclusive(CONF_CLIENT_KEY, 'client_key_auth',
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth',
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
vol.Optional(CONF_TLS_VERSION,
default=DEFAULT_TLS_PROTOCOL): vol.Any('auto', '1.0',
'1.1', '1.2'),
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA,
@ -311,18 +317,34 @@ def async_setup(hass, config):
certificate = os.path.join(os.path.dirname(__file__),
'addtrustexternalcaroot.crt')
# When the port indicates mqtts, use bundled certificates from requests
if certificate is None and port == 8883:
# When the certificate is set to auto, use bundled certs from requests
if certificate == 'auto':
certificate = requests.certs.where()
will_message = conf.get(CONF_WILL_MESSAGE)
birth_message = conf.get(CONF_BIRTH_MESSAGE)
# Be able to override versions other than TLSv1.0 under Python3.6
conf_tls_version = conf.get(CONF_TLS_VERSION)
if conf_tls_version == '1.2':
tls_version = ssl.PROTOCOL_TLSv1_2
elif conf_tls_version == '1.1':
tls_version = ssl.PROTOCOL_TLSv1_1
elif conf_tls_version == '1.0':
tls_version = ssl.PROTOCOL_TLSv1
else:
import sys
# Python3.6 supports automatic negotiation of highest TLS version
if sys.hexversion >= 0x03060000:
tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member
else:
tls_version = ssl.PROTOCOL_TLSv1
try:
hass.data[DATA_MQTT] = MQTT(
hass, broker, port, client_id, keepalive, username, password,
certificate, client_key, client_cert, tls_insecure, protocol,
will_message, birth_message)
will_message, birth_message, tls_version)
except socket.error:
_LOGGER.exception("Can't connect to the broker. "
"Please check your settings and the broker itself")
@ -380,7 +402,8 @@ class MQTT(object):
def __init__(self, hass, broker, port, client_id, keepalive, username,
password, certificate, client_key, client_cert,
tls_insecure, protocol, will_message, birth_message):
tls_insecure, protocol, will_message, birth_message,
tls_version):
"""Initialize Home Assistant MQTT client."""
import paho.mqtt.client as mqtt
@ -409,7 +432,8 @@ class MQTT(object):
if certificate is not None:
self._mqttc.tls_set(
certificate, certfile=client_cert, keyfile=client_key)
certificate, certfile=client_cert,
keyfile=client_key, tls_version=tls_version)
if tls_insecure is not None:
self._mqttc.tls_insecure_set(tls_insecure)

View file

@ -84,6 +84,11 @@ class iOSNotificationService(BaseNotificationService):
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
for target in targets:
if target not in ios.enabled_push_ids():
_LOGGER.error("The target (%s) does not exist in ios.conf.",
targets)
return
data[ATTR_TARGET] = target
req = requests.post(PUSH_URL, json=data, timeout=10)

View file

@ -4,24 +4,33 @@ Kodi notification service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.kodi/
"""
import asyncio
import logging
import aiohttp
import voluptuous as vol
from homeassistant.const import (ATTR_ICON, CONF_HOST, CONF_PORT,
CONF_USERNAME, CONF_PASSWORD)
from homeassistant.components.notify import (ATTR_TITLE, ATTR_TITLE_DEFAULT,
ATTR_DATA, PLATFORM_SCHEMA,
BaseNotificationService)
from homeassistant.const import (
ATTR_ICON, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_PROXY_SSL)
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
BaseNotificationService)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['jsonrpc-async==0.4']
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests==0.3']
DEFAULT_PORT = 8080
DEFAULT_PROXY_SSL = False
DEFAULT_TIMEOUT = 5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
})
@ -29,51 +38,66 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
ATTR_DISPLAYTIME = 'displaytime'
def get_service(hass, config, discovery_info=None):
@asyncio.coroutine
def async_get_service(hass, config, discovery_info=None):
"""Return the notify service."""
url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT))
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
encryption = config.get(CONF_PROXY_SSL)
if host.startswith('http://') or host.startswith('https://'):
host = host.lstrip('http://').lstrip('https://')
_LOGGER.warning(
"Kodi host name should no longer conatin http:// See updated "
"definitions here: "
"https://home-assistant.io/components/media_player.kodi/")
http_protocol = 'https' if encryption else 'http'
url = '{}://{}:{}/jsonrpc'.format(http_protocol, host, port)
if username is not None:
auth = (username, password)
auth = aiohttp.BasicAuth(username, password)
else:
auth = None
return KODINotificationService(
url,
auth
)
return KodiNotificationService(hass, url, auth)
class KODINotificationService(BaseNotificationService):
class KodiNotificationService(BaseNotificationService):
"""Implement the notification service for Kodi."""
def __init__(self, url, auth=None):
def __init__(self, hass, url, auth=None):
"""Initialize the service."""
import jsonrpc_requests
import jsonrpc_async
self._url = url
kwargs = {'timeout': 5}
kwargs = {
'timeout': DEFAULT_TIMEOUT,
'session': async_get_clientsession(hass),
}
if auth is not None:
kwargs['auth'] = auth
self._server = jsonrpc_requests.Server(
'{}/jsonrpc'.format(self._url), **kwargs)
self._server = jsonrpc_async.Server(self._url, **kwargs)
def send_message(self, message="", **kwargs):
@asyncio.coroutine
def async_send_message(self, message="", **kwargs):
"""Send a message to Kodi."""
import jsonrpc_requests
import jsonrpc_async
try:
data = kwargs.get(ATTR_DATA) or {}
displaytime = data.get(ATTR_DISPLAYTIME, 10000)
icon = data.get(ATTR_ICON, "info")
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
self._server.GUI.ShowNotification(title, message, icon,
displaytime)
yield from self._server.GUI.ShowNotification(
title, message, icon, displaytime)
except jsonrpc_requests.jsonrpc.TransportError:
except jsonrpc_async.TransportError:
_LOGGER.warning('Unable to fetch Kodi data, Is Kodi online?')

View file

@ -18,7 +18,8 @@ from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
BaseNotificationService)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT)
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
@ -32,6 +33,7 @@ CONF_SERVER = 'server'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 25
DEFAULT_TIMEOUT = 5
DEFAULT_DEBUG = False
DEFAULT_STARTTLS = False
@ -40,6 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RECIPIENT): vol.Email(),
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_SENDER): vol.Email(),
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
@ -53,6 +56,7 @@ def get_service(hass, config, discovery_info=None):
mail_service = MailNotificationService(
config.get(CONF_SERVER),
config.get(CONF_PORT),
config.get(CONF_TIMEOUT),
config.get(CONF_SENDER),
config.get(CONF_STARTTLS),
config.get(CONF_USERNAME),
@ -69,11 +73,12 @@ def get_service(hass, config, discovery_info=None):
class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-Mail messages."""
def __init__(self, server, port, sender, starttls, username,
def __init__(self, server, port, timeout, sender, starttls, username,
password, recipient, debug):
"""Initialize the service."""
self._server = server
self._port = port
self._timeout = timeout
self._sender = sender
self.starttls = starttls
self.username = username
@ -84,7 +89,7 @@ class MailNotificationService(BaseNotificationService):
def connect(self):
"""Connect/authenticate to SMTP Server."""
mail = smtplib.SMTP(self._server, self._port, timeout=5)
mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout)
mail.set_debuglevel(self.debug)
mail.ehlo_or_helo_if_needed()
if self.starttls:

View file

@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
REQUIREMENTS = ['sleekxmpp==1.3.1',
'dnspython3==1.15.0',
'pyasn1==0.2.2',
'pyasn1==0.2.3',
'pyasn1-modules==0.0.8']
_LOGGER = logging.getLogger(__name__)

View file

@ -35,7 +35,7 @@ from .util import session_scope
DOMAIN = 'recorder'
REQUIREMENTS = ['sqlalchemy==1.1.5']
REQUIREMENTS = ['sqlalchemy==1.1.6']
DEFAULT_URL = 'sqlite:///{hass_config_path}'
DEFAULT_DB_FILE = 'home-assistant_v2.db'
@ -287,13 +287,27 @@ class Recorder(threading.Thread):
def _setup_connection(self):
"""Ensure database is ready to fly."""
from sqlalchemy import create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.engine import Engine
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
from . import models
kwargs = {}
# pylint: disable=unused-variable
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
"""Set sqlite's WAL mode."""
if self.db_url.startswith("sqlite://"):
old_isolation = dbapi_connection.isolation_level
dbapi_connection.isolation_level = None
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.close()
dbapi_connection.isolation_level = old_isolation
if self.db_url == 'sqlite://' or ':memory:' in self.db_url:
from sqlalchemy.pool import StaticPool

View file

@ -112,7 +112,7 @@ def async_setup(hass, config):
if not remote.should_poll:
continue
update_coro = hass.loop.create_task(
update_coro = hass.async_add_job(
remote.async_update_ha_state(True))
if hasattr(remote, 'async_update'):
update_tasks.append(update_coro)

View file

@ -90,7 +90,7 @@ def async_setup(hass, config):
auth=auth
)
if request.status == 200:
if request.status < 400:
_LOGGER.info("Success call %s.", request.url)
return

View file

@ -316,6 +316,12 @@ class RflinkCommand(RflinkDevice):
cmd = str(int(args[0] / 17))
self._state = True
elif command == 'toggle':
cmd = 'on'
# if the state is unknown or false, it gets set as true
# if the state is true, it gets set as false
self._state = self._state in [STATE_UNKNOWN, False]
# Send initial command and queue repetitions.
# This allows the entity state to be updated quickly and not having to
# wait for all repetitions to be sent
@ -357,7 +363,7 @@ class RflinkCommand(RflinkDevice):
self._protocol.send_command, self._device_id, cmd))
if repetitions > 1:
self._repetition_task = self.hass.loop.create_task(
self._repetition_task = self.hass.async_add_job(
self._async_send_command(cmd, repetitions - 1))

View file

@ -0,0 +1,40 @@
"""
Support for Wink scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.wink/
"""
import logging
from homeassistant.components.scene import Scene
from homeassistant.components.wink import WinkDevice, DOMAIN
DEPENDENCIES = ['wink']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink platform."""
import pywink
for scene in pywink.get_scenes():
_id = scene.object_id() + scene.name()
if _id not in hass.data[DOMAIN]['unique_ids']:
add_devices([WinkScene(scene, hass)])
class WinkScene(WinkDevice, Scene):
"""Representation of a Wink shortcut/scene."""
def __init__(self, wink, hass):
"""Initialize the Wink device."""
super().__init__(wink, hass)
@property
def is_on(self):
"""Python-wink will always return False."""
return self.wink.state()
def activate(self, **kwargs):
"""Activate the scene."""
self.wink.activate()

View file

@ -99,11 +99,13 @@ class ComedHourlyPricingSensor(Entity):
if self.type == CONF_FIVE_MINUTE:
url_string = _RESOURCE + '?type=5minutefeed'
response = get(url_string, timeout=10)
self._state = float(response.json()[0]['price']) + self.offset
self._state = round(
float(response.json()[0]['price']) + self.offset, 2)
elif self.type == CONF_CURRENT_HOUR_AVERAGE:
url_string = _RESOURCE + '?type=currenthouraverage'
response = get(url_string, timeout=10)
self._state = float(response.json()[0]['price']) + self.offset
self._state = round(
float(response.json()[0]['price']) + self.offset, 2)
else:
self._state = STATE_UNKNOWN
except (RequestException, ValueError, KeyError):

View file

@ -13,7 +13,7 @@ from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['py-cpuinfo==0.2.6']
REQUIREMENTS = ['py-cpuinfo==0.2.7']
_LOGGER = logging.getLogger(__name__)
@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the CPU speed sensor."""
"""Set up the CPU speed sensor."""
name = config.get(CONF_NAME)
add_devices([CpuSpeedSensor(name)])

View file

@ -13,7 +13,8 @@ from requests.exceptions import ConnectionError as ConnectError, \
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_API_KEY, CONF_NAME, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION)
CONF_API_KEY, CONF_NAME, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION,
CONF_LATITUDE, CONF_LONGITUDE)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
@ -117,6 +118,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']),
vol.Inclusive(CONF_LATITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.longitude,
vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): (
vol.All(cv.time_period, cv.positive_timedelta)),
vol.Optional(CONF_FORECAST):
@ -126,10 +131,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Dark Sky sensor."""
# 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
# latitude and longitude are inclusive on config
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
if CONF_UNITS in config:
units = config[CONF_UNITS]
@ -140,8 +144,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
forecast_data = DarkSkyData(
api_key=config.get(CONF_API_KEY, None),
latitude=hass.config.latitude,
longitude=hass.config.longitude,
latitude=latitude,
longitude=longitude,
units=units,
interval=config.get(CONF_UPDATE_INTERVAL))
forecast_data.update()

View file

@ -108,7 +108,9 @@ class Dovado:
"""Update device state."""
_LOGGER.info("Updating")
try:
self.state.update(self._dovado.state or {})
self.state = self._dovado.state or {}
if not self.state:
return False
self.state.update(
connected=self.state.get("modem status") == "CONNECTED")
_LOGGER.debug("Received: %s", self.state)

View file

@ -40,7 +40,7 @@ import voluptuous as vol
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['dsmr_parser==0.6']
REQUIREMENTS = ['dsmr_parser==0.8']
CONF_DSMR_VERSION = 'dsmr_version'
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
@ -72,7 +72,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
from dsmr_parser import obis_references as obis_ref
from dsmr_parser.protocol import create_dsmr_reader, create_tcp_dsmr_reader
from dsmr_parser.clients.protocol import (create_dsmr_reader,
create_tcp_dsmr_reader)
import serial
dsmr_version = config[CONF_DSMR_VERSION]
@ -110,7 +111,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# Make all device entities aware of new telegram
for device in devices:
device.telegram = telegram
hass.async_add_job(device.async_update_ha_state)
hass.async_add_job(device.async_update_ha_state())
# Creates a asyncio.Protocol factory for reading DSMR telegrams from serial
# and calls update_entities_telegram to update entities on arrival
@ -132,7 +133,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def connect_and_reconnect():
"""Connect to DSMR and keep reconnecting until HA stops."""
while hass.state != CoreState.stopping:
# Start DSMR asycnio.Protocol reader
# Start DSMR asyncio.Protocol reader
try:
transport, protocol = yield from hass.loop.create_task(
reader_factory())
@ -160,6 +161,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL],
loop=hass.loop)
# Cannot be hass.async_add_job because job runs forever
hass.loop.create_task(connect_and_reconnect())

View file

@ -28,12 +28,14 @@ CONF_INSTANT = 'instant_readings'
CONF_AMOUNT = 'amount'
CONF_BUDGET = 'budget'
CONF_COST = 'cost'
CONF_CURRENT_VALUES = 'current_values'
SENSOR_TYPES = {
CONF_INSTANT: ['Energy Usage', 'kW'],
CONF_AMOUNT: ['Energy Consumed', 'kWh'],
CONF_BUDGET: ['Energy Budget', None],
CONF_COST: ['Energy Cost', None],
CONF_CURRENT_VALUES: ['Per-Device Usage', 'kW']
}
TYPES_SCHEMA = vol.In(SENSOR_TYPES)
@ -57,19 +59,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
utc_offset = str(config.get(CONF_UTC_OFFSET))
dev = []
for variable in config[CONF_MONITORED_VARIABLES]:
if variable[CONF_SENSOR_TYPE] == CONF_CURRENT_VALUES:
url_string = _RESOURCE + 'getCurrentValuesSummary?token=' \
+ app_token
response = get(url_string, timeout=10)
for sensor in response.json():
sid = sensor['sid']
dev.append(EfergySensor(variable[CONF_SENSOR_TYPE], app_token,
utc_offset, variable[CONF_PERIOD],
variable[CONF_CURRENCY], sid))
dev.append(EfergySensor(
variable[CONF_SENSOR_TYPE], app_token, utc_offset,
variable[CONF_PERIOD], variable[CONF_CURRENCY]))
add_devices(dev)
add_devices(dev, True)
class EfergySensor(Entity):
"""Implementation of an Efergy sensor."""
def __init__(self, sensor_type, app_token, utc_offset, period, currency):
def __init__(self, sensor_type, app_token, utc_offset, period,
currency, sid=None):
"""Initialize the sensor."""
self._name = SENSOR_TYPES[sensor_type][0]
self.sid = sid
if sid:
self._name = 'efergy_' + sid
else:
self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self.app_token = app_token
self.utc_offset = utc_offset
@ -119,6 +135,14 @@ class EfergySensor(Entity):
+ self.period
response = get(url_string, timeout=10)
self._state = response.json()['sum']
elif self.type == 'current_values':
url_string = _RESOURCE + 'getCurrentValuesSummary?token=' \
+ self.app_token
response = get(url_string, timeout=10)
for sensor in response.json():
if self.sid == sensor['sid']:
measurement = next(iter(sensor['data'][0].values()))
self._state = measurement / 1000
else:
self._state = 'Unknown'
except (RequestException, ValueError, KeyError):

View file

@ -9,13 +9,19 @@ import socket
import threading
import datetime
import time
import re
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME)
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME,
CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['fritzconnection==0.6.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Phone'
@ -27,13 +33,24 @@ VALUE_RING = 'ringing'
VALUE_CALL = 'dialing'
VALUE_CONNECT = 'talking'
VALUE_DISCONNECT = 'idle'
CONF_PHONEBOOK = 'phonebook'
CONF_PREFIXES = 'prefixes'
INTERVAL_RECONNECT = 60
# Return cached results if phonebook was downloaded less then this time ago.
MIN_TIME_PHONEBOOK_UPDATE = datetime.timedelta(hours=6)
SCAN_INTERVAL = datetime.timedelta(hours=3)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PASSWORD, default='admin'): cv.string,
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Optional(CONF_PHONEBOOK, default=0): cv.positive_int,
vol.Optional(CONF_PREFIXES, default=[]): vol.All(cv.ensure_list,
[cv.string])
})
@ -42,14 +59,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
phonebook_id = config.get('phonebook')
prefixes = config.get('prefixes')
sensor = FritzBoxCallSensor(name=name)
try:
phonebook = FritzBoxPhonebook(host=host, port=port,
username=username, password=password,
phonebook_id=phonebook_id,
prefixes=prefixes)
# pylint: disable=bare-except
except:
phonebook = None
_LOGGER.warning('Phonebook with ID %s not found on Fritz!Box',
phonebook_id)
sensor = FritzBoxCallSensor(name=name, phonebook=phonebook)
add_devices([sensor])
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
monitor.connect()
def _stop_listener(_event):
monitor.stopped.set()
hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP,
_stop_listener
)
if monitor.sock is None:
return False
else:
@ -59,11 +99,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class FritzBoxCallSensor(Entity):
"""Implementation of a Fritz!Box call monitor."""
def __init__(self, name):
def __init__(self, name, phonebook):
"""Initialize the sensor."""
self._state = VALUE_DEFAULT
self._attributes = {}
self._name = name
self.phonebook = phonebook
def set_state(self, state):
"""Set the state."""
@ -75,8 +116,11 @@ class FritzBoxCallSensor(Entity):
@property
def should_poll(self):
"""No polling needed."""
return False
"""Polling needed only to update phonebook, if defined."""
if self.phonebook is None:
return False
else:
return True
@property
def state(self):
@ -93,6 +137,18 @@ class FritzBoxCallSensor(Entity):
"""Return the state attributes."""
return self._attributes
def number_to_name(self, number):
"""Return a name for a given phone number."""
if self.phonebook is None:
return 'unknown'
else:
return self.phonebook.get_name(number)
def update(self):
"""Update the phonebook if it is defined."""
if self.phonebook is not None:
self.phonebook.update_phonebook()
class FritzBoxCallMonitor(object):
"""Event listener to monitor calls on the Fritz!Box."""
@ -103,6 +159,7 @@ class FritzBoxCallMonitor(object):
self.port = port
self.sock = None
self._sensor = sensor
self.stopped = threading.Event()
def connect(self):
"""Connect to the Fritz!Box."""
@ -110,7 +167,7 @@ class FritzBoxCallMonitor(object):
self.sock.settimeout(10)
try:
self.sock.connect((self.host, self.port))
threading.Thread(target=self._listen, daemon=True).start()
threading.Thread(target=self._listen).start()
except socket.error as err:
self.sock = None
_LOGGER.error("Cannot connect to %s on port %s: %s",
@ -118,7 +175,7 @@ class FritzBoxCallMonitor(object):
def _listen(self):
"""Listen to incoming or outgoing calls."""
while True:
while not self.stopped.isSet():
try:
response = self.sock.recv(2048)
except socket.timeout:
@ -152,6 +209,7 @@ class FritzBoxCallMonitor(object):
"to": line[4],
"device": line[5],
"initiated": isotime}
att["from_name"] = self._sensor.number_to_name(att["from"])
self._sensor.set_attributes(att)
elif line[1] == "CALL":
self._sensor.set_state(VALUE_CALL)
@ -160,13 +218,73 @@ class FritzBoxCallMonitor(object):
"to": line[5],
"device": line[6],
"initiated": isotime}
att["to_name"] = self._sensor.number_to_name(att["to"])
self._sensor.set_attributes(att)
elif line[1] == "CONNECT":
self._sensor.set_state(VALUE_CONNECT)
att = {"with": line[4], "device": [3], "accepted": isotime}
att["with_name"] = self._sensor.number_to_name(att["with"])
self._sensor.set_attributes(att)
elif line[1] == "DISCONNECT":
self._sensor.set_state(VALUE_DISCONNECT)
att = {"duration": line[3], "closed": isotime}
self._sensor.set_attributes(att)
self._sensor.schedule_update_ha_state()
class FritzBoxPhonebook(object):
"""This connects to a FritzBox router and downloads its phone book."""
def __init__(self, host, port, username, password,
phonebook_id=0, prefixes=None):
"""Initialize the class."""
self.host = host
self.username = username
self.password = password
self.port = port
self.phonebook_id = phonebook_id
self.phonebook_dict = None
self.number_dict = None
self.prefixes = prefixes or []
# pylint: disable=import-error
import fritzconnection as fc
# Establish a connection to the FRITZ!Box.
self.fph = fc.FritzPhonebook(address=self.host,
user=self.username,
password=self.password)
if self.phonebook_id not in self.fph.list_phonebooks:
raise ValueError("Phonebook with this ID not found.")
self.update_phonebook()
@Throttle(MIN_TIME_PHONEBOOK_UPDATE)
def update_phonebook(self):
"""Update the phone book dictionary."""
self.phonebook_dict = self.fph.get_all_names(self.phonebook_id)
self.number_dict = {re.sub(r'[^\d\+]', '', nr): name
for name, nrs in self.phonebook_dict.items()
for nr in nrs}
_LOGGER.info('Fritz!Box phone book successfully updated.')
def get_name(self, number):
"""Return a name for a given phone number."""
number = re.sub(r'[^\d\+]', '', str(number))
if self.number_dict is None:
return 'unknown'
try:
return self.number_dict[number]
except KeyError:
pass
if self.prefixes:
for prefix in self.prefixes:
try:
return self.number_dict[prefix + number]
except KeyError:
pass
try:
return self.number_dict[prefix + number.lstrip('0')]
except KeyError:
pass
return 'unknown'

View file

@ -17,7 +17,7 @@ from homeassistant.util import Throttle
from requests.exceptions import RequestException
REQUIREMENTS = ['fritzconnection==0.6']
REQUIREMENTS = ['fritzconnection==0.6.3']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)

View file

@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.location as location
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['googlemaps==2.4.4']
REQUIREMENTS = ['googlemaps==2.4.6']
_LOGGER = logging.getLogger(__name__)

View file

@ -16,7 +16,8 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, CONF_STATE, EVENT_HOMEASSISTANT_START)
CONF_NAME, CONF_ENTITY_ID, CONF_STATE, CONF_TYPE,
EVENT_HOMEASSISTANT_START)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_state_change
@ -31,15 +32,22 @@ CONF_END = 'end'
CONF_DURATION = 'duration'
CONF_PERIOD_KEYS = [CONF_START, CONF_END, CONF_DURATION]
CONF_TYPE_TIME = 'time'
CONF_TYPE_RATIO = 'ratio'
CONF_TYPE_COUNT = 'count'
CONF_TYPE_KEYS = [CONF_TYPE_TIME, CONF_TYPE_RATIO, CONF_TYPE_COUNT]
DEFAULT_NAME = 'unnamed statistics'
UNIT = 'h'
UNIT_RATIO = '%'
UNITS = {
CONF_TYPE_TIME: 'h',
CONF_TYPE_RATIO: '%',
CONF_TYPE_COUNT: ''
}
ICON = 'mdi:chart-line'
ATTR_START = 'from'
ATTR_END = 'to'
ATTR_VALUE = 'value'
ATTR_RATIO = 'ratio'
def exactly_two_period_keys(conf):
@ -62,6 +70,7 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({
vol.Optional(CONF_START, default=None): cv.template,
vol.Optional(CONF_END, default=None): cv.template,
vol.Optional(CONF_DURATION, default=None): cv.time_period,
vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}), exactly_two_period_keys)
@ -74,14 +83,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
start = config.get(CONF_START)
end = config.get(CONF_END)
duration = config.get(CONF_DURATION)
sensor_type = config.get(CONF_TYPE)
name = config.get(CONF_NAME)
for template in [start, end]:
if template is not None:
template.hass = hass
add_devices([HistoryStatsSensor(
hass, entity_id, entity_state, start, end, duration, name)])
add_devices([HistoryStatsSensor(hass, entity_id, entity_state, start, end,
duration, sensor_type, name)])
return True
@ -90,7 +100,8 @@ class HistoryStatsSensor(Entity):
"""Representation of a HistoryStats sensor."""
def __init__(
self, hass, entity_id, entity_state, start, end, duration, name):
self, hass, entity_id, entity_state, start, end, duration,
sensor_type, name):
"""Initialize the HistoryStats sensor."""
self._hass = hass
@ -99,11 +110,13 @@ class HistoryStatsSensor(Entity):
self._duration = duration
self._start = start
self._end = end
self._type = sensor_type
self._name = name
self._unit_of_measurement = UNIT
self._unit_of_measurement = UNITS[sensor_type]
self._period = (datetime.datetime.now(), datetime.datetime.now())
self.value = 0
self.count = 0
def force_refresh(*args):
"""Force the component to refresh."""
@ -123,7 +136,14 @@ class HistoryStatsSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return round(self.value, 2)
if self._type == CONF_TYPE_TIME:
return round(self.value, 2)
if self._type == CONF_TYPE_RATIO:
return HistoryStatsHelper.pretty_ratio(self.value, self._period)
if self._type == CONF_TYPE_COUNT:
return self.count
@property
def unit_of_measurement(self):
@ -142,7 +162,6 @@ class HistoryStatsSensor(Entity):
hsh = HistoryStatsHelper
return {
ATTR_VALUE: hsh.pretty_duration(self.value),
ATTR_RATIO: hsh.pretty_ratio(self.value, self._period),
ATTR_START: start.strftime('%Y-%m-%d %H:%M:%S'),
ATTR_END: end.strftime('%Y-%m-%d %H:%M:%S'),
}
@ -175,6 +194,7 @@ class HistoryStatsSensor(Entity):
last_state == self._entity_state)
last_time = dt_util.as_timestamp(start)
elapsed = 0
count = 0
# Make calculations
for item in history_list.get(self._entity_id):
@ -183,6 +203,8 @@ class HistoryStatsSensor(Entity):
if last_state:
elapsed += current_time - last_time
if current_state and not last_state:
count += 1
last_state = current_state
last_time = current_time
@ -196,6 +218,9 @@ class HistoryStatsSensor(Entity):
# Save value in hours
self.value = elapsed / 3600
# Save counter
self.count = count
def update_period(self):
"""Parse the templates and store a datetime tuple in _period."""
start = None
@ -267,10 +292,10 @@ class HistoryStatsHelper:
def pretty_ratio(value, period):
"""Format the ratio of value / period duration."""
if len(period) != 2 or period[0] == period[1]:
return '0,0' + UNIT_RATIO
return 0.0
ratio = 100 * 3600 * value / (period[1] - period[0]).total_seconds()
return str(round(ratio, 1)) + UNIT_RATIO
return round(ratio, 1)
@staticmethod
def handle_template_exception(ex, field):

View file

@ -0,0 +1,234 @@
"""
Support for the Lyft API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.lyft/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['lyft_rides==0.1.0b0']
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_END_LATITUDE = 'end_latitude'
CONF_END_LONGITUDE = 'end_longitude'
CONF_PRODUCT_IDS = 'product_ids'
CONF_START_LATITUDE = 'start_latitude'
CONF_START_LONGITUDE = 'start_longitude'
ICON = 'mdi:taxi'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Required(CONF_START_LATITUDE): cv.latitude,
vol.Required(CONF_START_LONGITUDE): cv.longitude,
vol.Optional(CONF_END_LATITUDE): cv.latitude,
vol.Optional(CONF_END_LONGITUDE): cv.longitude,
vol.Optional(CONF_PRODUCT_IDS, default=None):
vol.All(cv.ensure_list, [cv.string]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lyft sensor."""
from lyft_rides.auth import ClientCredentialGrant
auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID),
client_secret=config.get(
CONF_CLIENT_SECRET),
scopes="public",
is_sandbox_mode=False)
session = auth_flow.get_session()
wanted_product_ids = config.get(CONF_PRODUCT_IDS)
dev = []
timeandpriceest = LyftEstimate(
session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE],
config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE))
for product_id, product in timeandpriceest.products.items():
if (wanted_product_ids is not None) and \
(product_id not in wanted_product_ids):
continue
dev.append(LyftSensor('time', timeandpriceest, product_id, product))
if product.get('estimate') is not None:
dev.append(LyftSensor(
'price', timeandpriceest, product_id, product))
add_devices(dev, True)
class LyftSensor(Entity):
"""Implementation of an Lyft sensor."""
def __init__(self, sensorType, products, product_id, product):
"""Initialize the Lyft sensor."""
self.data = products
self._product_id = product_id
self._product = product
self._sensortype = sensorType
self._name = '{} {}'.format(self._product['display_name'],
self._sensortype)
if 'lyft' not in self._name.lower():
self._name = 'Lyft{}'.format(self._name)
if self._sensortype == 'time':
self._unit_of_measurement = 'min'
elif self._sensortype == 'price':
estimate = self._product['estimate']
if estimate is not None:
self._unit_of_measurement = estimate.get('currency')
self._state = None
@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 of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def device_state_attributes(self):
"""Return the state attributes."""
params = {
'Product ID': self._product['ride_type'],
'Product display name': self._product['display_name'],
'Vehicle Capacity': self._product['seats']
}
if self._product.get('pricing_details') is not None:
pricing_details = self._product['pricing_details']
params['Base price'] = pricing_details.get('base_charge')
params['Cancellation fee'] = pricing_details.get(
'cancel_penalty_amount')
params['Minimum price'] = pricing_details.get('cost_minimum')
params['Cost per mile'] = pricing_details.get('cost_per_mile')
params['Cost per minute'] = pricing_details.get('cost_per_minute')
params['Price currency code'] = pricing_details.get('currency')
params['Service fee'] = pricing_details.get('trust_and_service')
if self._product.get("estimate") is not None:
estimate = self._product['estimate']
params['Trip distance (in miles)'] = estimate.get(
'estimated_distance_miles')
params['High price estimate (in cents)'] = estimate.get(
'estimated_cost_cents_max')
params['Low price estimate (in cents)'] = estimate.get(
'estimated_cost_cents_min')
params['Trip duration (in seconds)'] = estimate.get(
'estimated_duration_seconds')
# Ignore the Prime Time percentage -- the Lyft API always
# returns 0 unless a user is logged in.
# params['Prime Time percentage'] = estimate.get(
# 'primetime_percentage')
if self._product.get("eta") is not None:
eta = self._product['eta']
params['Pickup time estimate (in seconds)'] = eta.get(
'eta_seconds')
return {k: v for k, v in params.items() if v is not None}
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
def update(self):
"""Get the latest data from the Lyft API and update the states."""
self.data.update()
try:
self._product = self.data.products[self._product_id]
except KeyError:
return
if self._sensortype == 'time':
eta = self._product['eta']
if (eta is not None) and (eta.get('is_valid_estimate')):
time_estimate = eta.get('eta_seconds', 0)
self._state = int(time_estimate / 60)
else:
self._state = 0
elif self._sensortype == 'price':
estimate = self._product['estimate']
if (estimate is not None) and \
estimate.get('is_valid_estimate'):
self._state = (int(
(estimate.get('estimated_cost_cents_min', 0) +
estimate.get('estimated_cost_cents_max', 0)) / 2) / 100)
else:
self._state = 0
class LyftEstimate(object):
"""The class for handling the time and price estimate."""
def __init__(self, session, start_latitude, start_longitude,
end_latitude=None, end_longitude=None):
"""Initialize the LyftEstimate object."""
self._session = session
self.start_latitude = start_latitude
self.start_longitude = start_longitude
self.end_latitude = end_latitude
self.end_longitude = end_longitude
self.products = None
self.__real_update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest product info and estimates from the Lyft API."""
self.__real_update()
def __real_update(self):
from lyft_rides.client import LyftRidesClient
client = LyftRidesClient(self._session)
self.products = {}
products_response = client.get_ride_types(
self.start_latitude, self.start_longitude)
products = products_response.json.get('ride_types')
for product in products:
self.products[product['ride_type']] = product
if self.end_latitude is not None and self.end_longitude is not None:
price_response = client.get_cost_estimates(
self.start_latitude, self.start_longitude,
self.end_latitude, self.end_longitude)
prices = price_response.json.get('cost_estimates', [])
for price in prices:
product = self.products[price['ride_type']]
if price.get("is_valid_estimate"):
product['estimate'] = price
eta_response = client.get_pickup_time_estimates(
self.start_latitude, self.start_longitude)
etas = eta_response.json.get('eta_estimates')
for eta in etas:
if eta.get("is_valid_estimate"):
self.products[eta['ride_type']]['eta'] = eta

View file

@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['astral==1.3.4']
REQUIREMENTS = ['astral==1.4']
_LOGGER = logging.getLogger(__name__)

View file

@ -19,12 +19,16 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_FORCE_UPDATE = 'force_update'
DEFAULT_NAME = 'MQTT Sensor'
DEFAULT_FORCE_UPDATE = False
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
})
@ -43,6 +47,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_STATE_TOPIC),
config.get(CONF_QOS),
config.get(CONF_UNIT_OF_MEASUREMENT),
config.get(CONF_FORCE_UPDATE),
value_template,
)])
@ -51,13 +56,14 @@ class MqttSensor(Entity):
"""Representation of a sensor that can be updated using MQTT."""
def __init__(self, name, state_topic, qos, unit_of_measurement,
value_template):
force_update, value_template):
"""Initialize the sensor."""
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
self._qos = qos
self._unit_of_measurement = unit_of_measurement
self._force_update = force_update
self._template = value_template
def async_added_to_hass(self):
@ -92,6 +98,11 @@ class MqttSensor(Entity):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
@property
def force_update(self):
"""Force update."""
return self._force_update
@property
def state(self):
"""Return the state of the entity."""

View file

@ -1,18 +1,21 @@
"""
Support for monitoring an Neurio hub.
Support for monitoring a Neurio energy sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.neurio_energy/
"""
import logging
from datetime import timedelta
import requests.exceptions
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_API_KEY, CONF_NAME)
from homeassistant.const import (CONF_API_KEY)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['neurio==0.3.1']
@ -21,48 +24,133 @@ _LOGGER = logging.getLogger(__name__)
CONF_API_SECRET = 'api_secret'
CONF_SENSOR_ID = 'sensor_id'
DEFAULT_NAME = 'Energy Usage'
ACTIVE_NAME = 'Energy Usage'
DAILY_NAME = 'Daily Energy Usage'
ACTIVE_TYPE = 'active'
DAILY_TYPE = 'daily'
ICON = 'mdi:flash'
MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=150)
MIN_TIME_BETWEEN_ACTIVE_UPDATES = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_SECRET): cv.string,
vol.Optional(CONF_SENSOR_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Neurio sensor."""
name = config.get(CONF_NAME)
api_key = config.get(CONF_API_KEY)
api_secret = config.get(CONF_API_SECRET)
sensor_id = config.get(CONF_SENSOR_ID)
if not sensor_id:
data = NeurioData(api_key, api_secret, sensor_id)
@Throttle(MIN_TIME_BETWEEN_DAILY_UPDATES)
def update_daily():
"""Update the daily power usage."""
data.get_daily_usage()
@Throttle(MIN_TIME_BETWEEN_ACTIVE_UPDATES)
def update_active():
"""Update the active power usage."""
data.get_active_power()
update_daily()
update_active()
# Active power sensor
add_devices([NeurioEnergy(data, ACTIVE_NAME, ACTIVE_TYPE, update_active)])
# Daily power sensor
add_devices([NeurioEnergy(data, DAILY_NAME, DAILY_TYPE, update_daily)])
class NeurioData(object):
"""Stores data retrieved from Neurio sensor."""
def __init__(self, api_key, api_secret, sensor_id):
"""Initialize the data."""
import neurio
neurio_tp = neurio.TokenProvider(key=api_key, secret=api_secret)
neurio_client = neurio.Client(token_provider=neurio_tp)
user_info = neurio_client.get_user_information()
_LOGGER.warning('Sensor ID auto-detected, set api_sensor_id: "%s"',
user_info["locations"][0]["sensors"][0]["sensorId"])
sensor_id = user_info["locations"][0]["sensors"][0]["sensorId"]
add_devices([NeurioEnergy(api_key, api_secret, name, sensor_id)])
class NeurioEnergy(Entity):
"""Implementation of an Neurio energy."""
def __init__(self, api_key, api_secret, name, sensor_id):
"""Initialize the sensor."""
self._name = name
self.api_key = api_key
self.api_secret = api_secret
self.sensor_id = sensor_id
self._daily_usage = None
self._active_power = None
self._state = None
self._unit_of_measurement = 'W'
neurio_tp = neurio.TokenProvider(key=api_key, secret=api_secret)
self.neurio_client = neurio.Client(token_provider=neurio_tp)
if not self.sensor_id:
user_info = self.neurio_client.get_user_information()
_LOGGER.warning('Sensor ID auto-detected: %s', user_info[
"locations"][0]["sensors"][0]["sensorId"])
self.sensor_id = user_info[
"locations"][0]["sensors"][0]["sensorId"]
@property
def daily_usage(self):
"""Return latest daily usage value."""
return self._daily_usage
@property
def active_power(self):
"""Return latest active power value."""
return self._active_power
def get_active_power(self):
"""Return current power value."""
try:
sample = self.neurio_client.get_samples_live_last(self.sensor_id)
self._active_power = sample['consumptionPower']
except (requests.exceptions.RequestException, ValueError, KeyError):
_LOGGER.warning('Could not update current power usage.')
return None
def get_daily_usage(self):
"""Return current daily power usage."""
kwh = 0
start_time = dt_util.start_of_local_day() \
.astimezone(dt_util.UTC).isoformat()
end_time = dt_util.utcnow().isoformat()
_LOGGER.debug('Start: %s, End: %s', start_time, end_time)
try:
history = self.neurio_client.get_samples_stats(
self.sensor_id, start_time, 'days', end_time)
except (requests.exceptions.RequestException, ValueError, KeyError):
_LOGGER.warning('Could not update daily power usage.')
return None
for result in history:
kwh += result['consumptionEnergy'] / 3600000
self._daily_usage = round(kwh, 2)
class NeurioEnergy(Entity):
"""Implementation of a Neurio energy sensor."""
def __init__(self, data, name, sensor_type, update_call):
"""Initialize the sensor."""
self._name = name
self._data = data
self._sensor_type = sensor_type
self.update_sensor = update_call
self._state = None
if sensor_type == ACTIVE_TYPE:
self._unit_of_measurement = 'W'
elif sensor_type == DAILY_TYPE:
self._unit_of_measurement = 'kWh'
@property
def name(self):
@ -85,14 +173,10 @@ class NeurioEnergy(Entity):
return ICON
def update(self):
"""Get the Neurio monitor data from the web service."""
import neurio
try:
neurio_tp = neurio.TokenProvider(
key=self.api_key, secret=self.api_secret)
neurio_client = neurio.Client(token_provider=neurio_tp)
sample = neurio_client.get_samples_live_last(
sensor_id=self.sensor_id)
self._state = sample['consumptionPower']
except (requests.exceptions.RequestException, ValueError, KeyError):
_LOGGER.warning('Could not update status for %s', self.name)
"""Get the latest data, update state."""
self.update_sensor()
if self._sensor_type == ACTIVE_TYPE:
self._state = self._data.active_power
elif self._sensor_type == DAILY_TYPE:
self._state = self._data.daily_usage

View file

@ -127,6 +127,6 @@ class OctoPrintSensor(Entity):
# Error calling the api, already logged in api.update()
return
if self._state is None:
if self._state is None and self.sensor_type != "completion":
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
return

View file

@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['psutil==5.1.3']
REQUIREMENTS = ['psutil==5.2.0']
_LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,209 @@
"""tado component to create some sensors for each zone."""
import logging
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
from homeassistant.components.tado import (
DATA_TADO)
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = ['temperature', 'humidity', 'power',
'link', 'heating', 'tado mode', 'overlay']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the sensor platform."""
# get the PyTado object from the hub component
tado = hass.data[DATA_TADO]
try:
zones = tado.get_zones()
except RuntimeError:
_LOGGER.error("Unable to get zone info from mytado")
return False
sensor_items = []
for zone in zones:
if zone['type'] == 'HEATING':
for variable in SENSOR_TYPES:
sensor_items.append(create_zone_sensor(
tado, zone, zone['name'], zone['id'],
variable))
me_data = tado.get_me()
sensor_items.append(create_device_sensor(
tado, me_data, me_data['homes'][0]['name'],
me_data['homes'][0]['id'], "tado bridge status"))
if len(sensor_items) > 0:
add_devices(sensor_items, True)
return True
else:
return False
def create_zone_sensor(tado, zone, name, zone_id, variable):
"""Create a zone sensor."""
data_id = 'zone {} {}'.format(name, zone_id)
tado.add_sensor(data_id, {
"zone": zone,
"name": name,
"id": zone_id,
"data_id": data_id
})
return TadoSensor(tado, name, zone_id, variable, data_id)
def create_device_sensor(tado, device, name, device_id, variable):
"""Create a device sensor."""
data_id = 'device {} {}'.format(name, device_id)
tado.add_sensor(data_id, {
"device": device,
"name": name,
"id": device_id,
"data_id": data_id
})
return TadoSensor(tado, name, device_id, variable, data_id)
class TadoSensor(Entity):
"""Representation of a tado Sensor."""
def __init__(self, store, zone_name, zone_id, zone_variable, data_id):
"""Initialization of TadoSensor class."""
self._store = store
self.zone_name = zone_name
self.zone_id = zone_id
self.zone_variable = zone_variable
self._unique_id = '{} {}'.format(zone_variable, zone_id)
self._data_id = data_id
self._state = None
self._state_attributes = None
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
return '{} {}'.format(self.zone_name, self.zone_variable)
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._state_attributes
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
if self.zone_variable == "temperature":
return self.hass.config.units.temperature_unit
elif self.zone_variable == "humidity":
return '%'
elif self.zone_variable == "heating":
return '%'
@property
def icon(self):
"""Icon for the sensor."""
if self.zone_variable == "temperature":
return 'mdi:thermometer'
elif self.zone_variable == "humidity":
return 'mdi:water-percent'
def update(self):
"""Update method called when should_poll is true."""
self._store.update()
data = self._store.get_data(self._data_id)
if data is None:
_LOGGER.debug('Recieved no data for zone %s',
self.zone_name)
return
unit = TEMP_CELSIUS
# pylint: disable=R0912
if self.zone_variable == 'temperature':
if 'sensorDataPoints' in data:
sensor_data = data['sensorDataPoints']
temperature = float(
sensor_data['insideTemperature']['celsius'])
self._state = self.hass.config.units.temperature(
temperature, unit)
self._state_attributes = {
"time":
sensor_data['insideTemperature']['timestamp'],
"setting": 0 # setting is used in climate device
}
# temperature setting will not exist when device is off
if 'temperature' in data['setting'] and \
data['setting']['temperature'] is not None:
temperature = float(
data['setting']['temperature']['celsius'])
self._state_attributes["setting"] = \
self.hass.config.units.temperature(
temperature, unit)
elif self.zone_variable == 'humidity':
if 'sensorDataPoints' in data:
sensor_data = data['sensorDataPoints']
self._state = float(
sensor_data['humidity']['percentage'])
self._state_attributes = {
"time": sensor_data['humidity']['timestamp'],
}
elif self.zone_variable == 'power':
if 'setting' in data:
self._state = data['setting']['power']
elif self.zone_variable == 'link':
if 'link' in data:
self._state = data['link']['state']
elif self.zone_variable == 'heating':
if 'activityDataPoints' in data:
activity_data = data['activityDataPoints']
self._state = float(
activity_data['heatingPower']['percentage'])
self._state_attributes = {
"time": activity_data['heatingPower']['timestamp'],
}
elif self.zone_variable == 'tado bridge status':
if 'connectionState' in data:
self._state = data['connectionState']['value']
elif self.zone_variable == 'tado mode':
if 'tadoMode' in data:
self._state = data['tadoMode']
elif self.zone_variable == 'overlay':
if 'overlay' in data and data['overlay'] is not None:
self._state = True
self._state_attributes = {
"termination": data['overlay']['termination']['type'],
}
else:
self._state = False
self._state_attributes = {}

View file

@ -41,11 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def decode(value):
"""Double-decode required."""
return value.encode('raw_unicode_escape').decode('utf-8')
def convert_pid(value):
"""Convert pid from hex string to integer."""
return int(value, 16)
@ -94,10 +89,10 @@ class TorqueReceiveDataView(HomeAssistantView):
if is_name:
pid = convert_pid(is_name.group(1))
names[pid] = decode(data[key])
names[pid] = data[key]
elif is_unit:
pid = convert_pid(is_unit.group(1))
units[pid] = decode(data[key])
units[pid] = data[key]
elif is_value:
pid = convert_pid(is_value.group(1))
if pid in self.sensors:
@ -110,7 +105,7 @@ class TorqueReceiveDataView(HomeAssistantView):
units.get(pid, None))
hass.async_add_job(self.add_devices, [self.sensors[pid]])
return None
return "OK!"
class TorqueSensor(Entity):

View file

@ -15,34 +15,32 @@ from homeassistant.components.zwave import async_setup_platform # noqa # pylint
_LOGGER = logging.getLogger(__name__)
def get_device(node, value, **kwargs):
def get_device(node, values, **kwargs):
"""Create zwave entity device."""
# Generic Device mappings
if value.command_class == zwave.const.COMMAND_CLASS_BATTERY:
return ZWaveSensor(value)
if node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL):
return ZWaveMultilevelSensor(value)
return ZWaveMultilevelSensor(values)
if node.has_command_class(zwave.const.COMMAND_CLASS_METER) and \
value.type == zwave.const.TYPE_DECIMAL:
return ZWaveMultilevelSensor(value)
values.primary.type == zwave.const.TYPE_DECIMAL:
return ZWaveMultilevelSensor(values)
if node.has_command_class(zwave.const.COMMAND_CLASS_ALARM) or \
node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_ALARM):
return ZWaveAlarmSensor(value)
return ZWaveAlarmSensor(values)
return None
class ZWaveSensor(zwave.ZWaveDeviceEntity):
"""Representation of a Z-Wave sensor."""
def __init__(self, value):
def __init__(self, values):
"""Initialize the sensor."""
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self.update_properties()
def update_properties(self):
"""Callback on data changes for node values."""
self._state = self._value.data
self._units = self._value.units
self._state = self.values.primary.data
self._units = self.values.primary.units
@property
def force_update(self):

Some files were not shown because too many files have changed in this diff Show more