mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
define a adapt_brightness and adapt_color switch and watch for color or brightness related data in light.turn_on
This commit is contained in:
parent
7555638b6f
commit
8b6527fd14
2 changed files with 138 additions and 73 deletions
|
|
@ -12,9 +12,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight"
|
|||
|
||||
CONF_NAME, DEFAULT_NAME = "name", "default"
|
||||
CONF_LIGHTS, DEFAULT_LIGHTS = "lights", []
|
||||
CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS = "adapt_brightness", True
|
||||
CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP = "adapt_color_temp", True
|
||||
CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR = "adapt_rgb_color", True
|
||||
CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
|
||||
"detect_non_ha_changes",
|
||||
False,
|
||||
|
|
@ -37,9 +34,13 @@ CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
|
|||
CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45
|
||||
|
||||
SLEEP_MODE_SWITCH = "sleep_mode_switch"
|
||||
ADAPT_COLOR_SWITCH = "adapt_color_switch"
|
||||
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
|
||||
ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
NONE_STR = "None"
|
||||
ATTR_ADAPT_COLOR = "adapt_color"
|
||||
ATTR_ADAPT_BRIGHTNESS = "adapt_brightness"
|
||||
|
||||
SERVICE_SET_MANUAL_CONTROL = "set_manual_control"
|
||||
CONF_MANUAL_CONTROL = "manual_control"
|
||||
|
|
@ -56,9 +57,6 @@ def int_between(min_int, max_int):
|
|||
|
||||
VALIDATION_TUPLES = [
|
||||
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
|
||||
(CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool),
|
||||
(CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool),
|
||||
(CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool),
|
||||
(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
|
||||
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
|
||||
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
|
||||
|
|
|
|||
|
|
@ -19,9 +19,17 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_BRIGHTNESS_STEP,
|
||||
ATTR_BRIGHTNESS_STEP_PCT,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_XY_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
|
|
@ -64,6 +72,7 @@ from homeassistant.helpers.event import (
|
|||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.sun import get_astral_location
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.color import (
|
||||
color_RGB_to_xy,
|
||||
color_temperature_kelvin_to_mired,
|
||||
|
|
@ -73,10 +82,11 @@ from homeassistant.util.color import (
|
|||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ADAPT_BRIGHTNESS_SWITCH,
|
||||
ADAPT_COLOR_SWITCH,
|
||||
ATTR_ADAPT_BRIGHTNESS,
|
||||
ATTR_ADAPT_COLOR,
|
||||
ATTR_TURN_ON_OFF_LISTENER,
|
||||
CONF_ADAPT_BRIGHTNESS,
|
||||
CONF_ADAPT_COLOR_TEMP,
|
||||
CONF_ADAPT_RGB_COLOR,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
CONF_INTERVAL,
|
||||
|
|
@ -129,6 +139,23 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range
|
|||
COLOR_TEMP_CHANGE = 20 # ≈5% of total range
|
||||
RGB_REDMEAN_CHANGE = 80 # ≈10% of total range
|
||||
|
||||
COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_WHITE_VALUE, # Should this be here?
|
||||
ATTR_XY_COLOR,
|
||||
}
|
||||
|
||||
BRIGHTNESS_ATTRS = {
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_BRIGHTNESS_STEP,
|
||||
ATTR_BRIGHTNESS_STEP_PCT,
|
||||
}
|
||||
|
||||
# Keep a short domain version for the context instances (which can only be 36 chars)
|
||||
_DOMAIN_SHORT = "adapt_lgt"
|
||||
|
||||
|
|
@ -165,9 +192,8 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
|
|||
await switch._adapt_light( # pylint: disable=protected-access
|
||||
light,
|
||||
data[CONF_TRANSITION],
|
||||
data[CONF_ADAPT_BRIGHTNESS],
|
||||
data[CONF_ADAPT_COLOR_TEMP],
|
||||
data[CONF_ADAPT_RGB_COLOR],
|
||||
data[ATTR_ADAPT_BRIGHTNESS],
|
||||
data[ATTR_ADAPT_COLOR],
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
|
@ -214,13 +240,27 @@ async def async_setup_entry(
|
|||
data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass)
|
||||
turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER]
|
||||
|
||||
sleep_mode_switch = AdaptiveSleepModeSwitch(hass, config_entry)
|
||||
switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener, sleep_mode_switch)
|
||||
sleep_mode_switch = SimpleSwitch("Sleep Mode", False, hass, config_entry)
|
||||
adapt_color_switch = SimpleSwitch("Adapt Color", True, hass, config_entry)
|
||||
adapt_brightness_switch = SimpleSwitch("Adapt Brightness", True, hass, config_entry)
|
||||
switch = AdaptiveSwitch(
|
||||
hass,
|
||||
config_entry,
|
||||
turn_on_off_listener,
|
||||
sleep_mode_switch,
|
||||
adapt_color_switch,
|
||||
adapt_brightness_switch,
|
||||
)
|
||||
|
||||
data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch
|
||||
data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch
|
||||
data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch
|
||||
data[config_entry.entry_id][SWITCH_DOMAIN] = switch
|
||||
|
||||
async_add_entities([switch, sleep_mode_switch], update_before_add=True)
|
||||
async_add_entities(
|
||||
[switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch],
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
# Register `apply` service
|
||||
platform = entity_platform.current_platform.get()
|
||||
|
|
@ -232,9 +272,8 @@ async def async_setup_entry(
|
|||
CONF_TRANSITION,
|
||||
default=switch._initial_transition, # pylint: disable=protected-access
|
||||
): VALID_TRANSITION,
|
||||
vol.Optional(CONF_ADAPT_BRIGHTNESS, default=True): cv.boolean,
|
||||
vol.Optional(CONF_ADAPT_COLOR_TEMP, default=True): cv.boolean,
|
||||
vol.Optional(CONF_ADAPT_RGB_COLOR, default=True): cv.boolean,
|
||||
vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean,
|
||||
vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean,
|
||||
vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
|
||||
},
|
||||
handle_apply,
|
||||
|
|
@ -264,7 +303,7 @@ def validate(config_entry: ConfigEntry):
|
|||
return data
|
||||
|
||||
|
||||
def match_state_event(event: Event, from_or_to_state: List[str]):
|
||||
def match_switch_state_event(event: Event, from_or_to_state: List[str]):
|
||||
"""Match state event when either 'from_state' or 'to_state' matches."""
|
||||
old_state = event.data.get("old_state")
|
||||
from_state_match = old_state is not None and old_state.state in from_or_to_state
|
||||
|
|
@ -324,8 +363,7 @@ def _attributes_have_changed(
|
|||
old_attributes: Dict[str, Any],
|
||||
new_attributes: Dict[str, Any],
|
||||
adapt_brightness: bool,
|
||||
adapt_color_temp: bool,
|
||||
adapt_rgb_color: bool,
|
||||
adapt_color: bool,
|
||||
context: Context,
|
||||
) -> bool:
|
||||
if (
|
||||
|
|
@ -347,7 +385,7 @@ def _attributes_have_changed(
|
|||
return True
|
||||
|
||||
if (
|
||||
adapt_color_temp
|
||||
adapt_color
|
||||
and ATTR_COLOR_TEMP in old_attributes
|
||||
and ATTR_COLOR_TEMP in new_attributes
|
||||
):
|
||||
|
|
@ -365,7 +403,7 @@ def _attributes_have_changed(
|
|||
return True
|
||||
|
||||
if (
|
||||
adapt_rgb_color
|
||||
adapt_color
|
||||
and ATTR_RGB_COLOR in old_attributes
|
||||
and ATTR_RGB_COLOR in new_attributes
|
||||
):
|
||||
|
|
@ -407,20 +445,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
hass,
|
||||
config_entry: ConfigEntry,
|
||||
turn_on_off_listener: TurnOnOffListener,
|
||||
sleep_mode_switch: AdaptiveSleepModeSwitch,
|
||||
sleep_mode_switch: SimpleSwitch,
|
||||
adapt_color_switch: SimpleSwitch,
|
||||
adapt_brightness_switch: SimpleSwitch,
|
||||
):
|
||||
"""Initialize the Adaptive Lighting switch."""
|
||||
self.hass = hass
|
||||
self.turn_on_off_listener = turn_on_off_listener
|
||||
self.sleep_mode_switch = sleep_mode_switch
|
||||
self.adapt_color_switch = adapt_color_switch
|
||||
self.adapt_brightness_switch = adapt_brightness_switch
|
||||
|
||||
data = validate(config_entry)
|
||||
self._name = data[CONF_NAME]
|
||||
self._lights = data[CONF_LIGHTS]
|
||||
|
||||
self._adapt_brightness = data[CONF_ADAPT_BRIGHTNESS]
|
||||
self._adapt_color_temp = data[CONF_ADAPT_COLOR_TEMP]
|
||||
self._adapt_rgb_color = data[CONF_ADAPT_RGB_COLOR]
|
||||
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
|
||||
self._initial_transition = data[CONF_INITIAL_TRANSITION]
|
||||
self._interval = data[CONF_INTERVAL]
|
||||
|
|
@ -527,12 +566,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
remove_interval = async_track_time_interval(
|
||||
self.hass, self._async_update_at_interval, self._interval
|
||||
)
|
||||
remove_sleep = async_track_state_change_event(
|
||||
self.hass,
|
||||
self.sleep_mode_switch.entity_id,
|
||||
self._sleep_state_event,
|
||||
)
|
||||
self.remove_listeners.extend([remove_interval, remove_sleep])
|
||||
remove_simple_switches = [
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
switch.entity_id,
|
||||
self._simple_switch_state_event,
|
||||
)
|
||||
for switch in (
|
||||
self.sleep_mode_switch,
|
||||
self.adapt_brightness_switch,
|
||||
self.adapt_color_switch,
|
||||
)
|
||||
]
|
||||
|
||||
self.remove_listeners.extend([remove_interval, *remove_simple_switches])
|
||||
|
||||
if self._lights:
|
||||
self._expand_light_groups()
|
||||
|
|
@ -614,8 +661,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
light: str,
|
||||
transition: Optional[int] = None,
|
||||
adapt_brightness: Optional[bool] = None,
|
||||
adapt_color_temp: Optional[bool] = None,
|
||||
adapt_rgb_color: Optional[bool] = None,
|
||||
adapt_color: Optional[bool] = None,
|
||||
force: bool = False,
|
||||
context: Optional[Context] = None,
|
||||
) -> None:
|
||||
|
|
@ -629,11 +675,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if transition is None:
|
||||
transition = self._transition
|
||||
if adapt_brightness is None:
|
||||
adapt_brightness = self._adapt_brightness
|
||||
if adapt_color_temp is None:
|
||||
adapt_color_temp = self._adapt_color_temp
|
||||
if adapt_rgb_color is None:
|
||||
adapt_rgb_color = self._adapt_rgb_color
|
||||
adapt_brightness = self.adapt_brightness_switch.is_on
|
||||
if adapt_color is None:
|
||||
adapt_color = self.adapt_color_switch.is_on
|
||||
|
||||
if "transition" in features:
|
||||
service_data[ATTR_TRANSITION] = transition
|
||||
|
|
@ -644,7 +688,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
if (
|
||||
"color_temp" in features
|
||||
and adapt_color_temp
|
||||
and adapt_color
|
||||
and not (self._prefer_rgb_color and "color" in features)
|
||||
):
|
||||
attributes = self.hass.states.get(light).attributes
|
||||
|
|
@ -652,7 +696,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
color_temp_mired = self._settings["color_temp_mired"]
|
||||
color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds)
|
||||
service_data[ATTR_COLOR_TEMP] = color_temp_mired
|
||||
elif "color" in features and adapt_rgb_color:
|
||||
elif "color" in features and adapt_color:
|
||||
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
|
||||
|
||||
context = context or self.create_context("adapt_lights")
|
||||
|
|
@ -662,9 +706,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
and not force
|
||||
and await self.turn_on_off_listener.significant_change(
|
||||
light,
|
||||
self._adapt_brightness,
|
||||
self._adapt_color_temp,
|
||||
self._adapt_rgb_color,
|
||||
adapt_brightness,
|
||||
adapt_color,
|
||||
context,
|
||||
)
|
||||
):
|
||||
|
|
@ -732,6 +775,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
and self.turn_on_off_listener.is_manually_controlled(
|
||||
light,
|
||||
force,
|
||||
self.adapt_brightness_switch.is_on,
|
||||
self.adapt_color_switch.is_on,
|
||||
)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
|
|
@ -743,15 +788,22 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
continue
|
||||
await self._adapt_light(light, transition, force=force, context=context)
|
||||
|
||||
async def _sleep_state_event(self, event: Event) -> None:
|
||||
if not match_state_event(event, (STATE_ON, STATE_OFF)):
|
||||
async def _simple_switch_state_event(self, event: Event) -> None:
|
||||
if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
|
||||
return
|
||||
_LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event)
|
||||
self.turn_on_off_listener.reset(*self._lights)
|
||||
which = {
|
||||
self.sleep_mode_switch.entity_id: "sleep_sw",
|
||||
self.adapt_color_switch.entity_id: "color_sw",
|
||||
self.adapt_brightness_switch.entity_id: "brigt_sw",
|
||||
}[event.data[ATTR_ENTITY_ID]]
|
||||
_LOGGER.debug("%s: _simple_switch_state_event, event: '%s'", self._name, event)
|
||||
if which == "sleep_sw":
|
||||
# Reset the manually controlled status when the "sleep mode" changes
|
||||
self.turn_on_off_listener.reset(*self._lights)
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
transition=self._initial_transition,
|
||||
force=True,
|
||||
context=self.create_context("sleep"),
|
||||
context=self.create_context(which),
|
||||
)
|
||||
|
||||
async def _light_event(self, event: Event) -> None:
|
||||
|
|
@ -805,26 +857,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self.turn_on_off_listener.reset(entity_id)
|
||||
|
||||
|
||||
class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity):
|
||||
class SimpleSwitch(SwitchEntity, RestoreEntity):
|
||||
"""Representation of a Adaptive Lighting switch."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry):
|
||||
def __init__(
|
||||
self, which: str, initial_state: bool, hass: HomeAssistant, config_entry
|
||||
):
|
||||
"""Initialize the Adaptive Lighting switch."""
|
||||
self.hass = hass
|
||||
data = validate(config_entry)
|
||||
self._name = data[CONF_NAME]
|
||||
self._icon = ICON
|
||||
self._state = None
|
||||
self._which = which
|
||||
self._unique_id = f"{self._name}_{slugify(self._which)}"
|
||||
self._name = f"Adaptive Lighting {which}: {self._name}"
|
||||
self._initial_state = initial_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return f"Adaptive Lighting Sleep Mode: {self._name}"
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of entity."""
|
||||
return f"{self._name}_sleep_mode"
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
|
|
@ -839,10 +897,15 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity):
|
|||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
last_state = await self.async_get_last_state()
|
||||
if last_state is None or STATE_OFF: # newly added to HA
|
||||
await self.async_turn_off()
|
||||
else:
|
||||
if last_state is None: # newly added to HA
|
||||
if self._initial_state:
|
||||
await self.async_turn_on()
|
||||
else:
|
||||
await self.async_turn_off()
|
||||
elif STATE_ON:
|
||||
await self.async_turn_on()
|
||||
elif STATE_OFF:
|
||||
await self.async_turn_off()
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn on adaptive lighting sleep mode."""
|
||||
|
|
@ -1124,6 +1187,8 @@ class TurnOnOffListener:
|
|||
self,
|
||||
light: str,
|
||||
force: bool,
|
||||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
) -> bool:
|
||||
"""Check if the light has been 'on' and is now manually controlled."""
|
||||
manual_control = self.manual_control.setdefault(light, False)
|
||||
|
|
@ -1137,26 +1202,29 @@ class TurnOnOffListener:
|
|||
and not is_our_context(turn_on_event.context)
|
||||
and not force
|
||||
):
|
||||
# Light was already on and 'light.turn_on' was not called by
|
||||
# the adaptive_lighting integration.
|
||||
manual_control = self.manual_control[light] = True
|
||||
_fire_manual_control_event(self.hass, light, turn_on_event.context)
|
||||
_LOGGER.debug(
|
||||
"'%s' was already on and 'light.turn_on' was not called by the"
|
||||
" adaptive_lighting integration (context.id='%s'), the Adaptive"
|
||||
" Lighting will stop adapting the light until the switch or the"
|
||||
" light turns off and then on again.",
|
||||
light,
|
||||
turn_on_event.context.id,
|
||||
)
|
||||
keys = turn_on_event.data[ATTR_SERVICE_DATA].keys()
|
||||
if (adapt_color and COLOR_ATTRS.intersection(keys)) or (
|
||||
adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys)
|
||||
):
|
||||
# Light was already on and 'light.turn_on' was not called by
|
||||
# the adaptive_lighting integration.
|
||||
manual_control = self.manual_control[light] = True
|
||||
_fire_manual_control_event(self.hass, light, turn_on_event.context)
|
||||
_LOGGER.debug(
|
||||
"'%s' was already on and 'light.turn_on' was not called by the"
|
||||
" adaptive_lighting integration (context.id='%s'), the Adaptive"
|
||||
" Lighting will stop adapting the light until the switch or the"
|
||||
" light turns off and then on again.",
|
||||
light,
|
||||
turn_on_event.context.id,
|
||||
)
|
||||
return manual_control
|
||||
|
||||
async def significant_change(
|
||||
self,
|
||||
light: str,
|
||||
adapt_brightness: bool,
|
||||
adapt_color_temp: bool,
|
||||
adapt_rgb_color: bool,
|
||||
adapt_color: bool,
|
||||
context: Context,
|
||||
) -> bool:
|
||||
"""Has the light made a significant change since last update.
|
||||
|
|
@ -1176,8 +1244,7 @@ class TurnOnOffListener:
|
|||
light=light,
|
||||
new_attributes=new_state.attributes,
|
||||
adapt_brightness=adapt_brightness,
|
||||
adapt_color_temp=adapt_color_temp,
|
||||
adapt_rgb_color=adapt_rgb_color,
|
||||
adapt_color=adapt_color,
|
||||
context=context,
|
||||
)
|
||||
for index, old_state in enumerate(old_states):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue