mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
commit
1f046972d9
16
.coveragerc
16
.coveragerc
@ -59,6 +59,9 @@ omit =
|
|||||||
homeassistant/components/lutron.py
|
homeassistant/components/lutron.py
|
||||||
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
|
||||||
homeassistant/components/*/modbus.py
|
homeassistant/components/*/modbus.py
|
||||||
|
|
||||||
@ -115,9 +118,6 @@ omit =
|
|||||||
homeassistant/components/zigbee.py
|
homeassistant/components/zigbee.py
|
||||||
homeassistant/components/*/zigbee.py
|
homeassistant/components/*/zigbee.py
|
||||||
|
|
||||||
homeassistant/components/zwave/*
|
|
||||||
homeassistant/components/*/zwave.py
|
|
||||||
|
|
||||||
homeassistant/components/enocean.py
|
homeassistant/components/enocean.py
|
||||||
homeassistant/components/*/enocean.py
|
homeassistant/components/*/enocean.py
|
||||||
|
|
||||||
@ -145,6 +145,9 @@ omit =
|
|||||||
homeassistant/components/maxcube.py
|
homeassistant/components/maxcube.py
|
||||||
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/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/concord232.py
|
homeassistant/components/alarm_control_panel/concord232.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.py
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
@ -171,6 +174,7 @@ omit =
|
|||||||
homeassistant/components/climate/oem.py
|
homeassistant/components/climate/oem.py
|
||||||
homeassistant/components/climate/proliphix.py
|
homeassistant/components/climate/proliphix.py
|
||||||
homeassistant/components/climate/radiotherm.py
|
homeassistant/components/climate/radiotherm.py
|
||||||
|
homeassistant/components/config/zwave.py
|
||||||
homeassistant/components/cover/garadget.py
|
homeassistant/components/cover/garadget.py
|
||||||
homeassistant/components/cover/homematic.py
|
homeassistant/components/cover/homematic.py
|
||||||
homeassistant/components/cover/myq.py
|
homeassistant/components/cover/myq.py
|
||||||
@ -269,6 +273,7 @@ omit =
|
|||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
homeassistant/components/media_player/squeezebox.py
|
homeassistant/components/media_player/squeezebox.py
|
||||||
homeassistant/components/media_player/vlc.py
|
homeassistant/components/media_player/vlc.py
|
||||||
|
homeassistant/components/media_player/volumio.py
|
||||||
homeassistant/components/media_player/yamaha.py
|
homeassistant/components/media_player/yamaha.py
|
||||||
homeassistant/components/notify/aws_lambda.py
|
homeassistant/components/notify/aws_lambda.py
|
||||||
homeassistant/components/notify/aws_sns.py
|
homeassistant/components/notify/aws_sns.py
|
||||||
@ -327,7 +332,6 @@ omit =
|
|||||||
homeassistant/components/sensor/dovado.py
|
homeassistant/components/sensor/dovado.py
|
||||||
homeassistant/components/sensor/dte_energy_bridge.py
|
homeassistant/components/sensor/dte_energy_bridge.py
|
||||||
homeassistant/components/sensor/ebox.py
|
homeassistant/components/sensor/ebox.py
|
||||||
homeassistant/components/sensor/efergy.py
|
|
||||||
homeassistant/components/sensor/eliqonline.py
|
homeassistant/components/sensor/eliqonline.py
|
||||||
homeassistant/components/sensor/emoncms.py
|
homeassistant/components/sensor/emoncms.py
|
||||||
homeassistant/components/sensor/fastdotcom.py
|
homeassistant/components/sensor/fastdotcom.py
|
||||||
@ -352,6 +356,7 @@ omit =
|
|||||||
homeassistant/components/sensor/lastfm.py
|
homeassistant/components/sensor/lastfm.py
|
||||||
homeassistant/components/sensor/linux_battery.py
|
homeassistant/components/sensor/linux_battery.py
|
||||||
homeassistant/components/sensor/loopenergy.py
|
homeassistant/components/sensor/loopenergy.py
|
||||||
|
homeassistant/components/sensor/lyft.py
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
homeassistant/components/sensor/modem_callerid.py
|
homeassistant/components/sensor/modem_callerid.py
|
||||||
homeassistant/components/sensor/mqtt_room.py
|
homeassistant/components/sensor/mqtt_room.py
|
||||||
@ -429,6 +434,9 @@ omit =
|
|||||||
homeassistant/components/weather/openweathermap.py
|
homeassistant/components/weather/openweathermap.py
|
||||||
homeassistant/components/weather/zamg.py
|
homeassistant/components/weather/zamg.py
|
||||||
homeassistant/components/zeroconf.py
|
homeassistant/components/zeroconf.py
|
||||||
|
homeassistant/components/zwave/__init__.py
|
||||||
|
homeassistant/components/zwave/util.py
|
||||||
|
homeassistant/components/zwave/workaround.py
|
||||||
|
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
include README.rst
|
include README.rst
|
||||||
include LICENSE
|
include LICENSE.md
|
||||||
graft homeassistant
|
graft homeassistant
|
||||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||||
recursive-exclude * *.py[co]
|
recursive-exclude * *.py[co]
|
||||||
|
@ -255,10 +255,13 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
|||||||
|
|
||||||
def cmdline() -> List[str]:
|
def cmdline() -> List[str]:
|
||||||
"""Collect path and arguments to re-execute the current hass instance."""
|
"""Collect path and arguments to re-execute the current hass instance."""
|
||||||
if sys.argv[0].endswith('/__main__.py'):
|
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||||
modulepath = os.path.dirname(sys.argv[0])
|
modulepath = os.path.dirname(sys.argv[0])
|
||||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
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,
|
def setup_and_run_hass(config_dir: str,
|
||||||
|
@ -21,7 +21,6 @@ import homeassistant.loader as loader
|
|||||||
from homeassistant.util.logging import AsyncHandler
|
from homeassistant.util.logging import AsyncHandler
|
||||||
from homeassistant.util.yaml import clear_secret_cache
|
from homeassistant.util.yaml import clear_secret_cache
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import event_decorators, service
|
|
||||||
from homeassistant.helpers.signal import async_register_signal_handling
|
from homeassistant.helpers.signal import async_register_signal_handling
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -127,10 +126,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
|
|
||||||
_LOGGER.info('Home Assistant core initialized')
|
_LOGGER.info('Home Assistant core initialized')
|
||||||
|
|
||||||
# Give event decorators access to HASS
|
|
||||||
event_decorators.HASS = hass
|
|
||||||
service.HASS = hass
|
|
||||||
|
|
||||||
# stage 1
|
# stage 1
|
||||||
for component in components:
|
for component in components:
|
||||||
if component not in FIRST_INIT_COMPONENT:
|
if component not in FIRST_INIT_COMPONENT:
|
||||||
|
@ -113,7 +113,7 @@ def async_setup(hass, config):
|
|||||||
if not alarm.should_poll:
|
if not alarm.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
alarm.async_update_ha_state(True))
|
alarm.async_update_ha_state(True))
|
||||||
if hasattr(alarm, 'async_update'):
|
if hasattr(alarm, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
@ -27,7 +27,7 @@ from homeassistant.components.camera.mjpeg import (
|
|||||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||||
|
|
||||||
DOMAIN = 'android_ip_webcam'
|
DOMAIN = 'android_ip_webcam'
|
||||||
REQUIREMENTS = ["pydroid-ipcam==0.4"]
|
REQUIREMENTS = ["pydroid-ipcam==0.6"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
@ -125,11 +125,8 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
|||||||
|
|
||||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||||
|
|
||||||
CONF_AUTO_DISCOVERY = 'auto_discovery'
|
|
||||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||||
|
|
||||||
DEFAULT_AUTO_DISCOVERY = True
|
|
||||||
DEFAULT_MOTION_SENSOR = False
|
|
||||||
DEFAULT_NAME = 'IP Webcam'
|
DEFAULT_NAME = 'IP Webcam'
|
||||||
DEFAULT_PORT = 8080
|
DEFAULT_PORT = 8080
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
@ -145,14 +142,11 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
cv.time_period,
|
cv.time_period,
|
||||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||||
vol.Optional(CONF_AUTO_DISCOVERY, default=DEFAULT_AUTO_DISCOVERY):
|
vol.Optional(CONF_SWITCHES, default=None):
|
||||||
cv.boolean,
|
|
||||||
vol.Optional(CONF_SWITCHES, default=[]):
|
|
||||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
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.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||||
vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR):
|
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
|
||||||
cv.boolean,
|
|
||||||
})])
|
})])
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -184,6 +178,18 @@ def async_setup(hass, config):
|
|||||||
timeout=cam_config[CONF_TIMEOUT]
|
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
|
@asyncio.coroutine
|
||||||
def async_update_data(now):
|
def async_update_data(now):
|
||||||
"""Update data from ipcam in SCAN_INTERVAL."""
|
"""Update data from ipcam in SCAN_INTERVAL."""
|
||||||
@ -195,20 +201,6 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
yield from async_update_data(None)
|
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
|
# load platforms
|
||||||
webcams[host] = cam
|
webcams[host] = cam
|
||||||
|
|
||||||
|
@ -327,6 +327,8 @@ class APIEventForwardingView(HomeAssistantView):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Setup an event forwarder."""
|
"""Setup an event forwarder."""
|
||||||
|
_LOGGER.warning('Event forwarding is deprecated. '
|
||||||
|
'Will be removed by 0.43')
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
try:
|
try:
|
||||||
data = yield from request.json()
|
data = yield from request.json()
|
||||||
|
@ -36,6 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
dev = []
|
dev = []
|
||||||
for droplet in droplets:
|
for droplet in droplets:
|
||||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
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(
|
dev.append(DigitalOceanBinarySensor(
|
||||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||||
|
|
||||||
|
157
homeassistant/components/binary_sensor/workday.py
Normal file
157
homeassistant/components/binary_sensor/workday.py
Normal 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
|
@ -19,34 +19,34 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
|
||||||
|
|
||||||
def get_device(value, **kwargs):
|
def get_device(values, **kwargs):
|
||||||
"""Create zwave entity device."""
|
"""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:
|
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||||
# Default the multiplier to 4
|
# Default the multiplier to 4
|
||||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
re_arm_multiplier = zwave.get_config_value(values.primary.node, 9) or 4
|
||||||
return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8)
|
return ZWaveTriggerSensor(values, "motion", re_arm_multiplier * 8)
|
||||||
|
|
||||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
if workaround.get_device_component_mapping(values.primary) == DOMAIN:
|
||||||
return ZWaveBinarySensor(value, None)
|
return ZWaveBinarySensor(values, None)
|
||||||
|
|
||||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||||
return ZWaveBinarySensor(value, None)
|
return ZWaveBinarySensor(values, None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||||
"""Representation of a binary sensor within Z-Wave."""
|
"""Representation of a binary sensor within Z-Wave."""
|
||||||
|
|
||||||
def __init__(self, value, device_class):
|
def __init__(self, values, device_class):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self._sensor_type = device_class
|
self._sensor_type = device_class
|
||||||
self._state = self._value.data
|
self._state = self.values.primary.data
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Callback on data changes for node values."""
|
"""Callback on data changes for node values."""
|
||||||
self._state = self._value.data
|
self._state = self.values.primary.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
@ -58,24 +58,19 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
|||||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||||
return self._sensor_type
|
return self._sensor_type
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""No polling needed."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||||
"""Representation of a stateless sensor within Z-Wave."""
|
"""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."""
|
"""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.re_arm_sec = re_arm_sec
|
||||||
self.invalidate_after = None
|
self.invalidate_after = None
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Called when a value for this entity's node has changed."""
|
"""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
|
# only allow this value to be true for re_arm secs
|
||||||
if not self.hass:
|
if not self.hass:
|
||||||
return
|
return
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'blink'
|
DOMAIN = 'blink'
|
||||||
REQUIREMENTS = ['blinkpy==0.4.4']
|
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.util import Throttle
|
|||||||
|
|
||||||
DEPENDENCIES = ['blink']
|
DEPENDENCIES = ['blink']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -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
|
|
@ -14,7 +14,7 @@ import async_timeout
|
|||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
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 (
|
from homeassistant.components.camera import (
|
||||||
Camera, PLATFORM_SCHEMA)
|
Camera, PLATFORM_SCHEMA)
|
||||||
from homeassistant.helpers.aiohttp_client import (
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DEFAULT_NAME = 'Synology Camera'
|
DEFAULT_NAME = 'Synology Camera'
|
||||||
DEFAULT_STREAM_ID = '0'
|
DEFAULT_STREAM_ID = '0'
|
||||||
TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
CONF_CAMERA_NAME = 'camera_name'
|
CONF_CAMERA_NAME = 'camera_name'
|
||||||
CONF_STREAM_ID = 'stream_id'
|
CONF_STREAM_ID = 'stream_id'
|
||||||
|
|
||||||
@ -51,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Required(CONF_URL): 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_WHITELIST, default=[]): cv.ensure_list,
|
||||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
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):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a Synology IP Camera."""
|
"""Setup a Synology IP Camera."""
|
||||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||||
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||||
|
|
||||||
# Determine API to use for authentication
|
# 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
|
query_req = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
query_req = yield from websession_init.get(
|
query_req = yield from websession_init.get(
|
||||||
syno_api_url,
|
syno_api_url,
|
||||||
params=query_payload
|
params=query_payload
|
||||||
@ -103,7 +105,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
websession_init,
|
websession_init,
|
||||||
config.get(CONF_USERNAME),
|
config.get(CONF_USERNAME),
|
||||||
config.get(CONF_PASSWORD),
|
config.get(CONF_PASSWORD),
|
||||||
syno_auth_url
|
syno_auth_url,
|
||||||
|
timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
# init websession
|
# init websession
|
||||||
@ -120,7 +123,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
'version': '1'
|
'version': '1'
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
camera_req = yield from websession.get(
|
camera_req = yield from websession.get(
|
||||||
syno_camera_url,
|
syno_camera_url,
|
||||||
params=camera_payload
|
params=camera_payload
|
||||||
@ -149,7 +152,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
snapshot_path,
|
snapshot_path,
|
||||||
streaming_path,
|
streaming_path,
|
||||||
camera_path,
|
camera_path,
|
||||||
auth_path
|
auth_path,
|
||||||
|
timeout
|
||||||
)
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
@ -157,7 +161,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@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."""
|
"""Get a session id."""
|
||||||
auth_payload = {
|
auth_payload = {
|
||||||
'api': AUTH_API,
|
'api': AUTH_API,
|
||||||
@ -170,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url):
|
|||||||
}
|
}
|
||||||
auth_req = None
|
auth_req = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
auth_req = yield from websession.get(
|
auth_req = yield from websession.get(
|
||||||
login_url,
|
login_url,
|
||||||
params=auth_payload
|
params=auth_payload
|
||||||
@ -192,7 +196,7 @@ class SynologyCamera(Camera):
|
|||||||
|
|
||||||
def __init__(self, hass, websession, config, camera_id,
|
def __init__(self, hass, websession, config, camera_id,
|
||||||
camera_name, snapshot_path, streaming_path, camera_path,
|
camera_name, snapshot_path, streaming_path, camera_path,
|
||||||
auth_path):
|
auth_path, timeout):
|
||||||
"""Initialize a Synology Surveillance Station camera."""
|
"""Initialize a Synology Surveillance Station camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -206,6 +210,7 @@ class SynologyCamera(Camera):
|
|||||||
self._streaming_path = streaming_path
|
self._streaming_path = streaming_path
|
||||||
self._camera_path = camera_path
|
self._camera_path = camera_path
|
||||||
self._auth_path = auth_path
|
self._auth_path = auth_path
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return bytes of camera image."""
|
"""Return bytes of camera image."""
|
||||||
@ -225,7 +230,7 @@ class SynologyCamera(Camera):
|
|||||||
'cameraId': self._camera_id
|
'cameraId': self._camera_id
|
||||||
}
|
}
|
||||||
try:
|
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(
|
response = yield from self._websession.get(
|
||||||
image_url,
|
image_url,
|
||||||
params=image_payload
|
params=image_payload
|
||||||
|
@ -19,6 +19,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DEPENDENCIES = ['zoneminder']
|
DEPENDENCIES = ['zoneminder']
|
||||||
DOMAIN = 'zoneminder'
|
DOMAIN = 'zoneminder'
|
||||||
|
|
||||||
|
# From ZoneMinder's web/includes/config.php.in
|
||||||
|
ZM_STATE_ALARM = "2"
|
||||||
|
|
||||||
|
|
||||||
def _get_image_url(hass, monitor, mode):
|
def _get_image_url(hass, monitor, mode):
|
||||||
zm_data = hass.data[DOMAIN]
|
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_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
|
||||||
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
|
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:
|
if not cameras:
|
||||||
_LOGGER.warning('No active cameras found')
|
_LOGGER.warning('No active cameras found')
|
||||||
return
|
return
|
||||||
|
|
||||||
async_add_devices(cameras)
|
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
|
||||||
|
@ -224,7 +224,7 @@ def async_setup(hass, config):
|
|||||||
if not climate.should_poll:
|
if not climate.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
climate.async_update_ha_state(True))
|
climate.async_update_ha_state(True))
|
||||||
if hasattr(climate, 'async_update'):
|
if hasattr(climate, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
296
homeassistant/components/climate/tado.py
Normal file
296
homeassistant/components/climate/tado.py
Normal 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
|
@ -85,11 +85,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
|||||||
return self.vera_device.fan_cycle()
|
return self.vera_device.fan_cycle()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_power_mwh(self):
|
def current_power_w(self):
|
||||||
"""Current power usage in mWh."""
|
"""Current power usage in W."""
|
||||||
power = self.vera_device.power
|
power = self.vera_device.power
|
||||||
if power:
|
if power:
|
||||||
return convert(power, float, 0.0) * 1000
|
return convert(power, float, 0.0)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Called by the vera device callback to update state."""
|
"""Called by the vera device callback to update state."""
|
||||||
|
@ -10,7 +10,6 @@ import logging
|
|||||||
from homeassistant.components.climate import DOMAIN
|
from homeassistant.components.climate import DOMAIN
|
||||||
from homeassistant.components.climate import ClimateDevice
|
from homeassistant.components.climate import ClimateDevice
|
||||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
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.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
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."""
|
"""Create zwave entity device."""
|
||||||
temp_unit = hass.config.units.temperature_unit
|
temp_unit = hass.config.units.temperature_unit
|
||||||
return ZWaveClimate(value, temp_unit)
|
return ZWaveClimate(values, temp_unit)
|
||||||
|
|
||||||
|
|
||||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||||
"""Representation of a Z-Wave Climate device."""
|
"""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."""
|
"""Initialize the Z-Wave climate device."""
|
||||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self._index = value.index
|
|
||||||
self._node = value.node
|
|
||||||
self._target_temperature = None
|
self._target_temperature = None
|
||||||
self._current_temperature = None
|
self._current_temperature = None
|
||||||
self._current_operation = None
|
self._current_operation = None
|
||||||
@ -61,10 +58,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||||
self._zxt_120 = None
|
self._zxt_120 = None
|
||||||
# Make sure that we have values for the key before converting to int
|
# Make sure that we have values for the key before converting to int
|
||||||
if (value.node.manufacturer_id.strip() and
|
if (self.node.manufacturer_id.strip() and
|
||||||
value.node.product_id.strip()):
|
self.node.product_id.strip()):
|
||||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
specific_sensor_key = (
|
||||||
int(value.node.product_id, 16))
|
int(self.node.manufacturer_id, 16),
|
||||||
|
int(self.node.product_id, 16))
|
||||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
||||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||||
@ -75,33 +73,25 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Callback on data changes for node values."""
|
"""Callback on data changes for node values."""
|
||||||
# Operation Mode
|
# Operation Mode
|
||||||
self._current_operation = self.get_value(
|
if self.values.mode:
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
|
self._current_operation = self.values.mode.data
|
||||||
operation_list = self.get_value(
|
operation_list = self.values.mode.data_items
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
|
||||||
member='data_items')
|
|
||||||
if operation_list:
|
if operation_list:
|
||||||
self._operation_list = list(operation_list)
|
self._operation_list = list(operation_list)
|
||||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||||
|
|
||||||
# Current Temp
|
# Current Temp
|
||||||
self._current_temperature = self.get_value(
|
if self.values.temperature:
|
||||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
self._current_temperature = self.values.temperature.data
|
||||||
label=['Temperature'], member='data')
|
device_unit = self.values.temperature.units
|
||||||
device_unit = self.get_value(
|
|
||||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
|
||||||
label=['Temperature'], member='units')
|
|
||||||
if device_unit is not None:
|
if device_unit is not None:
|
||||||
self._unit = device_unit
|
self._unit = device_unit
|
||||||
|
|
||||||
# Fan Mode
|
# Fan Mode
|
||||||
self._current_fan_mode = self.get_value(
|
if self.values.fan_mode:
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
self._current_fan_mode = self.values.fan_mode.data
|
||||||
member='data')
|
fan_list = self.values.fan_mode.data_items
|
||||||
fan_list = self.get_value(
|
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
|
||||||
member='data_items')
|
|
||||||
if fan_list:
|
if fan_list:
|
||||||
self._fan_list = list(fan_list)
|
self._fan_list = list(fan_list)
|
||||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||||
@ -109,52 +99,32 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
self._current_fan_mode)
|
self._current_fan_mode)
|
||||||
# Swing mode
|
# Swing mode
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
self._current_swing_mode = (
|
if self.values.zxt_120_swing_mode:
|
||||||
self.get_value(
|
self._current_swing_mode = self.values.zxt_120_swing_mode.data
|
||||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
swing_list = self.values.zxt_120_swing_mode.data_items
|
||||||
index=33,
|
|
||||||
member='data'))
|
|
||||||
swing_list = self.get_value(class_id=zwave.const
|
|
||||||
.COMMAND_CLASS_CONFIGURATION,
|
|
||||||
index=33,
|
|
||||||
member='data_items')
|
|
||||||
if swing_list:
|
if swing_list:
|
||||||
self._swing_list = list(swing_list)
|
self._swing_list = list(swing_list)
|
||||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||||
_LOGGER.debug("self._current_swing_mode=%s",
|
_LOGGER.debug("self._current_swing_mode=%s",
|
||||||
self._current_swing_mode)
|
self._current_swing_mode)
|
||||||
# Set point
|
# Set point
|
||||||
temps = []
|
if self.values.primary.data == 0:
|
||||||
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 "
|
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||||
"current_temperature=%s",
|
"current_temperature=%s",
|
||||||
self._current_temperature)
|
self._current_temperature)
|
||||||
self._target_temperature = (
|
self._target_temperature = (
|
||||||
round((float(self._current_temperature)), 1))
|
round((float(self._current_temperature)), 1))
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
self._target_temperature = round((float(value.data)), 1)
|
self._target_temperature = round(
|
||||||
|
(float(self.values.primary.data)), 1)
|
||||||
|
|
||||||
# Operating state
|
# Operating state
|
||||||
self._operating_state = self.get_value(
|
if self.values.operating_state:
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
self._operating_state = self.values.operating_state.data
|
||||||
member='data')
|
|
||||||
|
|
||||||
# Fan operating state
|
# Fan operating state
|
||||||
self._fan_state = self.get_value(
|
if self.values.fan_state:
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
self._fan_state = self.values.fan_state.data
|
||||||
member='data')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""No polling on Z-Wave."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_fan_mode(self):
|
def current_fan_mode(self):
|
||||||
@ -213,41 +183,31 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.set_value(
|
self.values.primary.data = temperature
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
|
||||||
index=self._index, data=temperature)
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
def set_fan_mode(self, fan):
|
def set_fan_mode(self, fan):
|
||||||
"""Set new target fan mode."""
|
"""Set new target fan mode."""
|
||||||
self.set_value(
|
if self.values.fan_mode:
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
self.values.fan_mode.data = bytes(fan, 'utf-8')
|
||||||
index=0, data=bytes(fan, 'utf-8'))
|
|
||||||
|
|
||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
"""Set new target operation mode."""
|
"""Set new target operation mode."""
|
||||||
self.set_value(
|
if self.values.mode:
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
self.values.mode.data = bytes(operation_mode, 'utf-8')
|
||||||
index=0, data=bytes(operation_mode, 'utf-8'))
|
|
||||||
|
|
||||||
def set_swing_mode(self, swing_mode):
|
def set_swing_mode(self, swing_mode):
|
||||||
"""Set new target swing mode."""
|
"""Set new target swing mode."""
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
self.set_value(
|
if self.values.zxt_120_swing_mode:
|
||||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
self.values.zxt_120_swing_mode.data = bytes(
|
||||||
index=33, data=bytes(swing_mode, 'utf-8'))
|
swing_mode, 'utf-8')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the device specific state attributes."""
|
"""Return the device specific state attributes."""
|
||||||
data = super().device_state_attributes
|
data = super().device_state_attributes
|
||||||
if self._operating_state:
|
if self._operating_state:
|
||||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
data[ATTR_OPERATING_STATE] = self._operating_state
|
||||||
if self._fan_state:
|
if self._fan_state:
|
||||||
data[ATTR_FAN_STATE] = self._fan_state
|
data[ATTR_FAN_STATE] = self._fan_state
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
|
||||||
def dependent_value_ids(self):
|
|
||||||
"""List of value IDs a device depends on."""
|
|
||||||
return None
|
|
||||||
|
@ -166,7 +166,7 @@ def async_setup(hass, config):
|
|||||||
if not cover.should_poll:
|
if not cover.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
cover.async_update_ha_state(True))
|
cover.async_update_ha_state(True))
|
||||||
if hasattr(cover, 'async_update'):
|
if hasattr(cover, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
@ -14,8 +14,8 @@ from homeassistant.const import (
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/arraylabs/pymyq/archive/v0.0.7.zip'
|
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||||
'#pymyq==0.0.7']
|
'#pymyq==0.0.8']
|
||||||
|
|
||||||
COVER_SCHEMA = vol.Schema({
|
COVER_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_TYPE): cv.string,
|
vol.Required(CONF_TYPE): cv.string,
|
||||||
|
@ -20,64 +20,49 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||||
|
|
||||||
|
|
||||||
def get_device(value, **kwargs):
|
def get_device(values, **kwargs):
|
||||||
"""Create zwave entity device."""
|
"""Create zwave entity device."""
|
||||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
if (values.primary.command_class ==
|
||||||
and value.index == 0):
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||||
return ZwaveRollershutter(value)
|
and values.primary.index == 0):
|
||||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
return ZwaveRollershutter(values)
|
||||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
elif (values.primary.command_class in [
|
||||||
return ZwaveGarageDoor(value)
|
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||||
|
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||||
|
return ZwaveGarageDoor(values)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||||
"""Representation of an Zwave roller shutter."""
|
"""Representation of an Zwave roller shutter."""
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, values):
|
||||||
"""Initialize the zwave rollershutter."""
|
"""Initialize the zwave rollershutter."""
|
||||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
self._node = value.node
|
|
||||||
self._open_id = None
|
self._open_id = None
|
||||||
self._close_id = None
|
self._close_id = None
|
||||||
self._current_position_id = None
|
|
||||||
self._current_position = None
|
self._current_position = None
|
||||||
|
|
||||||
self._workaround = workaround.get_device_mapping(value)
|
self._workaround = workaround.get_device_mapping(values.primary)
|
||||||
if self._workaround:
|
if self._workaround:
|
||||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||||
self.update_properties()
|
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):
|
def update_properties(self):
|
||||||
"""Callback on data changes for node values."""
|
"""Callback on data changes for node values."""
|
||||||
# Position value
|
# Position value
|
||||||
if not self._node.is_ready:
|
self._current_position = self.values.primary.data
|
||||||
if self._current_position_id is None:
|
|
||||||
self._current_position_id = self.get_value(
|
if self.values.open and self.values.close and \
|
||||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
self._open_id is None and self._close_id is None:
|
||||||
label=['Level'], member='value_id')
|
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||||
if self._open_id is None:
|
self._open_id = self.values.close.value_id
|
||||||
self._open_id = self.get_value(
|
self._close_id = self.values.open.value_id
|
||||||
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._workaround = None
|
||||||
self._current_position = self._node.get_dimmer_level(
|
else:
|
||||||
self._current_position_id)
|
self._open_id = self.values.open.value_id
|
||||||
|
self._close_id = self.values.close.value_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
@ -112,7 +97,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
|
|
||||||
def set_cover_position(self, position, **kwargs):
|
def set_cover_position(self, position, **kwargs):
|
||||||
"""Move the roller shutter to a specific position."""
|
"""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):
|
def stop_cover(self, **kwargs):
|
||||||
"""Stop the roller shutter."""
|
"""Stop the roller shutter."""
|
||||||
@ -122,14 +107,14 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||||
"""Representation of an Zwave garage door device."""
|
"""Representation of an Zwave garage door device."""
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, values):
|
||||||
"""Initialize the zwave garage door."""
|
"""Initialize the zwave garage door."""
|
||||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Callback on data changes for node values."""
|
"""Callback on data changes for node values."""
|
||||||
self._state = self._value.data
|
self._state = self.values.primary.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
@ -138,11 +123,11 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
|
|
||||||
def close_cover(self):
|
def close_cover(self):
|
||||||
"""Close the garage door."""
|
"""Close the garage door."""
|
||||||
self._value.data = False
|
self.values.primary.data = False
|
||||||
|
|
||||||
def open_cover(self):
|
def open_cover(self):
|
||||||
"""Open the garage door."""
|
"""Open the garage door."""
|
||||||
self._value.data = True
|
self.values.primary.data = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
|
|||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.util import Throttle
|
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.
|
# Return cached results if last scan was less then this time ago.
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||||
|
@ -14,6 +14,7 @@ import requests
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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):
|
def get_scanner(hass, config):
|
||||||
"""Validate the configuration and return a Luci scanner."""
|
"""Validate the configuration and return a Luci scanner."""
|
||||||
scanner = LuciDeviceScanner(config[DOMAIN])
|
scanner = LuciDeviceScanner(config[DOMAIN])
|
||||||
@ -46,8 +53,9 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
self.username = config[CONF_USERNAME]
|
||||||
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||||
|
|
||||||
@ -55,12 +63,15 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
self.token = _get_token(host, username, password)
|
self.refresh_token()
|
||||||
self.host = host
|
|
||||||
|
|
||||||
self.mac2name = None
|
self.mac2name = None
|
||||||
self.success_init = self.token is not 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):
|
def scan_devices(self):
|
||||||
"""Scan for new devices and return a list with found device IDs."""
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
self._update_info()
|
self._update_info()
|
||||||
@ -98,8 +109,15 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
_LOGGER.info('Checking ARP')
|
_LOGGER.info('Checking ARP')
|
||||||
|
|
||||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||||
|
|
||||||
|
try:
|
||||||
result = _req_json_rpc(url, 'net.arptable',
|
result = _req_json_rpc(url, 'net.arptable',
|
||||||
params={'auth': self.token})
|
params={'auth': self.token})
|
||||||
|
except InvalidLuciTokenError:
|
||||||
|
_LOGGER.info('Refreshing token')
|
||||||
|
self.refresh_token()
|
||||||
|
return False
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
for device_entry in result:
|
for device_entry in result:
|
||||||
@ -116,6 +134,7 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
def _req_json_rpc(url, method, *args, **kwargs):
|
def _req_json_rpc(url, method, *args, **kwargs):
|
||||||
"""Perform one JSON RPC operation."""
|
"""Perform one JSON RPC operation."""
|
||||||
data = json.dumps({'method': method, 'params': args})
|
data = json.dumps({'method': method, 'params': args})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
@ -139,6 +158,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
|
|||||||
"Failed to authenticate, "
|
"Failed to authenticate, "
|
||||||
"please check your username and password")
|
"please check your username and password")
|
||||||
return
|
return
|
||||||
|
elif res.status_code == 403:
|
||||||
|
_LOGGER.error('Luci responded with a 403 Invalid token')
|
||||||
|
raise InvalidLuciTokenError
|
||||||
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.error('Invalid response from luci: %s', res)
|
_LOGGER.error('Invalid response from luci: %s', res)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
|||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['python-digitalocean==1.10.1']
|
REQUIREMENTS = ['python-digitalocean==1.11']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -218,7 +218,7 @@ def async_setup(hass, config: dict):
|
|||||||
if not fan.should_poll:
|
if not fan.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
fan.async_update_ha_state(True))
|
fan.async_update_ha_state(True))
|
||||||
if hasattr(fan, 'async_update'):
|
if hasattr(fan, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
window.Polymer = {
|
window.Polymer = {
|
||||||
lazyRegister: true,
|
lazyRegister: true,
|
||||||
useNativeCSSProperties: true,
|
useNativeCSSProperties: true,
|
||||||
dom: 'shady',
|
dom: 'shadow',
|
||||||
suppressTemplateNotifications: true,
|
suppressTemplateNotifications: true,
|
||||||
suppressBindingNotifications: true,
|
suppressBindingNotifications: true,
|
||||||
};
|
};
|
||||||
|
@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||||
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
|
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||||
"frontend.html": "418f6ef8354ce71f1b9594ee2068ebef",
|
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f",
|
||||||
"mdi.html": "65413cdf82f822bd6480e577852f0292",
|
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
|
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
||||||
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
|
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
||||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||||
"panels/ha-panel-dev-service.html": "153aad076f98bbd626466bac50986874",
|
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
||||||
"panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
|
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
||||||
"panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
|
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
|
||||||
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
|
"panels/ha-panel-history.html": "bfd5f929d5aa9cefdd799ec37624efa1",
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2",
|
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
||||||
"panels/ha-panel-map.html": "e10704a3469e44d1714eac9ed8e4b6a0",
|
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5",
|
||||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
|||||||
Subproject commit de1b20b70a16aeb7c48a1b4867c97864c88adb1c
|
Subproject commit f4c59e1eff3223262c198a29cf70c62572de019b
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@ -85,7 +85,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
influx = InfluxDBClient(**kwargs)
|
influx = InfluxDBClient(**kwargs)
|
||||||
influx.query("SHOW DIAGNOSTICS;")
|
influx.query("SHOW DIAGNOSTICS;", database=conf[CONF_DB_NAME])
|
||||||
except exceptions.InfluxDBClientError as exc:
|
except exceptions.InfluxDBClientError as exc:
|
||||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||||
"check your entries in the configuration file and that "
|
"check your entries in the configuration file and that "
|
||||||
|
@ -255,7 +255,7 @@ def async_setup(hass, config):
|
|||||||
if not light.should_poll:
|
if not light.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
light.async_update_ha_state(True))
|
light.async_update_ha_state(True))
|
||||||
if hasattr(light, 'async_update'):
|
if hasattr(light, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
@ -6,6 +6,9 @@ https://home-assistant.io/components/light.lifx/
|
|||||||
"""
|
"""
|
||||||
import colorsys
|
import colorsys
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -13,117 +16,91 @@ from homeassistant.components.light import (
|
|||||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||||
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||||
from homeassistant.helpers.event import track_time_change
|
|
||||||
from homeassistant.util.color import (
|
from homeassistant.util.color import (
|
||||||
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
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
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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'
|
CONF_SERVER = 'server'
|
||||||
|
|
||||||
|
BYTE_MAX = 255
|
||||||
SHORT_MAX = 65535
|
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_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
||||||
SUPPORT_TRANSITION)
|
SUPPORT_TRANSITION)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_SERVER, default=None): cv.string,
|
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
||||||
vol.Optional(CONF_BROADCAST, default=None): cv.string,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# 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."""
|
"""Setup the LIFX platform."""
|
||||||
|
import aiolifx
|
||||||
|
|
||||||
server_addr = config.get(CONF_SERVER)
|
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
|
coro = hass.loop.create_datagram_endpoint(
|
||||||
track_time_change(hass, lifx_library.poll, second=[10, 40])
|
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):
|
class LIFXManager(object):
|
||||||
"""Representation of a LIFX light."""
|
"""Representation of all known LIFX entities."""
|
||||||
|
|
||||||
def __init__(self, add_devices_callback, server_addr=None,
|
def __init__(self, hass, async_add_devices):
|
||||||
broadcast_addr=None):
|
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
import liffylights
|
self.entities = {}
|
||||||
|
self.hass = hass
|
||||||
|
self.async_add_devices = async_add_devices
|
||||||
|
|
||||||
self._devices = []
|
@callback
|
||||||
|
def register(self, device):
|
||||||
self._add_devices_callback = add_devices_callback
|
"""Callback for newly detected bulb."""
|
||||||
|
if device.mac_addr in self.entities:
|
||||||
self._liffylights = liffylights.LiffyLights(
|
entity = self.entities[device.mac_addr]
|
||||||
self.on_device, self.on_power, self.on_color, server_addr,
|
_LOGGER.debug("%s register AGAIN", entity.ipaddr)
|
||||||
broadcast_addr)
|
entity.available = True
|
||||||
|
self.hass.async_add_job(entity.async_update_ha_state())
|
||||||
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])
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("update bulb %s %s %d %d %d %d %d",
|
_LOGGER.debug("%s register NEW", device.ip_addr)
|
||||||
ipaddr, name, power, hue, sat, bri, kel)
|
device.get_color(self.ready)
|
||||||
bulb.set_power(power)
|
|
||||||
bulb.set_color(hue, sat, bri, kel)
|
|
||||||
bulb.schedule_update_ha_state()
|
|
||||||
|
|
||||||
def on_color(self, ipaddr, hue, sat, bri, kel):
|
@callback
|
||||||
"""Initialize the light."""
|
def ready(self, device, msg):
|
||||||
bulb = self.find_bulb(ipaddr)
|
"""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:
|
@callback
|
||||||
bulb.set_color(hue, sat, bri, kel)
|
def unregister(self, device):
|
||||||
bulb.schedule_update_ha_state()
|
"""Callback for disappearing bulb."""
|
||||||
|
if device.mac_addr in self.entities:
|
||||||
def on_power(self, ipaddr, power):
|
entity = self.entities[device.mac_addr]
|
||||||
"""Initialize the light."""
|
_LOGGER.debug("%s unregister", entity.ipaddr)
|
||||||
bulb = self.find_bulb(ipaddr)
|
entity.available = False
|
||||||
|
entity.updated_event.set()
|
||||||
if bulb is not None:
|
self.hass.async_add_job(entity.async_update_ha_state())
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_rgb_to_hsv(rgb):
|
def convert_rgb_to_hsv(rgb):
|
||||||
@ -140,31 +117,35 @@ def convert_rgb_to_hsv(rgb):
|
|||||||
class LIFXLight(Light):
|
class LIFXLight(Light):
|
||||||
"""Representation of a LIFX light."""
|
"""Representation of a LIFX light."""
|
||||||
|
|
||||||
def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness,
|
def __init__(self, device):
|
||||||
kelvin):
|
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
_LOGGER.debug("LIFXLight: %s %s", ipaddr, name)
|
self.device = device
|
||||||
|
self.updated_event = asyncio.Event()
|
||||||
self._liffylights = liffy
|
self.blocker = None
|
||||||
self._ip = ipaddr
|
self.postponed_update = None
|
||||||
self.set_name(name)
|
self._available = True
|
||||||
self.set_power(power)
|
self.set_power(device.power_level)
|
||||||
self.set_color(hue, saturation, brightness, kelvin)
|
self.set_color(*device.color)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def available(self):
|
||||||
"""No polling needed for LIFX light."""
|
"""Return the availability of the device."""
|
||||||
return False
|
return self._available
|
||||||
|
|
||||||
|
@available.setter
|
||||||
|
def available(self, value):
|
||||||
|
"""Set the availability of the device."""
|
||||||
|
self._available = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return self._name
|
return self.device.label
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ipaddr(self):
|
def ipaddr(self):
|
||||||
"""Return the IP address of the device."""
|
"""Return the IP address of the device."""
|
||||||
return self._ip
|
return self.device.ip_addr[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rgb_color(self):
|
def rgb_color(self):
|
||||||
@ -199,16 +180,47 @@ class LIFXLight(Light):
|
|||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
return SUPPORT_LIFX
|
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."""
|
"""Turn the device on."""
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||||
else:
|
else:
|
||||||
fade = 0
|
fade = 0
|
||||||
|
|
||||||
|
changed_color = False
|
||||||
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
hue, saturation, brightness = \
|
hue, saturation, brightness = \
|
||||||
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||||
|
changed_color = True
|
||||||
else:
|
else:
|
||||||
hue = self._hue
|
hue = self._hue
|
||||||
saturation = self._sat
|
saturation = self._sat
|
||||||
@ -216,40 +228,64 @@ class LIFXLight(Light):
|
|||||||
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||||
|
changed_color = True
|
||||||
else:
|
else:
|
||||||
brightness = self._bri
|
brightness = self._bri
|
||||||
|
|
||||||
if ATTR_COLOR_TEMP in kwargs:
|
if ATTR_COLOR_TEMP in kwargs:
|
||||||
kelvin = int(color_temperature_mired_to_kelvin(
|
kelvin = int(color_temperature_mired_to_kelvin(
|
||||||
kwargs[ATTR_COLOR_TEMP]))
|
kwargs[ATTR_COLOR_TEMP]))
|
||||||
|
changed_color = True
|
||||||
else:
|
else:
|
||||||
kelvin = self._kel
|
kelvin = self._kel
|
||||||
|
|
||||||
|
hsbk = [hue, saturation, brightness, kelvin]
|
||||||
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||||
self._ip, self._power,
|
self.ipaddr, self._power, fade, *hsbk)
|
||||||
hue, saturation, brightness, kelvin, fade)
|
|
||||||
|
|
||||||
if self._power == 0:
|
if self._power == 0:
|
||||||
self._liffylights.set_color(self._ip, hue, saturation,
|
if changed_color:
|
||||||
brightness, kelvin, 0)
|
self.device.set_color(hsbk, None, 0)
|
||||||
self._liffylights.set_power(self._ip, 65535, fade)
|
self.device.set_power(True, None, fade)
|
||||||
else:
|
else:
|
||||||
self._liffylights.set_color(self._ip, hue, saturation,
|
self.device.set_power(True, None, 0) # racing for power status
|
||||||
brightness, kelvin, fade)
|
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."""
|
"""Turn the device off."""
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||||
else:
|
else:
|
||||||
fade = 0
|
fade = 0
|
||||||
|
|
||||||
_LOGGER.debug("turn_off: %s %d", self._ip, fade)
|
self.device.set_power(False, None, fade)
|
||||||
self._liffylights.set_power(self._ip, 0, fade)
|
|
||||||
|
|
||||||
def set_name(self, name):
|
self.update_later(fade)
|
||||||
"""Set name of the light."""
|
if fade < BULB_LATENCY:
|
||||||
self._name = name
|
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):
|
def set_power(self, power):
|
||||||
"""Set power state value."""
|
"""Set power state value."""
|
||||||
|
64
homeassistant/components/light/lutron_caseta.py
Normal file
64
homeassistant/components/light/lutron_caseta.py
Normal 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)
|
@ -14,8 +14,8 @@ from homeassistant.components.rflink import (
|
|||||||
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
||||||
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN,
|
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN,
|
||||||
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
|
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']
|
DEPENDENCIES = ['rflink']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
TYPE_DIMMABLE = 'dimmable'
|
TYPE_DIMMABLE = 'dimmable'
|
||||||
TYPE_SWITCHABLE = 'switchable'
|
TYPE_SWITCHABLE = 'switchable'
|
||||||
TYPE_HYBRID = 'hybrid'
|
TYPE_HYBRID = 'hybrid'
|
||||||
|
TYPE_TOGGLE = 'toggle'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema({
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): DOMAIN,
|
vol.Required(CONF_PLATFORM): DOMAIN,
|
||||||
@ -33,7 +34,8 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||||||
cv.string: {
|
cv.string: {
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_TYPE):
|
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.Optional(CONF_ALIASSES, default=[]):
|
||||||
vol.All(cv.ensure_list, [cv.string]),
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
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
|
# sends 'dim' and 'on' command to support both dimmers and on/off
|
||||||
# switches. Not compatible with signal repetition.
|
# switches. Not compatible with signal repetition.
|
||||||
TYPE_HYBRID: HybridRflinkLight,
|
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)
|
return entity_device_mapping.get(entity_type, RflinkLight)
|
||||||
@ -213,3 +218,38 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light):
|
|||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
return SUPPORT_BRIGHTNESS
|
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')
|
||||||
|
@ -18,6 +18,8 @@ DEPENDENCIES = ['wink']
|
|||||||
|
|
||||||
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
||||||
|
|
||||||
|
RGB_MODES = ['hsb', 'rgb']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Wink lights."""
|
"""Setup the Wink lights."""
|
||||||
@ -54,6 +56,8 @@ class WinkLight(WinkDevice, Light):
|
|||||||
"""Current bulb color in RGB."""
|
"""Current bulb color in RGB."""
|
||||||
if not self.wink.supports_hue_saturation():
|
if not self.wink.supports_hue_saturation():
|
||||||
return None
|
return None
|
||||||
|
elif self.wink.color_model() not in RGB_MODES:
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
hue = self.wink.color_hue()
|
hue = self.wink.color_hue()
|
||||||
saturation = self.wink.color_saturation()
|
saturation = self.wink.color_saturation()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).
|
Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
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 logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -49,9 +49,9 @@ SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
|
|||||||
| SUPPORT_COLOR_TEMP)
|
| SUPPORT_COLOR_TEMP)
|
||||||
|
|
||||||
|
|
||||||
def get_device(node, value, node_config, **kwargs):
|
def get_device(node, values, node_config, **kwargs):
|
||||||
"""Create zwave entity device."""
|
"""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)
|
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
|
||||||
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
||||||
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
_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)
|
refresh, delay)
|
||||||
|
|
||||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
||||||
return ZwaveColorLight(value, refresh, delay)
|
return ZwaveColorLight(values, refresh, delay)
|
||||||
else:
|
else:
|
||||||
return ZwaveDimmer(value, refresh, delay)
|
return ZwaveDimmer(values, refresh, delay)
|
||||||
|
|
||||||
|
|
||||||
def brightness_state(value):
|
def brightness_state(value):
|
||||||
@ -75,9 +75,9 @@ def brightness_state(value):
|
|||||||
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
"""Representation of a Z-Wave dimmer."""
|
"""Representation of a Z-Wave dimmer."""
|
||||||
|
|
||||||
def __init__(self, value, refresh, delay):
|
def __init__(self, values, refresh, delay):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self._brightness = None
|
self._brightness = None
|
||||||
self._state = None
|
self._state = None
|
||||||
self._delay = delay
|
self._delay = delay
|
||||||
@ -86,10 +86,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
|
|
||||||
# Enable appropriate workaround flags for our device
|
# Enable appropriate workaround flags for our device
|
||||||
# Make sure that we have values for the key before converting to int
|
# Make sure that we have values for the key before converting to int
|
||||||
if (value.node.manufacturer_id.strip() and
|
if (self.node.manufacturer_id.strip() and
|
||||||
value.node.product_id.strip()):
|
self.node.product_id.strip()):
|
||||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
specific_sensor_key = (int(self.node.manufacturer_id, 16),
|
||||||
int(value.node.product_id, 16))
|
int(self.node.product_id, 16))
|
||||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
||||||
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
||||||
@ -105,7 +105,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Update internal properties based on zwave values."""
|
"""Update internal properties based on zwave values."""
|
||||||
# Brightness
|
# Brightness
|
||||||
self._brightness, self._state = brightness_state(self._value)
|
self._brightness, self._state = brightness_state(self.values.primary)
|
||||||
|
|
||||||
def value_changed(self):
|
def value_changed(self):
|
||||||
"""Called when a value for this entity's node has changed."""
|
"""Called when a value for this entity's node has changed."""
|
||||||
@ -116,7 +116,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
def _refresh_value():
|
def _refresh_value():
|
||||||
"""Used timer callback for delayed value refresh."""
|
"""Used timer callback for delayed value refresh."""
|
||||||
self._refreshing = True
|
self._refreshing = True
|
||||||
self._value.refresh()
|
self.values.primary.refresh()
|
||||||
|
|
||||||
if self._timer is not None and self._timer.isAlive():
|
if self._timer is not None and self._timer.isAlive():
|
||||||
self._timer.cancel()
|
self._timer.cancel()
|
||||||
@ -151,12 +151,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
else:
|
else:
|
||||||
brightness = 255
|
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
|
self._state = STATE_ON
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""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
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
@ -170,73 +170,28 @@ def ct_to_rgb(temp):
|
|||||||
class ZwaveColorLight(ZwaveDimmer):
|
class ZwaveColorLight(ZwaveDimmer):
|
||||||
"""Representation of a Z-Wave color changing light."""
|
"""Representation of a Z-Wave color changing light."""
|
||||||
|
|
||||||
def __init__(self, value, refresh, delay):
|
def __init__(self, values, refresh, delay):
|
||||||
"""Initialize the light."""
|
"""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._color_channels = None
|
||||||
self._rgb = None
|
self._rgb = None
|
||||||
self._ct = None
|
self._ct = None
|
||||||
|
|
||||||
super().__init__(value, refresh, delay)
|
super().__init__(values, 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()
|
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Update internal properties based on zwave values."""
|
"""Update internal properties based on zwave values."""
|
||||||
super().update_properties()
|
super().update_properties()
|
||||||
|
|
||||||
if self._value_color is None:
|
if self.values.color is None:
|
||||||
return
|
return
|
||||||
if self._value_color_channels is None:
|
if self.values.color_channels is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Color Channels
|
# Color Channels
|
||||||
self._color_channels = self._value_color_channels.data
|
self._color_channels = self.values.color_channels.data
|
||||||
|
|
||||||
# Color Data String
|
# Color Data String
|
||||||
data = self._value_color.data
|
data = self.values.color.data
|
||||||
|
|
||||||
# RGB is always present in the openzwave color data string.
|
# RGB is always present in the openzwave color data string.
|
||||||
self._rgb = [
|
self._rgb = [
|
||||||
@ -309,10 +264,10 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||||||
if self._zw098:
|
if self._zw098:
|
||||||
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
|
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
|
||||||
self._ct = TEMP_WARM_HASS
|
self._ct = TEMP_WARM_HASS
|
||||||
rgbw = b'#000000FF00'
|
rgbw = b'#000000ff00'
|
||||||
else:
|
else:
|
||||||
self._ct = TEMP_COLD_HASS
|
self._ct = TEMP_COLD_HASS
|
||||||
rgbw = b'#00000000FF'
|
rgbw = b'#00000000ff'
|
||||||
|
|
||||||
elif ATTR_RGB_COLOR in kwargs:
|
elif ATTR_RGB_COLOR in kwargs:
|
||||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||||
@ -329,8 +284,8 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||||||
rgbw += format(colorval, '02x').encode('utf-8')
|
rgbw += format(colorval, '02x').encode('utf-8')
|
||||||
rgbw += b'0000'
|
rgbw += b'0000'
|
||||||
|
|
||||||
if rgbw and self._value_color:
|
if rgbw and self.values.color:
|
||||||
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
|
self.values.color.data = rgbw
|
||||||
|
|
||||||
super().turn_on(**kwargs)
|
super().turn_on(**kwargs)
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ def async_setup(hass, config):
|
|||||||
if not entity.should_poll:
|
if not entity.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
entity.async_update_ha_state(True))
|
entity.async_update_ha_state(True))
|
||||||
if hasattr(entity, 'async_update'):
|
if hasattr(entity, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/lock.zwave/
|
|||||||
"""
|
"""
|
||||||
# Because we do not compile openzwave on CI
|
# Because we do not compile openzwave on CI
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
@ -13,7 +14,6 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.lock import DOMAIN, LockDevice
|
from homeassistant.components.lock import DOMAIN, LockDevice
|
||||||
from homeassistant.components import zwave
|
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
|
from homeassistant.config import load_yaml_config_file
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -120,8 +120,12 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_device(hass, node, value, **kwargs):
|
@asyncio.coroutine
|
||||||
"""Create zwave entity device."""
|
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(
|
descriptions = load_yaml_config_file(
|
||||||
path.join(path.dirname(__file__), 'services.yaml'))
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
@ -140,6 +144,7 @@ def get_device(hass, node, value, **kwargs):
|
|||||||
_LOGGER.error('Invalid code provided: (%s)'
|
_LOGGER.error('Invalid code provided: (%s)'
|
||||||
' usercode must %s or less digits',
|
' usercode must %s or less digits',
|
||||||
usercode, len(value.data))
|
usercode, len(value.data))
|
||||||
|
break
|
||||||
value.data = str(usercode)
|
value.data = str(usercode)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -175,32 +180,34 @@ def get_device(hass, node, value, **kwargs):
|
|||||||
_LOGGER.info('Usercode at slot %s is cleared', value.index)
|
_LOGGER.info('Usercode at slot %s is cleared', value.index)
|
||||||
break
|
break
|
||||||
|
|
||||||
if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
|
hass.services.async_register(DOMAIN,
|
||||||
hass.services.register(DOMAIN,
|
|
||||||
SERVICE_SET_USERCODE,
|
SERVICE_SET_USERCODE,
|
||||||
set_usercode,
|
set_usercode,
|
||||||
descriptions.get(SERVICE_SET_USERCODE),
|
descriptions.get(SERVICE_SET_USERCODE),
|
||||||
schema=SET_USERCODE_SCHEMA)
|
schema=SET_USERCODE_SCHEMA)
|
||||||
hass.services.register(DOMAIN,
|
hass.services.async_register(DOMAIN,
|
||||||
SERVICE_GET_USERCODE,
|
SERVICE_GET_USERCODE,
|
||||||
get_usercode,
|
get_usercode,
|
||||||
descriptions.get(SERVICE_GET_USERCODE),
|
descriptions.get(SERVICE_GET_USERCODE),
|
||||||
schema=GET_USERCODE_SCHEMA)
|
schema=GET_USERCODE_SCHEMA)
|
||||||
hass.services.register(DOMAIN,
|
hass.services.async_register(DOMAIN,
|
||||||
SERVICE_CLEAR_USERCODE,
|
SERVICE_CLEAR_USERCODE,
|
||||||
clear_usercode,
|
clear_usercode,
|
||||||
descriptions.get(SERVICE_CLEAR_USERCODE),
|
descriptions.get(SERVICE_CLEAR_USERCODE),
|
||||||
schema=CLEAR_USERCODE_SCHEMA)
|
schema=CLEAR_USERCODE_SCHEMA)
|
||||||
return ZwaveLock(value)
|
|
||||||
|
|
||||||
|
def get_device(node, values, **kwargs):
|
||||||
|
"""Create zwave entity device."""
|
||||||
|
return ZwaveLock(values)
|
||||||
|
|
||||||
|
|
||||||
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||||
"""Representation of a Z-Wave Lock."""
|
"""Representation of a Z-Wave Lock."""
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, values):
|
||||||
"""Initialize the Z-Wave lock device."""
|
"""Initialize the Z-Wave lock device."""
|
||||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self._node = value.node
|
|
||||||
self._state = None
|
self._state = None
|
||||||
self._notification = None
|
self._notification = None
|
||||||
self._lock_status = None
|
self._lock_status = None
|
||||||
@ -208,10 +215,10 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||||||
|
|
||||||
# Enable appropriate workaround flags for our device
|
# Enable appropriate workaround flags for our device
|
||||||
# Make sure that we have values for the key before converting to int
|
# Make sure that we have values for the key before converting to int
|
||||||
if (value.node.manufacturer_id.strip() and
|
if (self.node.manufacturer_id.strip() and
|
||||||
value.node.product_id.strip()):
|
self.node.product_id.strip()):
|
||||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
specific_sensor_key = (int(self.node.manufacturer_id, 16),
|
||||||
int(value.node.product_id, 16))
|
int(self.node.product_id, 16))
|
||||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE:
|
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE:
|
||||||
self._v2btze = 1
|
self._v2btze = 1
|
||||||
@ -221,43 +228,41 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Callback on data changes for node values."""
|
"""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'
|
_LOGGER.debug('Lock state set from Bool value and'
|
||||||
' is %s', self._state)
|
' is %s', self._state)
|
||||||
notification_data = self.get_value(class_id=zwave.const
|
if self.values.access_control:
|
||||||
.COMMAND_CLASS_ALARM,
|
notification_data = self.values.access_control.data
|
||||||
label=['Access Control'],
|
|
||||||
member='data')
|
|
||||||
if notification_data:
|
|
||||||
self._notification = LOCK_NOTIFICATION.get(str(notification_data))
|
self._notification = LOCK_NOTIFICATION.get(str(notification_data))
|
||||||
|
|
||||||
if self._v2btze:
|
if self._v2btze:
|
||||||
advanced_config = self.get_value(class_id=zwave.const
|
if self.values.v2btze_advanced and \
|
||||||
.COMMAND_CLASS_CONFIGURATION,
|
self.values.v2btze_advanced.data == CONFIG_ADVANCED:
|
||||||
index=12,
|
|
||||||
data=CONFIG_ADVANCED,
|
|
||||||
member='data')
|
|
||||||
if advanced_config:
|
|
||||||
self._state = LOCK_STATUS.get(str(notification_data))
|
self._state = LOCK_STATUS.get(str(notification_data))
|
||||||
_LOGGER.debug('Lock state set from Access Control '
|
_LOGGER.debug('Lock state set from Access Control '
|
||||||
'value and is %s, get=%s',
|
'value and is %s, get=%s',
|
||||||
str(notification_data),
|
str(notification_data),
|
||||||
self.state)
|
self.state)
|
||||||
|
|
||||||
alarm_type = self.get_value(class_id=zwave.const
|
if not self.values.alarm_type:
|
||||||
.COMMAND_CLASS_ALARM,
|
return
|
||||||
label=['Alarm Type'], member='data')
|
|
||||||
|
alarm_type = self.values.alarm_type.data
|
||||||
_LOGGER.debug('Lock alarm_type is %s', str(alarm_type))
|
_LOGGER.debug('Lock alarm_type is %s', str(alarm_type))
|
||||||
alarm_level = self.get_value(class_id=zwave.const
|
if self.values.alarm_level:
|
||||||
.COMMAND_CLASS_ALARM,
|
alarm_level = self.values.alarm_level.data
|
||||||
label=['Alarm Level'], member='data')
|
else:
|
||||||
|
alarm_level = None
|
||||||
_LOGGER.debug('Lock alarm_level is %s', str(alarm_level))
|
_LOGGER.debug('Lock alarm_level is %s', str(alarm_level))
|
||||||
|
|
||||||
if not alarm_type:
|
if not alarm_type:
|
||||||
return
|
return
|
||||||
if alarm_type is 21:
|
if alarm_type is 21:
|
||||||
self._lock_status = '{}{}'.format(
|
self._lock_status = '{}{}'.format(
|
||||||
LOCK_ALARM_TYPE.get(str(alarm_type)),
|
LOCK_ALARM_TYPE.get(str(alarm_type)),
|
||||||
MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level)))
|
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(
|
self._lock_status = '{}{}'.format(
|
||||||
LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level))
|
LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level))
|
||||||
return
|
return
|
||||||
@ -277,11 +282,11 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||||||
|
|
||||||
def lock(self, **kwargs):
|
def lock(self, **kwargs):
|
||||||
"""Lock the device."""
|
"""Lock the device."""
|
||||||
self._value.data = True
|
self.values.primary.data = True
|
||||||
|
|
||||||
def unlock(self, **kwargs):
|
def unlock(self, **kwargs):
|
||||||
"""Unlock the device."""
|
"""Unlock the device."""
|
||||||
self._value.data = False
|
self.values.primary.data = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
@ -292,8 +297,3 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||||||
if self._lock_status:
|
if self._lock_status:
|
||||||
data[ATTR_LOCK_STATUS] = self._lock_status
|
data[ATTR_LOCK_STATUS] = self._lock_status
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
|
||||||
def dependent_value_ids(self):
|
|
||||||
"""List of value IDs a device depends on."""
|
|
||||||
return None
|
|
||||||
|
100
homeassistant/components/lutron_caseta.py
Normal file
100
homeassistant/components/lutron_caseta.py
Normal 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
|
@ -4,32 +4,37 @@ Support to interface with the Emby API.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/media_player.emby/
|
https://home-assistant.io/components/media_player.emby/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice,
|
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_PLAY, PLATFORM_SCHEMA)
|
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
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_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
|
CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME,
|
||||||
from homeassistant.helpers.event import (track_utc_time_change)
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.core import callback
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['pyemby==0.2']
|
REQUIREMENTS = ['pyemby==1.1']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
|
||||||
|
CONF_AUTO_HIDE = 'auto_hide'
|
||||||
|
|
||||||
MEDIA_TYPE_TRAILER = 'trailer'
|
MEDIA_TYPE_TRAILER = 'trailer'
|
||||||
|
MEDIA_TYPE_GENERIC_VIDEO = 'video'
|
||||||
|
|
||||||
|
DEFAULT_HOST = 'localhost'
|
||||||
DEFAULT_PORT = 8096
|
DEFAULT_PORT = 8096
|
||||||
|
DEFAULT_SSL_PORT = 8920
|
||||||
|
DEFAULT_SSL = False
|
||||||
|
DEFAULT_AUTO_HIDE = False
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -37,219 +42,210 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
|||||||
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
|
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
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."""
|
"""Setup the Emby platform."""
|
||||||
from pyemby.emby import EmbyRemote
|
from pyemby import EmbyServer
|
||||||
|
|
||||||
_host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
_key = config.get(CONF_API_KEY)
|
key = config.get(CONF_API_KEY)
|
||||||
_port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
|
ssl = config.get(CONF_SSL)
|
||||||
|
auto_hide = config.get(CONF_AUTO_HIDE)
|
||||||
|
|
||||||
if config.get(CONF_SSL):
|
if port is None:
|
||||||
_protocol = "https"
|
port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT
|
||||||
else:
|
|
||||||
_protocol = "http"
|
|
||||||
|
|
||||||
_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 = {}
|
@callback
|
||||||
emby_sessions = {}
|
def device_update_callback(data):
|
||||||
track_utc_time_change(hass, lambda now: update_devices(), second=30)
|
"""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)
|
elif dev_id in inactive_emby_devices:
|
||||||
def update_devices():
|
if emby.devices[dev_id].state != 'Off':
|
||||||
"""Update the devices objects."""
|
add = inactive_emby_devices.pop(dev_id)
|
||||||
devices = embyserver.get_sessions()
|
active_emby_devices[dev_id] = add
|
||||||
if devices is None:
|
_LOGGER.debug("Showing %s, item: %s", dev_id, add)
|
||||||
_LOGGER.error('Error listing Emby devices.')
|
add.set_available(True)
|
||||||
return
|
add.set_hidden(False)
|
||||||
|
|
||||||
new_emby_clients = []
|
if new_devices:
|
||||||
for device in devices:
|
_LOGGER.debug("Adding new devices to HASS: %s", new_devices)
|
||||||
if device['DeviceId'] == embyserver.unique_id:
|
async_add_devices(new_devices, update_before_add=True)
|
||||||
break
|
|
||||||
|
|
||||||
if device['DeviceId'] not in emby_clients:
|
@callback
|
||||||
_LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.',
|
def device_removal_callback(data):
|
||||||
device['DeviceId'])
|
"""Callback for when devices are removed from emby."""
|
||||||
new_client = EmbyClient(embyserver, device, emby_sessions,
|
if data in active_emby_devices:
|
||||||
update_devices, update_sessions)
|
rem = active_emby_devices.pop(data)
|
||||||
emby_clients[device['DeviceId']] = new_client
|
inactive_emby_devices[data] = rem
|
||||||
new_emby_clients.append(new_client)
|
_LOGGER.debug("Inactive %s, item: %s", data, rem)
|
||||||
else:
|
rem.set_available(False)
|
||||||
emby_clients[device['DeviceId']].set_device(device)
|
if auto_hide:
|
||||||
|
rem.set_hidden(True)
|
||||||
|
|
||||||
if new_emby_clients:
|
@callback
|
||||||
add_devices_callback(new_emby_clients)
|
def start_emby(event):
|
||||||
|
"""Start emby connection."""
|
||||||
|
emby.start()
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
@asyncio.coroutine
|
||||||
def update_sessions():
|
def stop_emby(event):
|
||||||
"""Update the sessions objects."""
|
"""Stop emby connection."""
|
||||||
sessions = embyserver.get_sessions()
|
yield from emby.stop()
|
||||||
if sessions is None:
|
|
||||||
_LOGGER.error('Error listing Emby sessions')
|
|
||||||
return
|
|
||||||
|
|
||||||
emby_sessions.clear()
|
emby.add_new_devices_callback(device_update_callback)
|
||||||
for session in sessions:
|
emby.add_stale_devices_callback(device_removal_callback)
|
||||||
emby_sessions[session['DeviceId']] = session
|
|
||||||
|
|
||||||
update_devices()
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emby)
|
||||||
update_sessions()
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby)
|
||||||
|
|
||||||
|
|
||||||
class EmbyClient(MediaPlayerDevice):
|
class EmbyDevice(MediaPlayerDevice):
|
||||||
"""Representation of a Emby device."""
|
"""Representation of an Emby device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-public-methods,
|
def __init__(self, emby, device_id):
|
||||||
|
|
||||||
def __init__(self, client, device, emby_sessions, update_devices,
|
|
||||||
update_sessions):
|
|
||||||
"""Initialize the Emby device."""
|
"""Initialize the Emby device."""
|
||||||
self.emby_sessions = emby_sessions
|
_LOGGER.debug('New Emby Device initialized with ID: %s', device_id)
|
||||||
self.update_devices = update_devices
|
self.emby = emby
|
||||||
self.update_sessions = update_sessions
|
self.device_id = device_id
|
||||||
self.client = client
|
self.device = self.emby.devices[self.device_id]
|
||||||
self.set_device(device)
|
|
||||||
|
self._hidden = False
|
||||||
|
self._available = True
|
||||||
|
|
||||||
self.media_status_last_position = None
|
self.media_status_last_position = None
|
||||||
self.media_status_received = None
|
self.media_status_received = None
|
||||||
|
|
||||||
def set_device(self, device):
|
@asyncio.coroutine
|
||||||
"""Set the device property."""
|
def async_added_to_hass(self):
|
||||||
self.device = device
|
"""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
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the id of this emby client."""
|
"""Return the id of this emby client."""
|
||||||
return '{}.{}'.format(
|
return '{}.{}'.format(self.__class__, self.device_id)
|
||||||
self.__class__, self.device['DeviceId'])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_remote_control(self):
|
def supports_remote_control(self):
|
||||||
"""Return control ability."""
|
"""Return control ability."""
|
||||||
return self.device['SupportsRemoteControl']
|
return self.device.supports_remote_control
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return 'emby_{}'.format(self.device['DeviceName']) or \
|
return 'Emby - {} - {}'.format(self.device.client, self.device.name) \
|
||||||
DEVICE_DEFAULT_NAME
|
or DEVICE_DEFAULT_NAME
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self):
|
def should_poll(self):
|
||||||
"""Return the session, if any."""
|
"""Return True if entity has to be polled for state."""
|
||||||
if self.device['DeviceId'] not in self.emby_sessions:
|
return False
|
||||||
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']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
session = self.session
|
state = self.device.state
|
||||||
if session:
|
if state == 'Paused':
|
||||||
if 'NowPlayingItem' in session:
|
|
||||||
if session['PlayState']['IsPaused']:
|
|
||||||
return STATE_PAUSED
|
return STATE_PAUSED
|
||||||
else:
|
elif state == 'Playing':
|
||||||
return STATE_PLAYING
|
return STATE_PLAYING
|
||||||
else:
|
elif state == 'Idle':
|
||||||
return STATE_IDLE
|
return STATE_IDLE
|
||||||
# This is nasty. Need to find a way to determine alive
|
elif state == 'Off':
|
||||||
else:
|
|
||||||
return 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
|
@property
|
||||||
def app_name(self):
|
def app_name(self):
|
||||||
"""Return current user as app_name."""
|
"""Return current user as app_name."""
|
||||||
# Ideally the media_player object would have a user property.
|
# Ideally the media_player object would have a user property.
|
||||||
try:
|
return self.device.username
|
||||||
return self.device['UserName']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_content_id(self):
|
def media_content_id(self):
|
||||||
"""Content ID of current playing media."""
|
"""Content ID of current playing media."""
|
||||||
if self.now_playing_item is not None:
|
return self.device.media_id
|
||||||
try:
|
|
||||||
return self.now_playing_item['Id']
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_content_type(self):
|
def media_content_type(self):
|
||||||
"""Content type of current playing media."""
|
"""Content type of current playing media."""
|
||||||
if self.now_playing_item is None:
|
media_type = self.device.media_type
|
||||||
return None
|
|
||||||
try:
|
|
||||||
media_type = self.now_playing_item['Type']
|
|
||||||
if media_type == 'Episode':
|
if media_type == 'Episode':
|
||||||
return MEDIA_TYPE_TVSHOW
|
return MEDIA_TYPE_TVSHOW
|
||||||
elif media_type == 'Movie':
|
elif media_type == 'Movie':
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_VIDEO
|
||||||
elif media_type == 'Trailer':
|
elif media_type == 'Trailer':
|
||||||
return MEDIA_TYPE_TRAILER
|
return MEDIA_TYPE_TRAILER
|
||||||
return None
|
elif media_type == 'Music':
|
||||||
except KeyError:
|
return MEDIA_TYPE_MUSIC
|
||||||
|
elif media_type == 'Video':
|
||||||
|
return MEDIA_TYPE_GENERIC_VIDEO
|
||||||
|
elif media_type == 'Audio':
|
||||||
|
return MEDIA_TYPE_MUSIC
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_duration(self):
|
def media_duration(self):
|
||||||
"""Duration of current playing media in seconds."""
|
"""Duration of current playing media in seconds."""
|
||||||
if self.now_playing_item and self.media_content_type:
|
return self.device.media_runtime
|
||||||
try:
|
|
||||||
return int(self.now_playing_item['RunTimeTicks']) / 10000000
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_position(self):
|
def media_position(self):
|
||||||
@ -268,45 +264,42 @@ class EmbyClient(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
if self.now_playing_item is not None:
|
return self.device.media_image_url
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Title of current playing media."""
|
"""Title of current playing media."""
|
||||||
# find a string we can use as a title
|
return self.device.media_title
|
||||||
if self.now_playing_item is not None:
|
|
||||||
return self.now_playing_item['Name']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_season(self):
|
def media_season(self):
|
||||||
"""Season of curent playing media (TV Show only)."""
|
"""Season of curent playing media (TV Show only)."""
|
||||||
if self.now_playing_item is not None and \
|
return self.device.media_season
|
||||||
'ParentIndexNumber' in self.now_playing_item:
|
|
||||||
return self.now_playing_item['ParentIndexNumber']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_series_title(self):
|
def media_series_title(self):
|
||||||
"""The title of the series of current playing media (TV Show only)."""
|
"""The title of the series of current playing media (TV Show only)."""
|
||||||
if self.now_playing_item is not None and \
|
return self.device.media_series_title
|
||||||
'SeriesName' in self.now_playing_item:
|
|
||||||
return self.now_playing_item['SeriesName']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_episode(self):
|
def media_episode(self):
|
||||||
"""Episode of current playing media (TV Show only)."""
|
"""Episode of current playing media (TV Show only)."""
|
||||||
if self.now_playing_item is not None and \
|
return self.device.media_episode
|
||||||
'IndexNumber' in self.now_playing_item:
|
|
||||||
return self.now_playing_item['IndexNumber']
|
@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
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
@ -316,20 +309,44 @@ class EmbyClient(MediaPlayerDevice):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def media_play(self):
|
def async_media_play(self):
|
||||||
"""Send play command."""
|
"""Play media.
|
||||||
if self.supports_remote_control:
|
|
||||||
self.client.play(self.session)
|
|
||||||
|
|
||||||
def media_pause(self):
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""Send pause command."""
|
"""
|
||||||
if self.supports_remote_control:
|
return self.device.media_play()
|
||||||
self.client.pause(self.session)
|
|
||||||
|
|
||||||
def media_next_track(self):
|
def async_media_pause(self):
|
||||||
"""Send next track command."""
|
"""Pause the media player.
|
||||||
self.client.next_track(self.session)
|
|
||||||
|
|
||||||
def media_previous_track(self):
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""Send previous track command."""
|
"""
|
||||||
self.client.previous_track(self.session)
|
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)
|
||||||
|
@ -16,16 +16,18 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
|
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 (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
|
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)
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -37,11 +39,26 @@ DEFAULT_NAME = 'Kodi'
|
|||||||
DEFAULT_PORT = 8080
|
DEFAULT_PORT = 8080
|
||||||
DEFAULT_TCP_PORT = 9090
|
DEFAULT_TCP_PORT = 9090
|
||||||
DEFAULT_TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
DEFAULT_SSL = False
|
DEFAULT_PROXY_SSL = False
|
||||||
DEFAULT_ENABLE_WEBSOCKET = True
|
DEFAULT_ENABLE_WEBSOCKET = True
|
||||||
|
|
||||||
TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown']
|
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_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP
|
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_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_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.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION),
|
||||||
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
||||||
vol.Inclusive(CONF_PASSWORD, '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)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
tcp_port = config.get(CONF_TCP_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)
|
websocket = config.get(CONF_ENABLE_WEBSOCKET)
|
||||||
|
|
||||||
if host.startswith('http://') or host.startswith('https://'):
|
if host.startswith('http://') or host.startswith('https://'):
|
||||||
@ -169,7 +186,6 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
self._properties = {}
|
self._properties = {}
|
||||||
self._item = {}
|
self._item = {}
|
||||||
self._app_properties = {}
|
self._app_properties = {}
|
||||||
self._ws_connected = False
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_on_speed_event(self, sender, data):
|
def async_on_speed_event(self, sender, data):
|
||||||
@ -244,29 +260,26 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
"""Connect to Kodi via websocket protocol."""
|
"""Connect to Kodi via websocket protocol."""
|
||||||
import jsonrpc_base
|
import jsonrpc_base
|
||||||
try:
|
try:
|
||||||
yield from self._ws_server.ws_connect()
|
ws_loop_future = yield from self._ws_server.ws_connect()
|
||||||
except jsonrpc_base.jsonrpc.TransportError:
|
except jsonrpc_base.jsonrpc.TransportError:
|
||||||
_LOGGER.info("Unable to connect to Kodi via websocket")
|
_LOGGER.info("Unable to connect to Kodi via websocket")
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Unable to connect to Kodi via websocket", exc_info=True)
|
"Unable to connect to Kodi via websocket", exc_info=True)
|
||||||
# Websocket connection is not required. Just return.
|
|
||||||
return
|
return
|
||||||
self.hass.loop.create_task(self.async_ws_loop())
|
|
||||||
self._ws_connected = True
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_ws_loop(self):
|
def ws_loop_wrapper():
|
||||||
"""Run the websocket asyncio message loop."""
|
"""Catch exceptions from the websocket loop task."""
|
||||||
import jsonrpc_base
|
|
||||||
try:
|
try:
|
||||||
yield from self._ws_server.ws_loop()
|
yield from ws_loop_future
|
||||||
except jsonrpc_base.jsonrpc.TransportError:
|
except jsonrpc_base.TransportError:
|
||||||
# Kodi abruptly ends ws connection when exiting. We only need to
|
# Kodi abruptly ends ws connection when exiting. We will try
|
||||||
# know that it was closed.
|
# to reconnect on the next poll.
|
||||||
pass
|
pass
|
||||||
finally:
|
|
||||||
yield from self._ws_server.close()
|
# Create a task instead of adding a tracking job, since this task will
|
||||||
self._ws_connected = False
|
# run until the websocket connection is closed.
|
||||||
|
self.hass.loop.create_task(ws_loop_wrapper())
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_update(self):
|
def async_update(self):
|
||||||
@ -279,8 +292,8 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
self._app_properties = {}
|
self._app_properties = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._enable_websocket and not self._ws_connected:
|
if self._enable_websocket and not self._ws_server.connected:
|
||||||
self.hass.loop.create_task(self.async_ws_connect())
|
self.hass.async_add_job(self.async_ws_connect())
|
||||||
|
|
||||||
self._app_properties = \
|
self._app_properties = \
|
||||||
yield from self.server.Application.GetProperties(
|
yield from self.server.Application.GetProperties(
|
||||||
@ -299,7 +312,8 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
self._item = (yield from self.server.Player.GetItem(
|
self._item = (yield from self.server.Player.GetItem(
|
||||||
player_id,
|
player_id,
|
||||||
['title', 'file', 'uniqueid', 'thumbnail', 'artist']
|
['title', 'file', 'uniqueid', 'thumbnail', 'artist',
|
||||||
|
'albumartist', 'showtitle', 'album', 'season', 'episode']
|
||||||
))['item']
|
))['item']
|
||||||
else:
|
else:
|
||||||
self._properties = {}
|
self._properties = {}
|
||||||
@ -309,7 +323,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def server(self):
|
def server(self):
|
||||||
"""Active server for json-rpc requests."""
|
"""Active server for json-rpc requests."""
|
||||||
if self._ws_connected:
|
if self._enable_websocket and self._ws_server.connected:
|
||||||
return self._ws_server
|
return self._ws_server
|
||||||
else:
|
else:
|
||||||
return self._http_server
|
return self._http_server
|
||||||
@ -322,7 +336,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""Return True if entity has to be polled for state."""
|
"""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
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
@ -343,8 +357,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def media_content_type(self):
|
def media_content_type(self):
|
||||||
"""Content type of current playing media."""
|
"""Content type of current playing media."""
|
||||||
if self._players is not None and len(self._players) > 0:
|
return MEDIA_TYPES.get(self._item.get('type'))
|
||||||
return self._players[0]['type']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_duration(self):
|
def media_duration(self):
|
||||||
@ -382,6 +395,44 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
return self._item.get(
|
return self._item.get(
|
||||||
'title', self._item.get('label', self._item.get('file')))
|
'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
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag media player features that are supported."""
|
"""Flag media player features that are supported."""
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
|
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
|
||||||
CONF_HOST)
|
CONF_HOST, CONF_NAME)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
@ -25,9 +25,7 @@ REQUIREMENTS = ['python-mpd2==0.5.5']
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_LOCATION = 'location'
|
DEFAULT_NAME = 'MPD'
|
||||||
|
|
||||||
DEFAULT_LOCATION = 'MPD'
|
|
||||||
DEFAULT_PORT = 6600
|
DEFAULT_PORT = 6600
|
||||||
|
|
||||||
PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120)
|
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({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
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_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
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."""
|
"""Setup the MPD platform."""
|
||||||
daemon = config.get(CONF_HOST)
|
daemon = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
location = config.get(CONF_LOCATION)
|
name = config.get(CONF_NAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
import mpd
|
import mpd
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@ -75,20 +72,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
add_devices([MpdDevice(daemon, port, location, password)])
|
add_devices([MpdDevice(daemon, port, password, name)])
|
||||||
|
|
||||||
|
|
||||||
class MpdDevice(MediaPlayerDevice):
|
class MpdDevice(MediaPlayerDevice):
|
||||||
"""Representation of a MPD server."""
|
"""Representation of a MPD server."""
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
def __init__(self, server, port, location, password):
|
def __init__(self, server, port, password, name):
|
||||||
"""Initialize the MPD device."""
|
"""Initialize the MPD device."""
|
||||||
import mpd
|
import mpd
|
||||||
|
|
||||||
self.server = server
|
self.server = server
|
||||||
self.port = port
|
self.port = port
|
||||||
self._name = location
|
self._name = name
|
||||||
self.password = password
|
self.password = password
|
||||||
self.status = None
|
self.status = None
|
||||||
self.currentsong = None
|
self.currentsong = None
|
||||||
|
@ -10,16 +10,35 @@ import os
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import urlparse
|
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 (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
MEDIA_TYPE_MUSIC,
|
||||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
|
MEDIA_TYPE_TVSHOW,
|
||||||
SUPPORT_PLAY, MediaPlayerDevice)
|
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 (
|
from homeassistant.const import (
|
||||||
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
DEVICE_DEFAULT_NAME,
|
||||||
STATE_UNKNOWN)
|
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.loader import get_component
|
||||||
from homeassistant.helpers.event import (track_utc_time_change)
|
|
||||||
|
|
||||||
REQUIREMENTS = ['plexapi==2.0.2']
|
REQUIREMENTS = ['plexapi==2.0.2']
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
@ -27,13 +46,24 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
|||||||
|
|
||||||
PLEX_CONFIG_FILE = 'plex.conf'
|
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
|
# Map ip to request id for configuring
|
||||||
_CONFIGURING = {}
|
_CONFIGURING = {}
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
def config_from_file(filename, config=None):
|
||||||
"""Small configuration file management function."""
|
"""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):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
"""Setup the Plex platform."""
|
"""Setup the Plex platform."""
|
||||||
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
# get config from plex.conf
|
||||||
if len(config):
|
file_config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
||||||
|
|
||||||
|
if len(file_config):
|
||||||
# Setup a configured PlexServer
|
# Setup a configured PlexServer
|
||||||
host, token = config.popitem()
|
host, token = file_config.popitem()
|
||||||
token = token['token']
|
token = token['token']
|
||||||
# Via discovery
|
# Via discovery
|
||||||
elif discovery_info is not None:
|
elif discovery_info is not None:
|
||||||
@ -79,22 +111,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
else:
|
else:
|
||||||
return
|
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."""
|
"""Setup a plexserver based on host parameter."""
|
||||||
import plexapi.server
|
import plexapi.server
|
||||||
import plexapi.exceptions
|
import plexapi.exceptions
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
||||||
except (plexapi.exceptions.BadRequest,
|
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
|
||||||
plexapi.exceptions.Unauthorized,
|
|
||||||
plexapi.exceptions.NotFound) as error:
|
plexapi.exceptions.NotFound) as error:
|
||||||
_LOGGER.info(error)
|
_LOGGER.info(error)
|
||||||
# No token or wrong token
|
# No token or wrong token
|
||||||
request_configuration(host, hass, add_devices_callback)
|
request_configuration(host, hass, config,
|
||||||
|
add_devices_callback)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If we came here and configuring this host, mark as done
|
# 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
|
# Save config
|
||||||
if not config_from_file(
|
if not config_from_file(
|
||||||
hass.config.path(PLEX_CONFIG_FILE),
|
hass.config.path(PLEX_CONFIG_FILE), {host: {
|
||||||
{host: {'token': token}}):
|
'token': token
|
||||||
|
}}):
|
||||||
_LOGGER.error('failed to save config file')
|
_LOGGER.error('failed to save config file')
|
||||||
|
|
||||||
_LOGGER.info('Connected to: http://%s', host)
|
_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)
|
track_utc_time_change(hass, lambda now: update_devices(), second=30)
|
||||||
|
|
||||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
def update_devices():
|
def update_devices():
|
||||||
"""Update the devices objects."""
|
"""Update the devices objects."""
|
||||||
try:
|
try:
|
||||||
@ -125,8 +159,8 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||||||
_LOGGER.exception('Error listing plex devices')
|
_LOGGER.exception('Error listing plex devices')
|
||||||
return
|
return
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.error(
|
_LOGGER.error('Could not connect to plex server at http://%s',
|
||||||
'Could not connect to plex server at http://%s', host)
|
host)
|
||||||
return
|
return
|
||||||
|
|
||||||
new_plex_clients = []
|
new_plex_clients = []
|
||||||
@ -136,12 +170,31 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if device.machineIdentifier not in plex_clients:
|
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)
|
update_sessions)
|
||||||
plex_clients[device.machineIdentifier] = new_client
|
plex_clients[device.machineIdentifier] = new_client
|
||||||
new_plex_clients.append(new_client)
|
new_plex_clients.append(new_client)
|
||||||
else:
|
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:
|
if new_plex_clients:
|
||||||
add_devices_callback(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()
|
plex_sessions.clear()
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
|
if (session.player is not None and
|
||||||
|
session.player.machineIdentifier is not None):
|
||||||
plex_sessions[session.player.machineIdentifier] = session
|
plex_sessions[session.player.machineIdentifier] = session
|
||||||
|
|
||||||
update_devices()
|
|
||||||
update_sessions()
|
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."""
|
"""Request configuration steps from the user."""
|
||||||
configurator = get_component('configurator')
|
configurator = get_component('configurator')
|
||||||
|
|
||||||
# We got an error if this method is called while we are configuring
|
# We got an error if this method is called while we are configuring
|
||||||
if host in _CONFIGURING:
|
if host in _CONFIGURING:
|
||||||
configurator.notify_errors(
|
configurator.notify_errors(_CONFIGURING[host],
|
||||||
_CONFIGURING[host], 'Failed to register, please try again.')
|
'Failed to register, please try again.')
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def plex_configuration_callback(data):
|
def plex_configuration_callback(data):
|
||||||
"""The actions to do when our configuration callback is called."""
|
"""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(
|
_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'),
|
description=('Enter the X-Plex-Token'),
|
||||||
entity_picture='/static/images/logo_plex_mediaserver.png',
|
entity_picture='/static/images/logo_plex_mediaserver.png',
|
||||||
submit_caption='Confirm',
|
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):
|
class PlexClient(MediaPlayerDevice):
|
||||||
"""Representation of a Plex device."""
|
"""Representation of a Plex device."""
|
||||||
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
def __init__(self, config, device, session, plex_sessions,
|
||||||
|
update_devices, update_sessions):
|
||||||
"""Initialize the Plex device."""
|
"""Initialize the Plex device."""
|
||||||
from plexapi.utils import NA
|
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.na_type = NA
|
||||||
|
self.config = config
|
||||||
self.plex_sessions = plex_sessions
|
self.plex_sessions = plex_sessions
|
||||||
self.update_devices = update_devices
|
self.update_devices = update_devices
|
||||||
self.update_sessions = update_sessions
|
self.update_sessions = update_sessions
|
||||||
self.set_device(device)
|
|
||||||
self._season = None
|
|
||||||
|
|
||||||
def set_device(self, device):
|
# Music
|
||||||
"""Set the device property."""
|
self._media_album_artist = None
|
||||||
self.device = device
|
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
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the id of this plex client."""
|
"""Return the id of this plex client."""
|
||||||
return '{}.{}'.format(
|
return '{}.{}'.format(self.__class__, self.machine_identifier or
|
||||||
self.__class__, self.device.machineIdentifier or self.device.name)
|
self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""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
|
@property
|
||||||
def session(self):
|
def session(self):
|
||||||
"""Return the session, if any."""
|
"""Return the session, if any."""
|
||||||
return self.plex_sessions.get(self.device.machineIdentifier, None)
|
return self._session
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self.session and self.session.player:
|
return self._state
|
||||||
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
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest details."""
|
"""Get the latest details."""
|
||||||
from plexapi.video import Show
|
|
||||||
|
|
||||||
self.update_devices(no_throttle=True)
|
self.update_devices(no_throttle=True)
|
||||||
self.update_sessions(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
|
# pylint: disable=no-self-use, singleton-comparison
|
||||||
def _convert_na_to_none(self, value):
|
def _convert_na_to_none(self, value):
|
||||||
"""Convert PlexAPI _NA() instances to None."""
|
"""Convert PlexAPI _NA() instances to None."""
|
||||||
@ -272,93 +559,298 @@ class PlexClient(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def media_content_id(self):
|
def media_content_id(self):
|
||||||
"""Content ID of current playing media."""
|
"""Content ID of current playing media."""
|
||||||
if self.session is not None:
|
return self._media_content_id
|
||||||
return self._convert_na_to_none(self.session.ratingKey)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_content_type(self):
|
def media_content_type(self):
|
||||||
"""Content type of current playing media."""
|
"""Content type of current playing media."""
|
||||||
if self.session is None:
|
if self._session_type == 'clip':
|
||||||
return None
|
_LOGGER.debug('Clip content type detected, '
|
||||||
media_type = self.session.type
|
'compatibility may vary: %s',
|
||||||
if media_type == 'episode':
|
self.entity_id)
|
||||||
return MEDIA_TYPE_TVSHOW
|
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
|
return MEDIA_TYPE_VIDEO
|
||||||
elif media_type == 'track':
|
elif self._session_type == 'track':
|
||||||
return MEDIA_TYPE_MUSIC
|
return MEDIA_TYPE_MUSIC
|
||||||
|
else:
|
||||||
return None
|
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
|
@property
|
||||||
def media_duration(self):
|
def media_duration(self):
|
||||||
"""Duration of current playing media in seconds."""
|
"""Duration of current playing media in seconds."""
|
||||||
if self.session is not None:
|
return self._media_duration
|
||||||
return self._convert_na_to_none(self.session.duration)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
if self.session is not None:
|
return self._media_image_url
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Title of current playing media."""
|
"""Title of current playing media."""
|
||||||
# find a string we can use as a title
|
return self._media_title
|
||||||
if self.session is not None:
|
|
||||||
return self._convert_na_to_none(self.session.title)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_season(self):
|
def media_season(self):
|
||||||
"""Season of curent playing media (TV Show only)."""
|
"""Season of curent playing media (TV Show only)."""
|
||||||
return self._season
|
return self._media_season
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_series_title(self):
|
def media_series_title(self):
|
||||||
"""The title of the series of current playing media (TV Show only)."""
|
"""The title of the series of current playing media (TV Show only)."""
|
||||||
from plexapi.video import Show
|
return self._media_series_title
|
||||||
if isinstance(self.session, Show):
|
|
||||||
return self._convert_na_to_none(self.session.grandparentTitle)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_episode(self):
|
def media_episode(self):
|
||||||
"""Episode of current playing media (TV Show only)."""
|
"""Episode of current playing media (TV Show only)."""
|
||||||
from plexapi.video import Show
|
return self._media_episode
|
||||||
if isinstance(self.session, Show):
|
|
||||||
return self._convert_na_to_none(self.session.index)
|
@property
|
||||||
|
def make(self):
|
||||||
|
"""The make of the device (ex. SHIELD Android TV)."""
|
||||||
|
return self._make
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag media player features that are supported."""
|
"""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):
|
def set_volume_level(self, volume):
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
self.device.setVolume(int(volume * 100),
|
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||||
self._active_media_plexapi_type)
|
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):
|
def media_play(self):
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
|
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||||
|
self._local_client_control_fix()
|
||||||
self.device.play(self._active_media_plexapi_type)
|
self.device.play(self._active_media_plexapi_type)
|
||||||
|
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
|
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||||
|
self._local_client_control_fix()
|
||||||
self.device.pause(self._active_media_plexapi_type)
|
self.device.pause(self._active_media_plexapi_type)
|
||||||
|
|
||||||
def media_stop(self):
|
def media_stop(self):
|
||||||
"""Send stop command."""
|
"""Send stop command."""
|
||||||
|
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||||
|
self._local_client_control_fix()
|
||||||
self.device.stop(self._active_media_plexapi_type)
|
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):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
|
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||||
|
self._local_client_control_fix()
|
||||||
self.device.skipNext(self._active_media_plexapi_type)
|
self.device.skipNext(self._active_media_plexapi_type)
|
||||||
|
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
|
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||||
|
self._local_client_control_fix()
|
||||||
self.device.skipPrevious(self._active_media_plexapi_type)
|
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))
|
||||||
|
237
homeassistant/components/media_player/volumio.py
Executable file
237
homeassistant/components/media_player/volumio.py
Executable 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})
|
@ -9,6 +9,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import ssl
|
||||||
import requests.certs
|
import requests.certs
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -48,6 +49,7 @@ CONF_CERTIFICATE = 'certificate'
|
|||||||
CONF_CLIENT_KEY = 'client_key'
|
CONF_CLIENT_KEY = 'client_key'
|
||||||
CONF_CLIENT_CERT = 'client_cert'
|
CONF_CLIENT_CERT = 'client_cert'
|
||||||
CONF_TLS_INSECURE = 'tls_insecure'
|
CONF_TLS_INSECURE = 'tls_insecure'
|
||||||
|
CONF_TLS_VERSION = 'tls_version'
|
||||||
|
|
||||||
CONF_BIRTH_MESSAGE = 'birth_message'
|
CONF_BIRTH_MESSAGE = 'birth_message'
|
||||||
CONF_WILL_MESSAGE = 'will_message'
|
CONF_WILL_MESSAGE = 'will_message'
|
||||||
@ -67,6 +69,7 @@ DEFAULT_RETAIN = False
|
|||||||
DEFAULT_PROTOCOL = PROTOCOL_311
|
DEFAULT_PROTOCOL = PROTOCOL_311
|
||||||
DEFAULT_DISCOVERY = False
|
DEFAULT_DISCOVERY = False
|
||||||
DEFAULT_DISCOVERY_PREFIX = 'homeassistant'
|
DEFAULT_DISCOVERY_PREFIX = 'homeassistant'
|
||||||
|
DEFAULT_TLS_PROTOCOL = 'auto'
|
||||||
|
|
||||||
ATTR_TOPIC = 'topic'
|
ATTR_TOPIC = 'topic'
|
||||||
ATTR_PAYLOAD = 'payload'
|
ATTR_PAYLOAD = 'payload'
|
||||||
@ -116,12 +119,15 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): 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',
|
vol.Inclusive(CONF_CLIENT_KEY, 'client_key_auth',
|
||||||
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
||||||
vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth',
|
vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth',
|
||||||
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
||||||
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
|
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.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
||||||
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
||||||
vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA,
|
vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA,
|
||||||
@ -311,18 +317,34 @@ def async_setup(hass, config):
|
|||||||
certificate = os.path.join(os.path.dirname(__file__),
|
certificate = os.path.join(os.path.dirname(__file__),
|
||||||
'addtrustexternalcaroot.crt')
|
'addtrustexternalcaroot.crt')
|
||||||
|
|
||||||
# When the port indicates mqtts, use bundled certificates from requests
|
# When the certificate is set to auto, use bundled certs from requests
|
||||||
if certificate is None and port == 8883:
|
if certificate == 'auto':
|
||||||
certificate = requests.certs.where()
|
certificate = requests.certs.where()
|
||||||
|
|
||||||
will_message = conf.get(CONF_WILL_MESSAGE)
|
will_message = conf.get(CONF_WILL_MESSAGE)
|
||||||
birth_message = conf.get(CONF_BIRTH_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:
|
try:
|
||||||
hass.data[DATA_MQTT] = MQTT(
|
hass.data[DATA_MQTT] = MQTT(
|
||||||
hass, broker, port, client_id, keepalive, username, password,
|
hass, broker, port, client_id, keepalive, username, password,
|
||||||
certificate, client_key, client_cert, tls_insecure, protocol,
|
certificate, client_key, client_cert, tls_insecure, protocol,
|
||||||
will_message, birth_message)
|
will_message, birth_message, tls_version)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_LOGGER.exception("Can't connect to the broker. "
|
_LOGGER.exception("Can't connect to the broker. "
|
||||||
"Please check your settings and the broker itself")
|
"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,
|
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||||
password, certificate, client_key, client_cert,
|
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."""
|
"""Initialize Home Assistant MQTT client."""
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
@ -409,7 +432,8 @@ class MQTT(object):
|
|||||||
|
|
||||||
if certificate is not None:
|
if certificate is not None:
|
||||||
self._mqttc.tls_set(
|
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:
|
if tls_insecure is not None:
|
||||||
self._mqttc.tls_insecure_set(tls_insecure)
|
self._mqttc.tls_insecure_set(tls_insecure)
|
||||||
|
@ -84,6 +84,11 @@ class iOSNotificationService(BaseNotificationService):
|
|||||||
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
||||||
|
|
||||||
for target in targets:
|
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
|
data[ATTR_TARGET] = target
|
||||||
|
|
||||||
req = requests.post(PUSH_URL, json=data, timeout=10)
|
req = requests.post(PUSH_URL, json=data, timeout=10)
|
||||||
|
@ -4,24 +4,33 @@ Kodi notification service.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/notify.kodi/
|
https://home-assistant.io/components/notify.kodi/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (ATTR_ICON, CONF_HOST, CONF_PORT,
|
from homeassistant.const import (
|
||||||
CONF_USERNAME, CONF_PASSWORD)
|
ATTR_ICON, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||||
from homeassistant.components.notify import (ATTR_TITLE, ATTR_TITLE_DEFAULT,
|
CONF_PROXY_SSL)
|
||||||
ATTR_DATA, PLATFORM_SCHEMA,
|
from homeassistant.components.notify import (
|
||||||
|
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
||||||
BaseNotificationService)
|
BaseNotificationService)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['jsonrpc-async==0.4']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['jsonrpc-requests==0.3']
|
|
||||||
|
|
||||||
DEFAULT_PORT = 8080
|
DEFAULT_PORT = 8080
|
||||||
|
DEFAULT_PROXY_SSL = False
|
||||||
|
DEFAULT_TIMEOUT = 5
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
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_USERNAME, 'auth'): cv.string,
|
||||||
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
||||||
})
|
})
|
||||||
@ -29,51 +38,66 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
ATTR_DISPLAYTIME = 'displaytime'
|
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."""
|
"""Return the notify service."""
|
||||||
url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT))
|
url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT))
|
||||||
|
|
||||||
username = config.get(CONF_USERNAME)
|
username = config.get(CONF_USERNAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
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:
|
if username is not None:
|
||||||
auth = (username, password)
|
auth = aiohttp.BasicAuth(username, password)
|
||||||
else:
|
else:
|
||||||
auth = None
|
auth = None
|
||||||
|
|
||||||
return KODINotificationService(
|
return KodiNotificationService(hass, url, auth)
|
||||||
url,
|
|
||||||
auth
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class KODINotificationService(BaseNotificationService):
|
class KodiNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for Kodi."""
|
"""Implement the notification service for Kodi."""
|
||||||
|
|
||||||
def __init__(self, url, auth=None):
|
def __init__(self, hass, url, auth=None):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
import jsonrpc_requests
|
import jsonrpc_async
|
||||||
self._url = url
|
self._url = url
|
||||||
|
|
||||||
kwargs = {'timeout': 5}
|
kwargs = {
|
||||||
|
'timeout': DEFAULT_TIMEOUT,
|
||||||
|
'session': async_get_clientsession(hass),
|
||||||
|
}
|
||||||
|
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
kwargs['auth'] = auth
|
kwargs['auth'] = auth
|
||||||
|
|
||||||
self._server = jsonrpc_requests.Server(
|
self._server = jsonrpc_async.Server(self._url, **kwargs)
|
||||||
'{}/jsonrpc'.format(self._url), **kwargs)
|
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
@asyncio.coroutine
|
||||||
|
def async_send_message(self, message="", **kwargs):
|
||||||
"""Send a message to Kodi."""
|
"""Send a message to Kodi."""
|
||||||
import jsonrpc_requests
|
import jsonrpc_async
|
||||||
try:
|
try:
|
||||||
data = kwargs.get(ATTR_DATA) or {}
|
data = kwargs.get(ATTR_DATA) or {}
|
||||||
|
|
||||||
displaytime = data.get(ATTR_DISPLAYTIME, 10000)
|
displaytime = data.get(ATTR_DISPLAYTIME, 10000)
|
||||||
icon = data.get(ATTR_ICON, "info")
|
icon = data.get(ATTR_ICON, "info")
|
||||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||||
self._server.GUI.ShowNotification(title, message, icon,
|
yield from self._server.GUI.ShowNotification(
|
||||||
displaytime)
|
title, message, icon, displaytime)
|
||||||
|
|
||||||
except jsonrpc_requests.jsonrpc.TransportError:
|
except jsonrpc_async.TransportError:
|
||||||
_LOGGER.warning('Unable to fetch Kodi data, Is Kodi online?')
|
_LOGGER.warning('Unable to fetch Kodi data, Is Kodi online?')
|
||||||
|
@ -18,7 +18,8 @@ from homeassistant.components.notify import (
|
|||||||
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
||||||
BaseNotificationService)
|
BaseNotificationService)
|
||||||
from homeassistant.const import (
|
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.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ CONF_SERVER = 'server'
|
|||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
DEFAULT_PORT = 25
|
DEFAULT_PORT = 25
|
||||||
|
DEFAULT_TIMEOUT = 5
|
||||||
DEFAULT_DEBUG = False
|
DEFAULT_DEBUG = False
|
||||||
DEFAULT_STARTTLS = False
|
DEFAULT_STARTTLS = False
|
||||||
|
|
||||||
@ -40,6 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Required(CONF_RECIPIENT): vol.Email(),
|
vol.Required(CONF_RECIPIENT): vol.Email(),
|
||||||
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
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_SENDER): vol.Email(),
|
||||||
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
@ -53,6 +56,7 @@ def get_service(hass, config, discovery_info=None):
|
|||||||
mail_service = MailNotificationService(
|
mail_service = MailNotificationService(
|
||||||
config.get(CONF_SERVER),
|
config.get(CONF_SERVER),
|
||||||
config.get(CONF_PORT),
|
config.get(CONF_PORT),
|
||||||
|
config.get(CONF_TIMEOUT),
|
||||||
config.get(CONF_SENDER),
|
config.get(CONF_SENDER),
|
||||||
config.get(CONF_STARTTLS),
|
config.get(CONF_STARTTLS),
|
||||||
config.get(CONF_USERNAME),
|
config.get(CONF_USERNAME),
|
||||||
@ -69,11 +73,12 @@ def get_service(hass, config, discovery_info=None):
|
|||||||
class MailNotificationService(BaseNotificationService):
|
class MailNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for E-Mail messages."""
|
"""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):
|
password, recipient, debug):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self._server = server
|
self._server = server
|
||||||
self._port = port
|
self._port = port
|
||||||
|
self._timeout = timeout
|
||||||
self._sender = sender
|
self._sender = sender
|
||||||
self.starttls = starttls
|
self.starttls = starttls
|
||||||
self.username = username
|
self.username = username
|
||||||
@ -84,7 +89,7 @@ class MailNotificationService(BaseNotificationService):
|
|||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connect/authenticate to SMTP Server."""
|
"""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.set_debuglevel(self.debug)
|
||||||
mail.ehlo_or_helo_if_needed()
|
mail.ehlo_or_helo_if_needed()
|
||||||
if self.starttls:
|
if self.starttls:
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
|
|||||||
|
|
||||||
REQUIREMENTS = ['sleekxmpp==1.3.1',
|
REQUIREMENTS = ['sleekxmpp==1.3.1',
|
||||||
'dnspython3==1.15.0',
|
'dnspython3==1.15.0',
|
||||||
'pyasn1==0.2.2',
|
'pyasn1==0.2.3',
|
||||||
'pyasn1-modules==0.0.8']
|
'pyasn1-modules==0.0.8']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -35,7 +35,7 @@ from .util import session_scope
|
|||||||
|
|
||||||
DOMAIN = 'recorder'
|
DOMAIN = 'recorder'
|
||||||
|
|
||||||
REQUIREMENTS = ['sqlalchemy==1.1.5']
|
REQUIREMENTS = ['sqlalchemy==1.1.6']
|
||||||
|
|
||||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||||
@ -287,13 +287,27 @@ class Recorder(threading.Thread):
|
|||||||
|
|
||||||
def _setup_connection(self):
|
def _setup_connection(self):
|
||||||
"""Ensure database is ready to fly."""
|
"""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 scoped_session
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
kwargs = {}
|
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:
|
if self.db_url == 'sqlite://' or ':memory:' in self.db_url:
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ def async_setup(hass, config):
|
|||||||
if not remote.should_poll:
|
if not remote.should_poll:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
update_coro = hass.loop.create_task(
|
update_coro = hass.async_add_job(
|
||||||
remote.async_update_ha_state(True))
|
remote.async_update_ha_state(True))
|
||||||
if hasattr(remote, 'async_update'):
|
if hasattr(remote, 'async_update'):
|
||||||
update_tasks.append(update_coro)
|
update_tasks.append(update_coro)
|
||||||
|
@ -90,7 +90,7 @@ def async_setup(hass, config):
|
|||||||
auth=auth
|
auth=auth
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.status == 200:
|
if request.status < 400:
|
||||||
_LOGGER.info("Success call %s.", request.url)
|
_LOGGER.info("Success call %s.", request.url)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -316,6 +316,12 @@ class RflinkCommand(RflinkDevice):
|
|||||||
cmd = str(int(args[0] / 17))
|
cmd = str(int(args[0] / 17))
|
||||||
self._state = True
|
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.
|
# Send initial command and queue repetitions.
|
||||||
# This allows the entity state to be updated quickly and not having to
|
# This allows the entity state to be updated quickly and not having to
|
||||||
# wait for all repetitions to be sent
|
# wait for all repetitions to be sent
|
||||||
@ -357,7 +363,7 @@ class RflinkCommand(RflinkDevice):
|
|||||||
self._protocol.send_command, self._device_id, cmd))
|
self._protocol.send_command, self._device_id, cmd))
|
||||||
|
|
||||||
if repetitions > 1:
|
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))
|
self._async_send_command(cmd, repetitions - 1))
|
||||||
|
|
||||||
|
|
||||||
|
40
homeassistant/components/scene/wink.py
Normal file
40
homeassistant/components/scene/wink.py
Normal 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()
|
@ -99,11 +99,13 @@ class ComedHourlyPricingSensor(Entity):
|
|||||||
if self.type == CONF_FIVE_MINUTE:
|
if self.type == CONF_FIVE_MINUTE:
|
||||||
url_string = _RESOURCE + '?type=5minutefeed'
|
url_string = _RESOURCE + '?type=5minutefeed'
|
||||||
response = get(url_string, timeout=10)
|
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:
|
elif self.type == CONF_CURRENT_HOUR_AVERAGE:
|
||||||
url_string = _RESOURCE + '?type=currenthouraverage'
|
url_string = _RESOURCE + '?type=currenthouraverage'
|
||||||
response = get(url_string, timeout=10)
|
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:
|
else:
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
except (RequestException, ValueError, KeyError):
|
except (RequestException, ValueError, KeyError):
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_NAME
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['py-cpuinfo==0.2.6']
|
REQUIREMENTS = ['py-cpuinfo==0.2.7']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@ from requests.exceptions import ConnectionError as ConnectError, \
|
|||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
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.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -117,6 +118,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']),
|
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.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): (
|
||||||
vol.All(cv.time_period, cv.positive_timedelta)),
|
vol.All(cv.time_period, cv.positive_timedelta)),
|
||||||
vol.Optional(CONF_FORECAST):
|
vol.Optional(CONF_FORECAST):
|
||||||
@ -126,10 +131,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Dark Sky sensor."""
|
"""Setup the Dark Sky sensor."""
|
||||||
# Validate the configuration
|
# latitude and longitude are inclusive on config
|
||||||
if None in (hass.config.latitude, hass.config.longitude):
|
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||||
return False
|
|
||||||
|
|
||||||
if CONF_UNITS in config:
|
if CONF_UNITS in config:
|
||||||
units = config[CONF_UNITS]
|
units = config[CONF_UNITS]
|
||||||
@ -140,8 +144,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
forecast_data = DarkSkyData(
|
forecast_data = DarkSkyData(
|
||||||
api_key=config.get(CONF_API_KEY, None),
|
api_key=config.get(CONF_API_KEY, None),
|
||||||
latitude=hass.config.latitude,
|
latitude=latitude,
|
||||||
longitude=hass.config.longitude,
|
longitude=longitude,
|
||||||
units=units,
|
units=units,
|
||||||
interval=config.get(CONF_UPDATE_INTERVAL))
|
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||||
forecast_data.update()
|
forecast_data.update()
|
||||||
|
@ -108,7 +108,9 @@ class Dovado:
|
|||||||
"""Update device state."""
|
"""Update device state."""
|
||||||
_LOGGER.info("Updating")
|
_LOGGER.info("Updating")
|
||||||
try:
|
try:
|
||||||
self.state.update(self._dovado.state or {})
|
self.state = self._dovado.state or {}
|
||||||
|
if not self.state:
|
||||||
|
return False
|
||||||
self.state.update(
|
self.state.update(
|
||||||
connected=self.state.get("modem status") == "CONNECTED")
|
connected=self.state.get("modem status") == "CONNECTED")
|
||||||
_LOGGER.debug("Received: %s", self.state)
|
_LOGGER.debug("Received: %s", self.state)
|
||||||
|
@ -40,7 +40,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['dsmr_parser==0.6']
|
REQUIREMENTS = ['dsmr_parser==0.8']
|
||||||
|
|
||||||
CONF_DSMR_VERSION = 'dsmr_version'
|
CONF_DSMR_VERSION = 'dsmr_version'
|
||||||
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
|
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)
|
logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
|
||||||
|
|
||||||
from dsmr_parser import obis_references as obis_ref
|
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
|
import serial
|
||||||
|
|
||||||
dsmr_version = config[CONF_DSMR_VERSION]
|
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
|
# Make all device entities aware of new telegram
|
||||||
for device in devices:
|
for device in devices:
|
||||||
device.telegram = telegram
|
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
|
# Creates a asyncio.Protocol factory for reading DSMR telegrams from serial
|
||||||
# and calls update_entities_telegram to update entities on arrival
|
# 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():
|
def connect_and_reconnect():
|
||||||
"""Connect to DSMR and keep reconnecting until HA stops."""
|
"""Connect to DSMR and keep reconnecting until HA stops."""
|
||||||
while hass.state != CoreState.stopping:
|
while hass.state != CoreState.stopping:
|
||||||
# Start DSMR asycnio.Protocol reader
|
# Start DSMR asyncio.Protocol reader
|
||||||
try:
|
try:
|
||||||
transport, protocol = yield from hass.loop.create_task(
|
transport, protocol = yield from hass.loop.create_task(
|
||||||
reader_factory())
|
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],
|
yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL],
|
||||||
loop=hass.loop)
|
loop=hass.loop)
|
||||||
|
|
||||||
|
# Cannot be hass.async_add_job because job runs forever
|
||||||
hass.loop.create_task(connect_and_reconnect())
|
hass.loop.create_task(connect_and_reconnect())
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,12 +28,14 @@ CONF_INSTANT = 'instant_readings'
|
|||||||
CONF_AMOUNT = 'amount'
|
CONF_AMOUNT = 'amount'
|
||||||
CONF_BUDGET = 'budget'
|
CONF_BUDGET = 'budget'
|
||||||
CONF_COST = 'cost'
|
CONF_COST = 'cost'
|
||||||
|
CONF_CURRENT_VALUES = 'current_values'
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
CONF_INSTANT: ['Energy Usage', 'kW'],
|
CONF_INSTANT: ['Energy Usage', 'kW'],
|
||||||
CONF_AMOUNT: ['Energy Consumed', 'kWh'],
|
CONF_AMOUNT: ['Energy Consumed', 'kWh'],
|
||||||
CONF_BUDGET: ['Energy Budget', None],
|
CONF_BUDGET: ['Energy Budget', None],
|
||||||
CONF_COST: ['Energy Cost', None],
|
CONF_COST: ['Energy Cost', None],
|
||||||
|
CONF_CURRENT_VALUES: ['Per-Device Usage', 'kW']
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPES_SCHEMA = vol.In(SENSOR_TYPES)
|
TYPES_SCHEMA = vol.In(SENSOR_TYPES)
|
||||||
@ -57,18 +59,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
utc_offset = str(config.get(CONF_UTC_OFFSET))
|
utc_offset = str(config.get(CONF_UTC_OFFSET))
|
||||||
dev = []
|
dev = []
|
||||||
for variable in config[CONF_MONITORED_VARIABLES]:
|
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(
|
dev.append(EfergySensor(
|
||||||
variable[CONF_SENSOR_TYPE], app_token, utc_offset,
|
variable[CONF_SENSOR_TYPE], app_token, utc_offset,
|
||||||
variable[CONF_PERIOD], variable[CONF_CURRENCY]))
|
variable[CONF_PERIOD], variable[CONF_CURRENCY]))
|
||||||
|
|
||||||
add_devices(dev)
|
add_devices(dev, True)
|
||||||
|
|
||||||
|
|
||||||
class EfergySensor(Entity):
|
class EfergySensor(Entity):
|
||||||
"""Implementation of an Efergy sensor."""
|
"""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."""
|
"""Initialize the sensor."""
|
||||||
|
self.sid = sid
|
||||||
|
if sid:
|
||||||
|
self._name = 'efergy_' + sid
|
||||||
|
else:
|
||||||
self._name = SENSOR_TYPES[sensor_type][0]
|
self._name = SENSOR_TYPES[sensor_type][0]
|
||||||
self.type = sensor_type
|
self.type = sensor_type
|
||||||
self.app_token = app_token
|
self.app_token = app_token
|
||||||
@ -119,6 +135,14 @@ class EfergySensor(Entity):
|
|||||||
+ self.period
|
+ self.period
|
||||||
response = get(url_string, timeout=10)
|
response = get(url_string, timeout=10)
|
||||||
self._state = response.json()['sum']
|
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:
|
else:
|
||||||
self._state = 'Unknown'
|
self._state = 'Unknown'
|
||||||
except (RequestException, ValueError, KeyError):
|
except (RequestException, ValueError, KeyError):
|
||||||
|
@ -9,13 +9,19 @@ import socket
|
|||||||
import threading
|
import threading
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
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
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEFAULT_NAME = 'Phone'
|
DEFAULT_NAME = 'Phone'
|
||||||
@ -27,13 +33,24 @@ VALUE_RING = 'ringing'
|
|||||||
VALUE_CALL = 'dialing'
|
VALUE_CALL = 'dialing'
|
||||||
VALUE_CONNECT = 'talking'
|
VALUE_CONNECT = 'talking'
|
||||||
VALUE_DISCONNECT = 'idle'
|
VALUE_DISCONNECT = 'idle'
|
||||||
|
CONF_PHONEBOOK = 'phonebook'
|
||||||
|
CONF_PREFIXES = 'prefixes'
|
||||||
|
|
||||||
INTERVAL_RECONNECT = 60
|
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({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
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)
|
name = config.get(CONF_NAME)
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
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])
|
add_devices([sensor])
|
||||||
|
|
||||||
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
|
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
|
||||||
monitor.connect()
|
monitor.connect()
|
||||||
|
|
||||||
|
def _stop_listener(_event):
|
||||||
|
monitor.stopped.set()
|
||||||
|
|
||||||
|
hass.bus.listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
_stop_listener
|
||||||
|
)
|
||||||
|
|
||||||
if monitor.sock is None:
|
if monitor.sock is None:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -59,11 +99,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class FritzBoxCallSensor(Entity):
|
class FritzBoxCallSensor(Entity):
|
||||||
"""Implementation of a Fritz!Box call monitor."""
|
"""Implementation of a Fritz!Box call monitor."""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name, phonebook):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._state = VALUE_DEFAULT
|
self._state = VALUE_DEFAULT
|
||||||
self._attributes = {}
|
self._attributes = {}
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self.phonebook = phonebook
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
"""Set the state."""
|
"""Set the state."""
|
||||||
@ -75,8 +116,11 @@ class FritzBoxCallSensor(Entity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""No polling needed."""
|
"""Polling needed only to update phonebook, if defined."""
|
||||||
|
if self.phonebook is None:
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -93,6 +137,18 @@ class FritzBoxCallSensor(Entity):
|
|||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return self._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):
|
class FritzBoxCallMonitor(object):
|
||||||
"""Event listener to monitor calls on the Fritz!Box."""
|
"""Event listener to monitor calls on the Fritz!Box."""
|
||||||
@ -103,6 +159,7 @@ class FritzBoxCallMonitor(object):
|
|||||||
self.port = port
|
self.port = port
|
||||||
self.sock = None
|
self.sock = None
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
|
self.stopped = threading.Event()
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connect to the Fritz!Box."""
|
"""Connect to the Fritz!Box."""
|
||||||
@ -110,7 +167,7 @@ class FritzBoxCallMonitor(object):
|
|||||||
self.sock.settimeout(10)
|
self.sock.settimeout(10)
|
||||||
try:
|
try:
|
||||||
self.sock.connect((self.host, self.port))
|
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:
|
except socket.error as err:
|
||||||
self.sock = None
|
self.sock = None
|
||||||
_LOGGER.error("Cannot connect to %s on port %s: %s",
|
_LOGGER.error("Cannot connect to %s on port %s: %s",
|
||||||
@ -118,7 +175,7 @@ class FritzBoxCallMonitor(object):
|
|||||||
|
|
||||||
def _listen(self):
|
def _listen(self):
|
||||||
"""Listen to incoming or outgoing calls."""
|
"""Listen to incoming or outgoing calls."""
|
||||||
while True:
|
while not self.stopped.isSet():
|
||||||
try:
|
try:
|
||||||
response = self.sock.recv(2048)
|
response = self.sock.recv(2048)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
@ -152,6 +209,7 @@ class FritzBoxCallMonitor(object):
|
|||||||
"to": line[4],
|
"to": line[4],
|
||||||
"device": line[5],
|
"device": line[5],
|
||||||
"initiated": isotime}
|
"initiated": isotime}
|
||||||
|
att["from_name"] = self._sensor.number_to_name(att["from"])
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
elif line[1] == "CALL":
|
elif line[1] == "CALL":
|
||||||
self._sensor.set_state(VALUE_CALL)
|
self._sensor.set_state(VALUE_CALL)
|
||||||
@ -160,13 +218,73 @@ class FritzBoxCallMonitor(object):
|
|||||||
"to": line[5],
|
"to": line[5],
|
||||||
"device": line[6],
|
"device": line[6],
|
||||||
"initiated": isotime}
|
"initiated": isotime}
|
||||||
|
att["to_name"] = self._sensor.number_to_name(att["to"])
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
elif line[1] == "CONNECT":
|
elif line[1] == "CONNECT":
|
||||||
self._sensor.set_state(VALUE_CONNECT)
|
self._sensor.set_state(VALUE_CONNECT)
|
||||||
att = {"with": line[4], "device": [3], "accepted": isotime}
|
att = {"with": line[4], "device": [3], "accepted": isotime}
|
||||||
|
att["with_name"] = self._sensor.number_to_name(att["with"])
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
elif line[1] == "DISCONNECT":
|
elif line[1] == "DISCONNECT":
|
||||||
self._sensor.set_state(VALUE_DISCONNECT)
|
self._sensor.set_state(VALUE_DISCONNECT)
|
||||||
att = {"duration": line[3], "closed": isotime}
|
att = {"duration": line[3], "closed": isotime}
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
self._sensor.schedule_update_ha_state()
|
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'
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.util import Throttle
|
|||||||
|
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
REQUIREMENTS = ['fritzconnection==0.6']
|
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
import homeassistant.helpers.location as location
|
import homeassistant.helpers.location as location
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['googlemaps==2.4.4']
|
REQUIREMENTS = ['googlemaps==2.4.6']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
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.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import track_state_change
|
from homeassistant.helpers.event import track_state_change
|
||||||
@ -31,15 +32,22 @@ CONF_END = 'end'
|
|||||||
CONF_DURATION = 'duration'
|
CONF_DURATION = 'duration'
|
||||||
CONF_PERIOD_KEYS = [CONF_START, CONF_END, CONF_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'
|
DEFAULT_NAME = 'unnamed statistics'
|
||||||
UNIT = 'h'
|
UNITS = {
|
||||||
UNIT_RATIO = '%'
|
CONF_TYPE_TIME: 'h',
|
||||||
|
CONF_TYPE_RATIO: '%',
|
||||||
|
CONF_TYPE_COUNT: ''
|
||||||
|
}
|
||||||
ICON = 'mdi:chart-line'
|
ICON = 'mdi:chart-line'
|
||||||
|
|
||||||
ATTR_START = 'from'
|
ATTR_START = 'from'
|
||||||
ATTR_END = 'to'
|
ATTR_END = 'to'
|
||||||
ATTR_VALUE = 'value'
|
ATTR_VALUE = 'value'
|
||||||
ATTR_RATIO = 'ratio'
|
|
||||||
|
|
||||||
|
|
||||||
def exactly_two_period_keys(conf):
|
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_START, default=None): cv.template,
|
||||||
vol.Optional(CONF_END, default=None): cv.template,
|
vol.Optional(CONF_END, default=None): cv.template,
|
||||||
vol.Optional(CONF_DURATION, default=None): cv.time_period,
|
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,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
}), exactly_two_period_keys)
|
}), exactly_two_period_keys)
|
||||||
|
|
||||||
@ -74,14 +83,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
start = config.get(CONF_START)
|
start = config.get(CONF_START)
|
||||||
end = config.get(CONF_END)
|
end = config.get(CONF_END)
|
||||||
duration = config.get(CONF_DURATION)
|
duration = config.get(CONF_DURATION)
|
||||||
|
sensor_type = config.get(CONF_TYPE)
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
|
|
||||||
for template in [start, end]:
|
for template in [start, end]:
|
||||||
if template is not None:
|
if template is not None:
|
||||||
template.hass = hass
|
template.hass = hass
|
||||||
|
|
||||||
add_devices([HistoryStatsSensor(
|
add_devices([HistoryStatsSensor(hass, entity_id, entity_state, start, end,
|
||||||
hass, entity_id, entity_state, start, end, duration, name)])
|
duration, sensor_type, name)])
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -90,7 +100,8 @@ class HistoryStatsSensor(Entity):
|
|||||||
"""Representation of a HistoryStats sensor."""
|
"""Representation of a HistoryStats sensor."""
|
||||||
|
|
||||||
def __init__(
|
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."""
|
"""Initialize the HistoryStats sensor."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@ -99,11 +110,13 @@ class HistoryStatsSensor(Entity):
|
|||||||
self._duration = duration
|
self._duration = duration
|
||||||
self._start = start
|
self._start = start
|
||||||
self._end = end
|
self._end = end
|
||||||
|
self._type = sensor_type
|
||||||
self._name = name
|
self._name = name
|
||||||
self._unit_of_measurement = UNIT
|
self._unit_of_measurement = UNITS[sensor_type]
|
||||||
|
|
||||||
self._period = (datetime.datetime.now(), datetime.datetime.now())
|
self._period = (datetime.datetime.now(), datetime.datetime.now())
|
||||||
self.value = 0
|
self.value = 0
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
def force_refresh(*args):
|
def force_refresh(*args):
|
||||||
"""Force the component to refresh."""
|
"""Force the component to refresh."""
|
||||||
@ -123,8 +136,15 @@ class HistoryStatsSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
|
if self._type == CONF_TYPE_TIME:
|
||||||
return round(self.value, 2)
|
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
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit the value is expressed in."""
|
"""Return the unit the value is expressed in."""
|
||||||
@ -142,7 +162,6 @@ class HistoryStatsSensor(Entity):
|
|||||||
hsh = HistoryStatsHelper
|
hsh = HistoryStatsHelper
|
||||||
return {
|
return {
|
||||||
ATTR_VALUE: hsh.pretty_duration(self.value),
|
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_START: start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
ATTR_END: end.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_state == self._entity_state)
|
||||||
last_time = dt_util.as_timestamp(start)
|
last_time = dt_util.as_timestamp(start)
|
||||||
elapsed = 0
|
elapsed = 0
|
||||||
|
count = 0
|
||||||
|
|
||||||
# Make calculations
|
# Make calculations
|
||||||
for item in history_list.get(self._entity_id):
|
for item in history_list.get(self._entity_id):
|
||||||
@ -183,6 +203,8 @@ class HistoryStatsSensor(Entity):
|
|||||||
|
|
||||||
if last_state:
|
if last_state:
|
||||||
elapsed += current_time - last_time
|
elapsed += current_time - last_time
|
||||||
|
if current_state and not last_state:
|
||||||
|
count += 1
|
||||||
|
|
||||||
last_state = current_state
|
last_state = current_state
|
||||||
last_time = current_time
|
last_time = current_time
|
||||||
@ -196,6 +218,9 @@ class HistoryStatsSensor(Entity):
|
|||||||
# Save value in hours
|
# Save value in hours
|
||||||
self.value = elapsed / 3600
|
self.value = elapsed / 3600
|
||||||
|
|
||||||
|
# Save counter
|
||||||
|
self.count = count
|
||||||
|
|
||||||
def update_period(self):
|
def update_period(self):
|
||||||
"""Parse the templates and store a datetime tuple in _period."""
|
"""Parse the templates and store a datetime tuple in _period."""
|
||||||
start = None
|
start = None
|
||||||
@ -267,10 +292,10 @@ class HistoryStatsHelper:
|
|||||||
def pretty_ratio(value, period):
|
def pretty_ratio(value, period):
|
||||||
"""Format the ratio of value / period duration."""
|
"""Format the ratio of value / period duration."""
|
||||||
if len(period) != 2 or period[0] == period[1]:
|
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()
|
ratio = 100 * 3600 * value / (period[1] - period[0]).total_seconds()
|
||||||
return str(round(ratio, 1)) + UNIT_RATIO
|
return round(ratio, 1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_template_exception(ex, field):
|
def handle_template_exception(ex, field):
|
||||||
|
234
homeassistant/components/sensor/lyft.py
Normal file
234
homeassistant/components/sensor/lyft.py
Normal 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
|
@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['astral==1.3.4']
|
REQUIREMENTS = ['astral==1.4']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -19,12 +19,16 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_FORCE_UPDATE = 'force_update'
|
||||||
|
|
||||||
DEFAULT_NAME = 'MQTT Sensor'
|
DEFAULT_NAME = 'MQTT Sensor'
|
||||||
|
DEFAULT_FORCE_UPDATE = False
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ['mqtt']
|
||||||
|
|
||||||
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): 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_STATE_TOPIC),
|
||||||
config.get(CONF_QOS),
|
config.get(CONF_QOS),
|
||||||
config.get(CONF_UNIT_OF_MEASUREMENT),
|
config.get(CONF_UNIT_OF_MEASUREMENT),
|
||||||
|
config.get(CONF_FORCE_UPDATE),
|
||||||
value_template,
|
value_template,
|
||||||
)])
|
)])
|
||||||
|
|
||||||
@ -51,13 +56,14 @@ class MqttSensor(Entity):
|
|||||||
"""Representation of a sensor that can be updated using MQTT."""
|
"""Representation of a sensor that can be updated using MQTT."""
|
||||||
|
|
||||||
def __init__(self, name, state_topic, qos, unit_of_measurement,
|
def __init__(self, name, state_topic, qos, unit_of_measurement,
|
||||||
value_template):
|
force_update, value_template):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state_topic = state_topic
|
self._state_topic = state_topic
|
||||||
self._qos = qos
|
self._qos = qos
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
|
self._force_update = force_update
|
||||||
self._template = value_template
|
self._template = value_template
|
||||||
|
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
@ -92,6 +98,11 @@ class MqttSensor(Entity):
|
|||||||
"""Return the unit this state is expressed in."""
|
"""Return the unit this state is expressed in."""
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def force_update(self):
|
||||||
|
"""Force update."""
|
||||||
|
return self._force_update
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
|
@ -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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.neurio_energy/
|
https://home-assistant.io/components/sensor.neurio_energy/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
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.helpers.entity import Entity
|
||||||
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['neurio==0.3.1']
|
REQUIREMENTS = ['neurio==0.3.1']
|
||||||
|
|
||||||
@ -21,48 +24,133 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
CONF_API_SECRET = 'api_secret'
|
CONF_API_SECRET = 'api_secret'
|
||||||
CONF_SENSOR_ID = 'sensor_id'
|
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'
|
ICON = 'mdi:flash'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=150)
|
||||||
|
MIN_TIME_BETWEEN_ACTIVE_UPDATES = timedelta(seconds=10)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
vol.Required(CONF_API_SECRET): cv.string,
|
vol.Required(CONF_API_SECRET): cv.string,
|
||||||
vol.Optional(CONF_SENSOR_ID): 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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Neurio sensor."""
|
"""Setup the Neurio sensor."""
|
||||||
name = config.get(CONF_NAME)
|
|
||||||
api_key = config.get(CONF_API_KEY)
|
api_key = config.get(CONF_API_KEY)
|
||||||
api_secret = config.get(CONF_API_SECRET)
|
api_secret = config.get(CONF_API_SECRET)
|
||||||
sensor_id = config.get(CONF_SENSOR_ID)
|
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
|
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_key = api_key
|
||||||
self.api_secret = api_secret
|
self.api_secret = api_secret
|
||||||
self.sensor_id = sensor_id
|
self.sensor_id = sensor_id
|
||||||
|
|
||||||
|
self._daily_usage = None
|
||||||
|
self._active_power = None
|
||||||
|
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
|
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'
|
self._unit_of_measurement = 'W'
|
||||||
|
elif sensor_type == DAILY_TYPE:
|
||||||
|
self._unit_of_measurement = 'kWh'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -85,14 +173,10 @@ class NeurioEnergy(Entity):
|
|||||||
return ICON
|
return ICON
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the Neurio monitor data from the web service."""
|
"""Get the latest data, update state."""
|
||||||
import neurio
|
self.update_sensor()
|
||||||
try:
|
|
||||||
neurio_tp = neurio.TokenProvider(
|
if self._sensor_type == ACTIVE_TYPE:
|
||||||
key=self.api_key, secret=self.api_secret)
|
self._state = self._data.active_power
|
||||||
neurio_client = neurio.Client(token_provider=neurio_tp)
|
elif self._sensor_type == DAILY_TYPE:
|
||||||
sample = neurio_client.get_samples_live_last(
|
self._state = self._data.daily_usage
|
||||||
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)
|
|
||||||
|
@ -127,6 +127,6 @@ class OctoPrintSensor(Entity):
|
|||||||
# Error calling the api, already logged in api.update()
|
# Error calling the api, already logged in api.update()
|
||||||
return
|
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)
|
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
|
||||||
return
|
return
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['psutil==5.1.3']
|
REQUIREMENTS = ['psutil==5.2.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
209
homeassistant/components/sensor/tado.py
Normal file
209
homeassistant/components/sensor/tado.py
Normal 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 = {}
|
@ -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):
|
def convert_pid(value):
|
||||||
"""Convert pid from hex string to integer."""
|
"""Convert pid from hex string to integer."""
|
||||||
return int(value, 16)
|
return int(value, 16)
|
||||||
@ -94,10 +89,10 @@ class TorqueReceiveDataView(HomeAssistantView):
|
|||||||
|
|
||||||
if is_name:
|
if is_name:
|
||||||
pid = convert_pid(is_name.group(1))
|
pid = convert_pid(is_name.group(1))
|
||||||
names[pid] = decode(data[key])
|
names[pid] = data[key]
|
||||||
elif is_unit:
|
elif is_unit:
|
||||||
pid = convert_pid(is_unit.group(1))
|
pid = convert_pid(is_unit.group(1))
|
||||||
units[pid] = decode(data[key])
|
units[pid] = data[key]
|
||||||
elif is_value:
|
elif is_value:
|
||||||
pid = convert_pid(is_value.group(1))
|
pid = convert_pid(is_value.group(1))
|
||||||
if pid in self.sensors:
|
if pid in self.sensors:
|
||||||
@ -110,7 +105,7 @@ class TorqueReceiveDataView(HomeAssistantView):
|
|||||||
units.get(pid, None))
|
units.get(pid, None))
|
||||||
hass.async_add_job(self.add_devices, [self.sensors[pid]])
|
hass.async_add_job(self.add_devices, [self.sensors[pid]])
|
||||||
|
|
||||||
return None
|
return "OK!"
|
||||||
|
|
||||||
|
|
||||||
class TorqueSensor(Entity):
|
class TorqueSensor(Entity):
|
||||||
|
@ -15,34 +15,32 @@ from homeassistant.components.zwave import async_setup_platform # noqa # pylint
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_device(node, value, **kwargs):
|
def get_device(node, values, **kwargs):
|
||||||
"""Create zwave entity device."""
|
"""Create zwave entity device."""
|
||||||
# Generic Device mappings
|
# 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):
|
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 \
|
if node.has_command_class(zwave.const.COMMAND_CLASS_METER) and \
|
||||||
value.type == zwave.const.TYPE_DECIMAL:
|
values.primary.type == zwave.const.TYPE_DECIMAL:
|
||||||
return ZWaveMultilevelSensor(value)
|
return ZWaveMultilevelSensor(values)
|
||||||
if node.has_command_class(zwave.const.COMMAND_CLASS_ALARM) or \
|
if node.has_command_class(zwave.const.COMMAND_CLASS_ALARM) or \
|
||||||
node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_ALARM):
|
node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_ALARM):
|
||||||
return ZWaveAlarmSensor(value)
|
return ZWaveAlarmSensor(values)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ZWaveSensor(zwave.ZWaveDeviceEntity):
|
class ZWaveSensor(zwave.ZWaveDeviceEntity):
|
||||||
"""Representation of a Z-Wave sensor."""
|
"""Representation of a Z-Wave sensor."""
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, values):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Callback on data changes for node values."""
|
"""Callback on data changes for node values."""
|
||||||
self._state = self._value.data
|
self._state = self.values.primary.data
|
||||||
self._units = self._value.units
|
self._units = self.values.primary.units
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def force_update(self):
|
def force_update(self):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user