hass-core/homeassistant/components/isy994.py
Ryan Kraus 5dd45efac3 Updated ISY component to not overwrite state_attributes. (#5433)
* Updated ISY component to not overwrite state_attributes.

The ISY component included an ISYDevice base class that is used by all
of the isy994 platforms. This still overwrote the state_attributes
property instead of the more appropriate device_state_attributes
property. This was also repeated in the isy994 light platform. Both of
these were addressed. This also fixes issue #5428.

* Removed custom state attributes from ISY lights.

The brightness attribute need not be manually reported by the isy994
light platform.

* Removed ISY Node cleanup.

The ISY entities don’t really need to unsubscribe themselves while hass
is shutting down. Because these updates are not sent in a thread, there
is no negative impact from shutting down without unsubscribing. This
greatly speeds up hass shutdown.

* Removed unused attribute from isy994 light platform.

* Cleaned up ISY994 light entity class.

1) Removed the state property. This property is set in the Entity base
class and shouldn’t be overridden here.
2) Set the brightness property. This is the proper way of setting the
brightness for the Light base class.
3) Removed properties that are now unused because of these changes.
2017-01-19 22:22:33 -08:00

296 lines
8.6 KiB
Python

"""
Support the ISY-994 controllers.
For configuration details please visit the documentation for this component at
https://home-assistant.io/components/isy994/
"""
from collections import namedtuple
import logging
from urllib.parse import urlparse
import voluptuous as vol
from homeassistant.core import HomeAssistant # noqa
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa
DOMAIN = "isy994"
REQUIREMENTS = ['PyISY==1.0.7']
ISY = None
DEFAULT_SENSOR_STRING = 'sensor'
DEFAULT_HIDDEN_STRING = '{HIDE ME}'
CONF_TLS_VER = 'tls'
CONF_HIDDEN_STRING = 'hidden_string'
CONF_SENSOR_STRING = 'sensor_string'
KEY_MY_PROGRAMS = 'My Programs'
KEY_FOLDER = 'folder'
KEY_ACTIONS = 'actions'
KEY_STATUS = 'status'
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.url,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TLS_VER): vol.Coerce(float),
vol.Optional(CONF_HIDDEN_STRING,
default=DEFAULT_HIDDEN_STRING): cv.string,
vol.Optional(CONF_SENSOR_STRING,
default=DEFAULT_SENSOR_STRING): cv.string
})
}, extra=vol.ALLOW_EXTRA)
SENSOR_NODES = []
WEATHER_NODES = []
NODES = []
GROUPS = []
PROGRAMS = {}
PYISY = None
HIDDEN_STRING = DEFAULT_HIDDEN_STRING
SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock',
'sensor', 'switch']
WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom'))
def filter_nodes(nodes: list, units: list=None, states: list=None) -> list:
"""Filter a list of ISY nodes based on the units and states provided."""
filtered_nodes = []
units = units if units else []
states = states if states else []
for node in nodes:
match_unit = False
match_state = True
for uom in node.uom:
if uom in units:
match_unit = True
continue
elif uom not in states:
match_state = False
if match_unit:
continue
if match_unit or match_state:
filtered_nodes.append(node)
return filtered_nodes
def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
"""Categorize the ISY994 nodes."""
global SENSOR_NODES
global NODES
global GROUPS
SENSOR_NODES = []
NODES = []
GROUPS = []
# pylint: disable=no-member
for (path, node) in ISY.nodes:
hidden = hidden_identifier in path or hidden_identifier in node.name
if hidden:
node.name += hidden_identifier
if sensor_identifier in path or sensor_identifier in node.name:
SENSOR_NODES.append(node)
elif isinstance(node, PYISY.Nodes.Node):
NODES.append(node)
elif isinstance(node, PYISY.Nodes.Group):
GROUPS.append(node)
def _categorize_programs() -> None:
"""Categorize the ISY994 programs."""
global PROGRAMS
PROGRAMS = {}
for component in SUPPORTED_DOMAINS:
try:
folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component]
except KeyError:
pass
else:
for dtype, _, node_id in folder.children:
if dtype is KEY_FOLDER:
program = folder[node_id]
try:
node = program[KEY_STATUS].leaf
assert node.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
if component not in PROGRAMS:
PROGRAMS[component] = []
PROGRAMS[component].append(program)
def _categorize_weather() -> None:
"""Categorize the ISY994 weather data."""
global WEATHER_NODES
climate_attrs = dir(ISY.climate)
WEATHER_NODES = [WeatherNode(getattr(ISY.climate, attr), attr,
getattr(ISY.climate, attr + '_units'))
for attr in climate_attrs
if attr + '_units' in climate_attrs]
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ISY 994 platform."""
isy_config = config.get(DOMAIN)
user = isy_config.get(CONF_USERNAME)
password = isy_config.get(CONF_PASSWORD)
tls_version = isy_config.get(CONF_TLS_VER)
host = urlparse(isy_config.get(CONF_HOST))
port = host.port
addr = host.geturl()
hidden_identifier = isy_config.get(CONF_HIDDEN_STRING,
DEFAULT_HIDDEN_STRING)
sensor_identifier = isy_config.get(CONF_SENSOR_STRING,
DEFAULT_SENSOR_STRING)
global HIDDEN_STRING
HIDDEN_STRING = hidden_identifier
if host.scheme == 'http':
addr = addr.replace('http://', '')
https = False
elif host.scheme == 'https':
addr = addr.replace('https://', '')
https = True
else:
_LOGGER.error('isy994 host value in configuration is invalid.')
return False
addr = addr.replace(':{}'.format(port), '')
import PyISY
global PYISY
PYISY = PyISY
# Connect to ISY controller.
global ISY
ISY = PyISY.ISY(addr, port, username=user, password=password,
use_https=https, tls_ver=tls_version, log=_LOGGER)
if not ISY.connected:
return False
_categorize_nodes(hidden_identifier, sensor_identifier)
_categorize_programs()
if ISY.configuration.get('Weather Information'):
_categorize_weather()
# Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# Load platforms for the devices in the ISY controller that we support.
for component in SUPPORTED_DOMAINS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
ISY.auto_update = True
return True
# pylint: disable=unused-argument
def stop(event: object) -> None:
"""Stop ISY auto updates."""
ISY.auto_update = False
class ISYDevice(Entity):
"""Representation of an ISY994 device."""
_attrs = {}
_domain = None # type: str
_name = None # type: str
def __init__(self, node) -> None:
"""Initialize the insteon device."""
self._node = node
self._change_handler = self._node.status.subscribe('changed',
self.on_update)
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node."""
self.update_ha_state()
@property
def domain(self) -> str:
"""Get the domain of the device."""
return self._domain
@property
def unique_id(self) -> str:
"""Get the unique identifier of the device."""
# pylint: disable=protected-access
return self._node._id
@property
def raw_name(self) -> str:
"""Get the raw name of the device."""
return str(self._name) \
if self._name is not None else str(self._node.name)
@property
def name(self) -> str:
"""Get the name of the device."""
return self.raw_name.replace(HIDDEN_STRING, '').strip() \
.replace('_', ' ')
@property
def should_poll(self) -> bool:
"""No polling required since we're using the subscription."""
return False
@property
def value(self) -> object:
"""Get the current value of the device."""
# pylint: disable=protected-access
return self._node.status._val
@property
def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device."""
attr = {}
if hasattr(self._node, 'aux_properties'):
for name, val in self._node.aux_properties.items():
attr[name] = '{} {}'.format(val.get('value'), val.get('uom'))
return attr
@property
def hidden(self) -> bool:
"""Get whether the device should be hidden from the UI."""
return HIDDEN_STRING in self.raw_name
@property
def unit_of_measurement(self) -> str:
"""Get the device unit of measure."""
return None
def _attr_filter(self, attr: str) -> str:
"""Filter the attribute."""
# pylint: disable=no-self-use
return attr
def update(self) -> None:
"""Perform an update for the device."""
pass