Add target temperature range to homekit_controller (#42817)
* Add support for temperature range in thermostat. * Add tests for setting temperature range. * Update Lennox E30/Ecobee 3 tests to reflect new supported feature * Add support for thermostate mode specific min/max temp values.
This commit is contained in:
parent
23f8ae8fec
commit
8b01f681ab
4 changed files with 213 additions and 13 deletions
|
@ -19,6 +19,8 @@ from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
|
ATTR_TARGET_TEMP_LOW,
|
||||||
CURRENT_HVAC_COOL,
|
CURRENT_HVAC_COOL,
|
||||||
CURRENT_HVAC_HEAT,
|
CURRENT_HVAC_HEAT,
|
||||||
CURRENT_HVAC_IDLE,
|
CURRENT_HVAC_IDLE,
|
||||||
|
@ -30,6 +32,7 @@ from homeassistant.components.climate.const import (
|
||||||
SUPPORT_SWING_MODE,
|
SUPPORT_SWING_MODE,
|
||||||
SUPPORT_TARGET_HUMIDITY,
|
SUPPORT_TARGET_HUMIDITY,
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
SWING_OFF,
|
SWING_OFF,
|
||||||
SWING_VERTICAL,
|
SWING_VERTICAL,
|
||||||
)
|
)
|
||||||
|
@ -329,7 +332,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
return [
|
return [
|
||||||
CharacteristicsTypes.HEATING_COOLING_CURRENT,
|
CharacteristicsTypes.HEATING_COOLING_CURRENT,
|
||||||
CharacteristicsTypes.HEATING_COOLING_TARGET,
|
CharacteristicsTypes.HEATING_COOLING_TARGET,
|
||||||
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
||||||
CharacteristicsTypes.TEMPERATURE_CURRENT,
|
CharacteristicsTypes.TEMPERATURE_CURRENT,
|
||||||
|
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
||||||
CharacteristicsTypes.TEMPERATURE_TARGET,
|
CharacteristicsTypes.TEMPERATURE_TARGET,
|
||||||
CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
|
CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
|
||||||
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET,
|
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET,
|
||||||
|
@ -338,10 +343,23 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
async def async_set_temperature(self, **kwargs):
|
async def async_set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||||
await self.async_put_characteristics(
|
cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
{CharacteristicsTypes.TEMPERATURE_TARGET: temp}
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
)
|
if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}:
|
||||||
|
if temp is None:
|
||||||
|
temp = (cool_temp + heat_temp) / 2
|
||||||
|
await self.async_put_characteristics(
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: heat_temp,
|
||||||
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: cool_temp,
|
||||||
|
CharacteristicsTypes.TEMPERATURE_TARGET: temp,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.async_put_characteristics(
|
||||||
|
{CharacteristicsTypes.TEMPERATURE_TARGET: temp}
|
||||||
|
)
|
||||||
|
|
||||||
async def async_set_humidity(self, humidity):
|
async def async_set_humidity(self, humidity):
|
||||||
"""Set new target humidity."""
|
"""Set new target humidity."""
|
||||||
|
@ -367,22 +385,57 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
|
if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}:
|
||||||
|
return None
|
||||||
return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET)
|
return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_high(self):
|
||||||
|
"""Return the highbound target temperature we try to reach."""
|
||||||
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
|
if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}:
|
||||||
|
return None
|
||||||
|
return self.service.value(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_low(self):
|
||||||
|
"""Return the lowbound target temperature we try to reach."""
|
||||||
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
|
if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}:
|
||||||
|
return None
|
||||||
|
return self.service.value(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_temp(self):
|
def min_temp(self):
|
||||||
"""Return the minimum target temp."""
|
"""Return the minimum target temp."""
|
||||||
if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET):
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET]
|
if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}:
|
||||||
return char.minValue
|
min_temp = self.service[
|
||||||
|
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
|
||||||
|
].minValue
|
||||||
|
if min_temp is not None:
|
||||||
|
return min_temp
|
||||||
|
if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}:
|
||||||
|
min_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].minValue
|
||||||
|
if min_temp is not None:
|
||||||
|
return min_temp
|
||||||
return super().min_temp
|
return super().min_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_temp(self):
|
def max_temp(self):
|
||||||
"""Return the maximum target temp."""
|
"""Return the maximum target temp."""
|
||||||
if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET):
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET]
|
if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}:
|
||||||
return char.maxValue
|
max_temp = self.service[
|
||||||
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
|
||||||
|
].maxValue
|
||||||
|
if max_temp is not None:
|
||||||
|
return max_temp
|
||||||
|
if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}:
|
||||||
|
max_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].maxValue
|
||||||
|
if max_temp is not None:
|
||||||
|
return max_temp
|
||||||
return super().max_temp
|
return super().max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -443,6 +496,11 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET):
|
if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET):
|
||||||
features |= SUPPORT_TARGET_TEMPERATURE
|
features |= SUPPORT_TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
if self.service.has(
|
||||||
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
|
||||||
|
) and self.service.has(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD):
|
||||||
|
features |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
|
|
||||||
if self.service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET):
|
if self.service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET):
|
||||||
features |= SUPPORT_TARGET_HUMIDITY
|
features |= SUPPORT_TARGET_HUMIDITY
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ from aiohomekit.testing import FakePairing
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
SUPPORT_TARGET_HUMIDITY,
|
SUPPORT_TARGET_HUMIDITY,
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
|
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
|
||||||
|
|
||||||
|
@ -40,7 +41,9 @@ async def test_ecobee3_setup(hass):
|
||||||
climate_state = await climate_helper.poll_and_get_state()
|
climate_state = await climate_helper.poll_and_get_state()
|
||||||
assert climate_state.attributes["friendly_name"] == "HomeW"
|
assert climate_state.attributes["friendly_name"] == "HomeW"
|
||||||
assert climate_state.attributes["supported_features"] == (
|
assert climate_state.attributes["supported_features"] == (
|
||||||
SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY
|
SUPPORT_TARGET_TEMPERATURE
|
||||||
|
| SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
|
| SUPPORT_TARGET_HUMIDITY
|
||||||
)
|
)
|
||||||
|
|
||||||
assert climate_state.attributes["hvac_modes"] == [
|
assert climate_state.attributes["hvac_modes"] == [
|
||||||
|
|
|
@ -4,7 +4,10 @@ Regression tests for Aqara Gateway V3.
|
||||||
https://github.com/home-assistant/core/issues/20885
|
https://github.com/home-assistant/core/issues/20885
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE
|
from homeassistant.components.climate.const import (
|
||||||
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.components.homekit_controller.common import (
|
from tests.components.homekit_controller.common import (
|
||||||
Helper,
|
Helper,
|
||||||
|
@ -29,7 +32,7 @@ async def test_lennox_e30_setup(hass):
|
||||||
climate_state = await climate_helper.poll_and_get_state()
|
climate_state = await climate_helper.poll_and_get_state()
|
||||||
assert climate_state.attributes["friendly_name"] == "Lennox"
|
assert climate_state.attributes["friendly_name"] == "Lennox"
|
||||||
assert climate_state.attributes["supported_features"] == (
|
assert climate_state.attributes["supported_features"] == (
|
||||||
SUPPORT_TARGET_TEMPERATURE
|
SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
)
|
)
|
||||||
|
|
||||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
|
|
@ -24,6 +24,14 @@ from tests.components.homekit_controller.common import setup_test_component
|
||||||
|
|
||||||
HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target")
|
HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target")
|
||||||
HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current")
|
HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current")
|
||||||
|
THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD = (
|
||||||
|
"thermostat",
|
||||||
|
"temperature.cooling-threshold",
|
||||||
|
)
|
||||||
|
THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD = (
|
||||||
|
"thermostat",
|
||||||
|
"temperature.heating-threshold",
|
||||||
|
)
|
||||||
TEMPERATURE_TARGET = ("thermostat", "temperature.target")
|
TEMPERATURE_TARGET = ("thermostat", "temperature.target")
|
||||||
TEMPERATURE_CURRENT = ("thermostat", "temperature.current")
|
TEMPERATURE_CURRENT = ("thermostat", "temperature.current")
|
||||||
HUMIDITY_TARGET = ("thermostat", "relative-humidity.target")
|
HUMIDITY_TARGET = ("thermostat", "relative-humidity.target")
|
||||||
|
@ -42,6 +50,16 @@ def create_thermostat_service(accessory):
|
||||||
char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
||||||
char.value = 0
|
char.value = 0
|
||||||
|
|
||||||
|
char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD)
|
||||||
|
char.minValue = 15
|
||||||
|
char.maxValue = 40
|
||||||
|
char.value = 0
|
||||||
|
|
||||||
|
char = service.add_char(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD)
|
||||||
|
char.minValue = 4
|
||||||
|
char.maxValue = 30
|
||||||
|
char.value = 0
|
||||||
|
|
||||||
char = service.add_char(CharacteristicsTypes.TEMPERATURE_TARGET)
|
char = service.add_char(CharacteristicsTypes.TEMPERATURE_TARGET)
|
||||||
char.minValue = 7
|
char.minValue = 7
|
||||||
char.maxValue = 35
|
char.maxValue = 35
|
||||||
|
@ -126,6 +144,41 @@ async def test_climate_change_thermostat_state(hass, utcnow):
|
||||||
assert helper.characteristics[HEATING_COOLING_TARGET].value == 0
|
assert helper.characteristics[HEATING_COOLING_TARGET].value == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_check_min_max_values_per_mode(hass, utcnow):
|
||||||
|
"""Test that we we get the appropriate min/max values for each mode."""
|
||||||
|
helper = await setup_test_component(hass, create_thermostat_service)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
climate_state = await helper.poll_and_get_state()
|
||||||
|
assert climate_state.attributes["min_temp"] == 7
|
||||||
|
assert climate_state.attributes["max_temp"] == 35
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
climate_state = await helper.poll_and_get_state()
|
||||||
|
assert climate_state.attributes["min_temp"] == 7
|
||||||
|
assert climate_state.attributes["max_temp"] == 35
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
climate_state = await helper.poll_and_get_state()
|
||||||
|
assert climate_state.attributes["min_temp"] == 4
|
||||||
|
assert climate_state.attributes["max_temp"] == 40
|
||||||
|
|
||||||
|
|
||||||
async def test_climate_change_thermostat_temperature(hass, utcnow):
|
async def test_climate_change_thermostat_temperature(hass, utcnow):
|
||||||
"""Test that we can turn a HomeKit thermostat on and off again."""
|
"""Test that we can turn a HomeKit thermostat on and off again."""
|
||||||
helper = await setup_test_component(hass, create_thermostat_service)
|
helper = await setup_test_component(hass, create_thermostat_service)
|
||||||
|
@ -147,6 +200,89 @@ async def test_climate_change_thermostat_temperature(hass, utcnow):
|
||||||
assert helper.characteristics[TEMPERATURE_TARGET].value == 25
|
assert helper.characteristics[TEMPERATURE_TARGET].value == 25
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_change_thermostat_temperature_range(hass, utcnow):
|
||||||
|
"""Test that we can set separate heat and cool setpoints in heat_cool mode."""
|
||||||
|
helper = await setup_test_component(hass, create_thermostat_service)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.testdevice",
|
||||||
|
"hvac_mode": HVAC_MODE_HEAT_COOL,
|
||||||
|
"target_temp_high": 25,
|
||||||
|
"target_temp_low": 20,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert helper.characteristics[TEMPERATURE_TARGET].value == 22.5
|
||||||
|
assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20
|
||||||
|
assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 25
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_change_thermostat_temperature_range_iphone(hass, utcnow):
|
||||||
|
"""Test that we can set all three set points at once (iPhone heat_cool mode support)."""
|
||||||
|
helper = await setup_test_component(hass, create_thermostat_service)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.testdevice",
|
||||||
|
"hvac_mode": HVAC_MODE_HEAT_COOL,
|
||||||
|
"temperature": 22,
|
||||||
|
"target_temp_low": 20,
|
||||||
|
"target_temp_high": 24,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert helper.characteristics[TEMPERATURE_TARGET].value == 22
|
||||||
|
assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20
|
||||||
|
assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 24
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcnow):
|
||||||
|
"""Test that we cannot set range values when not in heat_cool mode."""
|
||||||
|
helper = await setup_test_component(hass, create_thermostat_service)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.testdevice",
|
||||||
|
"hvac_mode": HVAC_MODE_HEAT_COOL,
|
||||||
|
"temperature": 22,
|
||||||
|
"target_temp_low": 20,
|
||||||
|
"target_temp_high": 24,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert helper.characteristics[TEMPERATURE_TARGET].value == 22
|
||||||
|
assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 0
|
||||||
|
assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_climate_change_thermostat_humidity(hass, utcnow):
|
async def test_climate_change_thermostat_humidity(hass, utcnow):
|
||||||
"""Test that we can turn a HomeKit thermostat on and off again."""
|
"""Test that we can turn a HomeKit thermostat on and off again."""
|
||||||
helper = await setup_test_component(hass, create_thermostat_service)
|
helper = await setup_test_component(hass, create_thermostat_service)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue