Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions custom_components/isy994/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
SUPPORTED_DOMAINS,
SUPPORTED_PROGRAM_DOMAINS,
SUPPORTED_VARIABLE_DOMAINS,
UNDO_UPDATE_LISTENER,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -459,8 +460,15 @@ async def async_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Set up the ISY 994 platform."""
# As there currently is no way to import options from yaml
# when setting up a config entry, we fallback to adding
# the options to the config entry and pull them out here if
# they are missing from the options
_async_import_options_from_data_if_missing(hass, entry)

hass.data[DOMAIN][entry.entry_id] = {}
hass_isy_data = hass.data[DOMAIN][entry.entry_id]

hass_isy_data[ISY994_NODES] = {}
for domain in SUPPORTED_DOMAINS:
hass_isy_data[ISY994_NODES][domain] = []
Expand All @@ -474,6 +482,7 @@ async def async_setup_entry(
hass_isy_data[ISY994_VARIABLES][domain] = []

isy_config = entry.data
isy_options = entry.options

# Required
user = isy_config[CONF_USERNAME]
Expand All @@ -482,8 +491,8 @@ async def async_setup_entry(

# Optional
tls_version = isy_config.get(CONF_TLS_VER)
ignore_identifier = isy_config.get(CONF_IGNORE_STRING)
sensor_identifier = isy_config.get(CONF_SENSOR_STRING)
ignore_identifier = isy_options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
sensor_identifier = isy_options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)
isy_variables = isy_config.get(CONF_ISY_VARIABLES, {})

if host.scheme == "http":
Expand Down Expand Up @@ -547,9 +556,35 @@ def _start_auto_update() -> None:

await hass.async_add_executor_job(_start_auto_update)

undo_listener = entry.add_update_listener(_async_update_listener)

hass_isy_data[UNDO_UPDATE_LISTENER] = undo_listener

return True


async def _async_update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)


@callback
def _async_import_options_from_data_if_missing(
hass: HomeAssistant, entry: config_entries.ConfigEntry
):
options = dict(entry.options)
modified = False
for importable_option in [CONF_IGNORE_STRING, CONF_SENSOR_STRING]:
if importable_option not in entry.options and importable_option in entry.data:
options[importable_option] = entry.data[importable_option]
modified = True

if modified:
hass.config_entries.async_update_entry(entry, options=options)


async def _async_get_or_create_isy_device_in_registry(
hass: HomeAssistant, entry: config_entries.ConfigEntry, isy
) -> None:
Expand Down Expand Up @@ -579,7 +614,9 @@ async def async_unload_entry(
)
)

isy = hass.data[DOMAIN][entry.entry_id][ISY994_ISY]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]

isy = hass_isy_data[ISY994_ISY]

def _stop_auto_update() -> None:
"""Start isy auto update."""
Expand All @@ -588,6 +625,8 @@ def _stop_auto_update() -> None:

await hass.async_add_executor_job(_stop_auto_update)

hass_isy_data[UNDO_UPDATE_LISTENER]()

if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

Expand Down
46 changes: 43 additions & 3 deletions custom_components/isy994/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback

from .const import (
CONF_IGNORE_STRING,
CONF_RESTORE_LIGHT_STATE,
CONF_SENSOR_STRING,
CONF_TLS_VER,
DEFAULT_IGNORE_STRING,
DEFAULT_RESTORE_LIGHT_STATE,
DEFAULT_SENSOR_STRING,
DEFAULT_TLS_VERSION,
)
from .const import DOMAIN # pylint:disable=unused-import

Expand All @@ -27,9 +31,7 @@
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_TLS_VER): vol.Coerce(float),
vol.Optional(CONF_IGNORE_STRING, default=DEFAULT_IGNORE_STRING): str,
vol.Optional(CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING): str
vol.Optional(CONF_TLS_VER, default=DEFAULT_TLS_VERSION): vol.In([1.1, 1.2]),
# Variables require yaml
},
extra=vol.ALLOW_EXTRA,
Expand Down Expand Up @@ -120,6 +122,44 @@ async def async_step_import(self, user_input):
"""Handle import."""
return await self.async_step_user(user_input)

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for isy994."""

def __init__(self, config_entry: config_entries.ConfigEntry):
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

options = self.config_entry.options
restore_light_state = options.get(
CONF_RESTORE_LIGHT_STATE, DEFAULT_RESTORE_LIGHT_STATE
)
ignore_string = options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
sensor_string = options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)

options_schema = vol.Schema(
{
vol.Required(
CONF_RESTORE_LIGHT_STATE, default=restore_light_state
): bool,
vol.Optional(CONF_IGNORE_STRING, default=ignore_string): str,
vol.Optional(CONF_SENSOR_STRING, default=sensor_string): str,
}
)

return self.async_show_form(step_id="init", data_schema=options_schema,)


class InvalidHost(exceptions.HomeAssistantError):
"""Error to indicate the host value is invalid."""
Expand Down
7 changes: 7 additions & 0 deletions custom_components/isy994/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,18 @@

MANUFACTURER = "Universal Devices, Inc"

ATTR_LAST_BRIGHTNESS = "last_brightness"

CONF_IGNORE_STRING = "ignore_string"
CONF_SENSOR_STRING = "sensor_string"
CONF_ISY_VARIABLES = "isy_variables"
CONF_TLS_VER = "tls"
CONF_RESTORE_LIGHT_STATE = "restore_light_state"

DEFAULT_IGNORE_STRING = "{IGNORE ME}"
DEFAULT_SENSOR_STRING = "sensor"
DEFAULT_RESTORE_LIGHT_STATE = False
DEFAULT_TLS_VERSION = 1.1

DEFAULT_ON_VALUE = 1
DEFAULT_OFF_VALUE = 0
Expand Down Expand Up @@ -216,6 +221,8 @@
ISY994_PROGRAMS = "isy994_programs"
ISY994_VARIABLES = "isy994_variables"

UNDO_UPDATE_LISTENER = "undo_update_listener"

ISY_HVAC_MODES = [
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
Expand Down
53 changes: 49 additions & 4 deletions custom_components/isy994/light.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
"""Support for ISY994 lights."""
import logging
from typing import Callable
from typing import Callable, Dict

from pyisy.constants import ISY_VALUE_UNKNOWN

from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, Light
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import HomeAssistantType

from . import ISYDevice, migrate_old_unique_ids
from .const import DOMAIN as ISY994_DOMAIN, ISY994_NODES
from .const import (
ATTR_LAST_BRIGHTNESS,
CONF_RESTORE_LIGHT_STATE,
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
)

_LOGGER = logging.getLogger(__name__)

Expand All @@ -22,17 +28,26 @@ async def async_setup_entry(
) -> bool:
"""Set up the ISY994 light platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
isy_options = entry.options
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)

devices = []
for node in hass_isy_data[ISY994_NODES][DOMAIN]:
devices.append(ISYLightDevice(node))
devices.append(ISYLightDevice(node, restore_light_state))

await migrate_old_unique_ids(hass, DOMAIN, devices)
async_add_entities(devices)


class ISYLightDevice(ISYDevice, Light):
class ISYLightDevice(ISYDevice, Light, RestoreEntity):
"""Representation of an ISY994 light device."""

def __init__(self, node, restore_light_state) -> None:
"""Initialize the ISY994 light device."""
super().__init__(node)
self._last_brightness = self.brightness
self._restore_light_state = restore_light_state

@property
def is_on(self) -> bool:
"""Get whether the ISY994 light is on."""
Expand All @@ -45,18 +60,48 @@ def brightness(self) -> float:
"""Get the brightness of the ISY994 light."""
return STATE_UNKNOWN if self.value == ISY_VALUE_UNKNOWN else int(self.value)

@property
def device_state_attributes(self) -> Dict:
"""Return the light attributes."""
attribs = super().device_state_attributes
attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness
return attribs

def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device."""
self._last_brightness = self.brightness
if not self._node.turn_off():
_LOGGER.debug("Unable to turn off light")

def on_update(self, event: object) -> None:
"""Save brightness in the update event from the ISY994 Node."""
if self.value not in (0, ISY_VALUE_UNKNOWN):
self._last_brightness = self.value
super().on_update(event)

# pylint: disable=arguments-differ
def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""
if self._restore_light_state and brightness is None and self._last_brightness:
brightness = self._last_brightness
if not self._node.turn_on(val=brightness):
_LOGGER.debug("Unable to turn on light")

@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS

async def async_added_to_hass(self) -> None:
"""Restore last_brightness on restart."""
await super().async_added_to_hass()

last_state = await self.async_get_last_state()
if not last_state:
return

if (
ATTR_LAST_BRIGHTNESS in last_state.attributes
and last_state.attributes[ATTR_LAST_BRIGHTNESS]
):
self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS]
19 changes: 15 additions & 4 deletions custom_components/isy994/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
"step" : {
"user" : {
"data" : {
"sensor_string" : "Sensor String: If this string is found in the device name or folder, Home Assistant will assume it is as a sensor or binary sensor.",
"username" : "Username",
"host" : "URL",
"ignore_string" : "Ignore String: Any devices that contain this string in their name will be ignored.",
"password" : "Password",
"tls" : "The TLS version of the ISY controller. This value can be either 1.1 or 1.2"
"tls" : "The TLS version of the ISY controller."
},
"description" : "The host entry must be in full URL format, e.g., http://192.168.10.100:80",
"title" : "Connect to your ISY994"
Expand All @@ -24,5 +22,18 @@
"abort" : {
"already_configured" : "Device is already configured"
}
}
},
"options": {
"step": {
"init": {
"title" : "ISY994 Options",
"description": "Any device or folder that contains 'Sensor String' in the name will be treated as a sensor or binary sensor. Any device with 'Ignore String' in the name will be ignored. If 'Restore Light Brightness' is enabled, the previous brightness will be restored when turning on a light instead of the On-Level.",
"data": {
"sensor_string" : "Sensor String",
"ignore_string" : "Ignore String",
"restore_light_state" : "Restore Light Brightness"
}
}
}
}
}
19 changes: 15 additions & 4 deletions custom_components/isy994/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
"step" : {
"user" : {
"data" : {
"sensor_string" : "Sensor String: If this string is found in the device name or folder, Home Assistant will assume it is as a sensor or binary sensor.",
"username" : "Username",
"host" : "URL",
"ignore_string" : "Ignore String: Any devices that contain this string in their name will be ignored.",
"password" : "Password",
"tls" : "The TLS version of the ISY controller. This value can be either 1.1 or 1.2"
"tls" : "The TLS version of the ISY controller."
},
"description" : "The host entry must be in full URL format, e.g., http://192.168.10.100:80",
"title" : "Connect to your ISY994"
Expand All @@ -24,5 +22,18 @@
"abort" : {
"already_configured" : "Device is already configured"
}
}
},
"options": {
"step": {
"init": {
"title" : "ISY994 Options",
"description": "Any device or folder that contains 'Sensor String' in the name will be treated as a sensor or binary sensor. Any device with 'Ignore String' in the name will be ignored. If 'Restore Light Brightness' is enabled, the previous brightness will be restored when turning on a light instead of the On-Level.",
"data": {
"sensor_string" : "Sensor String",
"ignore_string" : "Ignore String",
"restore_light_state" : "Restore Light Brightness"
}
}
}
}
}