Enable strict typing for greeneye_monitor (#58571)
* Enable strict typing for greeneye_monitor * Fix pylint
This commit is contained in:
parent
061b1abd1b
commit
687c40a622
4 changed files with 110 additions and 47 deletions
|
@ -52,6 +52,7 @@ homeassistant.components.fritz.*
|
||||||
homeassistant.components.geo_location.*
|
homeassistant.components.geo_location.*
|
||||||
homeassistant.components.gios.*
|
homeassistant.components.gios.*
|
||||||
homeassistant.components.goalzero.*
|
homeassistant.components.goalzero.*
|
||||||
|
homeassistant.components.greeneye_monitor.*
|
||||||
homeassistant.components.group.*
|
homeassistant.components.group.*
|
||||||
homeassistant.components.guardian.*
|
homeassistant.components.guardian.*
|
||||||
homeassistant.components.history.*
|
homeassistant.components.history.*
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
"""Support for monitoring a GreenEye Monitor energy monitor."""
|
"""Support for monitoring a GreenEye Monitor energy monitor."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from greeneye import Monitors
|
from greeneye import Monitors
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -15,8 +18,10 @@ from homeassistant.const import (
|
||||||
TIME_MINUTES,
|
TIME_MINUTES,
|
||||||
TIME_SECONDS,
|
TIME_SECONDS,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -117,7 +122,7 @@ COMPONENT_SCHEMA = vol.Schema(
|
||||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the GreenEye Monitor component."""
|
"""Set up the GreenEye Monitor component."""
|
||||||
monitors = Monitors()
|
monitors = Monitors()
|
||||||
hass.data[DATA_GREENEYE_MONITOR] = monitors
|
hass.data[DATA_GREENEYE_MONITOR] = monitors
|
||||||
|
@ -125,7 +130,7 @@ async def async_setup(hass, config):
|
||||||
server_config = config[DOMAIN]
|
server_config = config[DOMAIN]
|
||||||
server = await monitors.start_server(server_config[CONF_PORT])
|
server = await monitors.start_server(server_config[CONF_PORT])
|
||||||
|
|
||||||
async def close_server(*args):
|
async def close_server(*args: list[Any]) -> None:
|
||||||
"""Close the monitoring server."""
|
"""Close the monitoring server."""
|
||||||
await server.close()
|
await server.close()
|
||||||
|
|
||||||
|
@ -189,7 +194,7 @@ async def async_setup(hass, config):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
async_load_platform(hass, "sensor", DOMAIN, all_sensors, config)
|
async_load_platform(hass, "sensor", DOMAIN, {CONF_SENSORS: all_sensors}, config)
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
"""Support for the sensors in a GreenEye Monitor."""
|
"""Support for the sensors in a GreenEye Monitor."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
|
import greeneye
|
||||||
|
from greeneye import Monitors
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import SensorEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_SENSOR_TYPE,
|
CONF_SENSOR_TYPE,
|
||||||
|
CONF_SENSORS,
|
||||||
CONF_TEMPERATURE_UNIT,
|
CONF_TEMPERATURE_UNIT,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
ELECTRIC_POTENTIAL_VOLT,
|
ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
@ -11,6 +19,9 @@ from homeassistant.const import (
|
||||||
TIME_MINUTES,
|
TIME_MINUTES,
|
||||||
TIME_SECONDS,
|
TIME_SECONDS,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import Config, HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_COUNTED_QUANTITY,
|
CONF_COUNTED_QUANTITY,
|
||||||
|
@ -37,13 +48,15 @@ TEMPERATURE_ICON = "mdi:thermometer"
|
||||||
VOLTAGE_ICON = "mdi:current-ac"
|
VOLTAGE_ICON = "mdi:current-ac"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: Config,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
discovery_info: DiscoveryInfoType,
|
||||||
|
) -> None:
|
||||||
"""Set up a single GEM temperature sensor."""
|
"""Set up a single GEM temperature sensor."""
|
||||||
if not discovery_info:
|
entities: list[GEMSensor] = []
|
||||||
return
|
for sensor in discovery_info[CONF_SENSORS]:
|
||||||
|
|
||||||
entities = []
|
|
||||||
for sensor in discovery_info:
|
|
||||||
sensor_type = sensor[CONF_SENSOR_TYPE]
|
sensor_type = sensor[CONF_SENSOR_TYPE]
|
||||||
if sensor_type == SENSOR_TYPE_CURRENT:
|
if sensor_type == SENSOR_TYPE_CURRENT:
|
||||||
entities.append(
|
entities.append(
|
||||||
|
@ -86,42 +99,53 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class GEMSensor(SensorEntity):
|
T = TypeVar(
|
||||||
|
"T",
|
||||||
|
greeneye.monitor.Channel,
|
||||||
|
greeneye.monitor.Monitor,
|
||||||
|
greeneye.monitor.PulseCounter,
|
||||||
|
greeneye.monitor.TemperatureSensor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GEMSensor(Generic[T], SensorEntity):
|
||||||
"""Base class for GreenEye Monitor sensors."""
|
"""Base class for GreenEye Monitor sensors."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(self, monitor_serial_number, name, sensor_type, number):
|
def __init__(
|
||||||
|
self, monitor_serial_number: int, name: str, sensor_type: str, number: int
|
||||||
|
) -> None:
|
||||||
"""Construct the entity."""
|
"""Construct the entity."""
|
||||||
self._monitor_serial_number = monitor_serial_number
|
self._monitor_serial_number = monitor_serial_number
|
||||||
self._name = name
|
self._name = name
|
||||||
self._sensor = None
|
self._sensor: T | None = None
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._number = number
|
self._number = number
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self) -> str:
|
||||||
"""Return a unique ID for this sensor."""
|
"""Return a unique ID for this sensor."""
|
||||||
return f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}"
|
return f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Return the name of the channel."""
|
"""Return the name of the channel."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Wait for and connect to the sensor."""
|
"""Wait for and connect to the sensor."""
|
||||||
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
||||||
|
|
||||||
if not self._try_connect_to_monitor(monitors):
|
if not self._try_connect_to_monitor(monitors):
|
||||||
monitors.add_listener(self._on_new_monitor)
|
monitors.add_listener(self._on_new_monitor)
|
||||||
|
|
||||||
def _on_new_monitor(self, *args):
|
def _on_new_monitor(self, *args: list[Any]) -> None:
|
||||||
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
||||||
if self._try_connect_to_monitor(monitors):
|
if self._try_connect_to_monitor(monitors):
|
||||||
monitors.remove_listener(self._on_new_monitor)
|
monitors.remove_listener(self._on_new_monitor)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Remove listener from the sensor."""
|
"""Remove listener from the sensor."""
|
||||||
if self._sensor:
|
if self._sensor:
|
||||||
self._sensor.remove_listener(self.async_write_ha_state)
|
self._sensor.remove_listener(self.async_write_ha_state)
|
||||||
|
@ -129,7 +153,7 @@ class GEMSensor(SensorEntity):
|
||||||
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
||||||
monitors.remove_listener(self._on_new_monitor)
|
monitors.remove_listener(self._on_new_monitor)
|
||||||
|
|
||||||
def _try_connect_to_monitor(self, monitors):
|
def _try_connect_to_monitor(self, monitors: Monitors) -> bool:
|
||||||
monitor = monitors.monitors.get(self._monitor_serial_number)
|
monitor = monitors.monitors.get(self._monitor_serial_number)
|
||||||
if not monitor:
|
if not monitor:
|
||||||
return False
|
return False
|
||||||
|
@ -140,34 +164,39 @@ class GEMSensor(SensorEntity):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _get_sensor(self, monitor):
|
def _get_sensor(self, monitor: greeneye.monitor.Monitor) -> T:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class CurrentSensor(GEMSensor):
|
class CurrentSensor(GEMSensor[greeneye.monitor.Channel]):
|
||||||
"""Entity showing power usage on one channel of the monitor."""
|
"""Entity showing power usage on one channel of the monitor."""
|
||||||
|
|
||||||
_attr_icon = CURRENT_SENSOR_ICON
|
_attr_icon = CURRENT_SENSOR_ICON
|
||||||
_attr_native_unit_of_measurement = UNIT_WATTS
|
_attr_native_unit_of_measurement = UNIT_WATTS
|
||||||
|
|
||||||
def __init__(self, monitor_serial_number, number, name, net_metering):
|
def __init__(
|
||||||
|
self, monitor_serial_number: int, number: int, name: str, net_metering: bool
|
||||||
|
) -> None:
|
||||||
"""Construct the entity."""
|
"""Construct the entity."""
|
||||||
super().__init__(monitor_serial_number, name, "current", number)
|
super().__init__(monitor_serial_number, name, "current", number)
|
||||||
self._net_metering = net_metering
|
self._net_metering = net_metering
|
||||||
|
|
||||||
def _get_sensor(self, monitor):
|
def _get_sensor(
|
||||||
|
self, monitor: greeneye.monitor.Monitor
|
||||||
|
) -> greeneye.monitor.Channel:
|
||||||
return monitor.channels[self._number - 1]
|
return monitor.channels[self._number - 1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self) -> float | None:
|
||||||
"""Return the current number of watts being used by the channel."""
|
"""Return the current number of watts being used by the channel."""
|
||||||
if not self._sensor:
|
if not self._sensor:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
assert isinstance(self._sensor.watts, float)
|
||||||
return self._sensor.watts
|
return self._sensor.watts
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return total wattseconds in the state dictionary."""
|
"""Return total wattseconds in the state dictionary."""
|
||||||
if not self._sensor:
|
if not self._sensor:
|
||||||
return None
|
return None
|
||||||
|
@ -180,43 +209,47 @@ class CurrentSensor(GEMSensor):
|
||||||
return {DATA_WATT_SECONDS: watt_seconds}
|
return {DATA_WATT_SECONDS: watt_seconds}
|
||||||
|
|
||||||
|
|
||||||
class PulseCounter(GEMSensor):
|
class PulseCounter(GEMSensor[greeneye.monitor.PulseCounter]):
|
||||||
"""Entity showing rate of change in one pulse counter of the monitor."""
|
"""Entity showing rate of change in one pulse counter of the monitor."""
|
||||||
|
|
||||||
_attr_icon = COUNTER_ICON
|
_attr_icon = COUNTER_ICON
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
monitor_serial_number,
|
monitor_serial_number: int,
|
||||||
number,
|
number: int,
|
||||||
name,
|
name: str,
|
||||||
counted_quantity,
|
counted_quantity: str,
|
||||||
time_unit,
|
time_unit: str,
|
||||||
counted_quantity_per_pulse,
|
counted_quantity_per_pulse: float,
|
||||||
):
|
) -> None:
|
||||||
"""Construct the entity."""
|
"""Construct the entity."""
|
||||||
super().__init__(monitor_serial_number, name, "pulse", number)
|
super().__init__(monitor_serial_number, name, "pulse", number)
|
||||||
self._counted_quantity = counted_quantity
|
self._counted_quantity = counted_quantity
|
||||||
self._counted_quantity_per_pulse = counted_quantity_per_pulse
|
self._counted_quantity_per_pulse = counted_quantity_per_pulse
|
||||||
self._time_unit = time_unit
|
self._time_unit = time_unit
|
||||||
|
|
||||||
def _get_sensor(self, monitor):
|
def _get_sensor(
|
||||||
|
self, monitor: greeneye.monitor.Monitor
|
||||||
|
) -> greeneye.monitor.PulseCounter:
|
||||||
return monitor.pulse_counters[self._number - 1]
|
return monitor.pulse_counters[self._number - 1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self) -> float | None:
|
||||||
"""Return the current rate of change for the given pulse counter."""
|
"""Return the current rate of change for the given pulse counter."""
|
||||||
if not self._sensor or self._sensor.pulses_per_second is None:
|
if not self._sensor or self._sensor.pulses_per_second is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return (
|
result = (
|
||||||
self._sensor.pulses_per_second
|
self._sensor.pulses_per_second
|
||||||
* self._counted_quantity_per_pulse
|
* self._counted_quantity_per_pulse
|
||||||
* self._seconds_per_time_unit
|
* self._seconds_per_time_unit
|
||||||
)
|
)
|
||||||
|
assert isinstance(result, float)
|
||||||
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _seconds_per_time_unit(self):
|
def _seconds_per_time_unit(self) -> int:
|
||||||
"""Return the number of seconds in the given display time unit."""
|
"""Return the number of seconds in the given display time unit."""
|
||||||
if self._time_unit == TIME_SECONDS:
|
if self._time_unit == TIME_SECONDS:
|
||||||
return 1
|
return 1
|
||||||
|
@ -225,13 +258,18 @@ class PulseCounter(GEMSensor):
|
||||||
if self._time_unit == TIME_HOURS:
|
if self._time_unit == TIME_HOURS:
|
||||||
return 3600
|
return 3600
|
||||||
|
|
||||||
|
# Config schema should have ensured it is one of the above values
|
||||||
|
raise Exception(
|
||||||
|
f"Invalid value for time unit: {self._time_unit}. Expected one of {TIME_SECONDS}, {TIME_MINUTES}, or {TIME_HOURS}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self):
|
def native_unit_of_measurement(self) -> str:
|
||||||
"""Return the unit of measurement for this pulse counter."""
|
"""Return the unit of measurement for this pulse counter."""
|
||||||
return f"{self._counted_quantity}/{self._time_unit}"
|
return f"{self._counted_quantity}/{self._time_unit}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return total pulses in the data dictionary."""
|
"""Return total pulses in the data dictionary."""
|
||||||
if not self._sensor:
|
if not self._sensor:
|
||||||
return None
|
return None
|
||||||
|
@ -239,52 +277,60 @@ class PulseCounter(GEMSensor):
|
||||||
return {DATA_PULSES: self._sensor.pulses}
|
return {DATA_PULSES: self._sensor.pulses}
|
||||||
|
|
||||||
|
|
||||||
class TemperatureSensor(GEMSensor):
|
class TemperatureSensor(GEMSensor[greeneye.monitor.TemperatureSensor]):
|
||||||
"""Entity showing temperature from one temperature sensor."""
|
"""Entity showing temperature from one temperature sensor."""
|
||||||
|
|
||||||
_attr_device_class = DEVICE_CLASS_TEMPERATURE
|
_attr_device_class = DEVICE_CLASS_TEMPERATURE
|
||||||
_attr_icon = TEMPERATURE_ICON
|
_attr_icon = TEMPERATURE_ICON
|
||||||
|
|
||||||
def __init__(self, monitor_serial_number, number, name, unit):
|
def __init__(
|
||||||
|
self, monitor_serial_number: int, number: int, name: str, unit: str
|
||||||
|
) -> None:
|
||||||
"""Construct the entity."""
|
"""Construct the entity."""
|
||||||
super().__init__(monitor_serial_number, name, "temp", number)
|
super().__init__(monitor_serial_number, name, "temp", number)
|
||||||
self._unit = unit
|
self._unit = unit
|
||||||
|
|
||||||
def _get_sensor(self, monitor):
|
def _get_sensor(
|
||||||
|
self, monitor: greeneye.monitor.Monitor
|
||||||
|
) -> greeneye.monitor.TemperatureSensor:
|
||||||
return monitor.temperature_sensors[self._number - 1]
|
return monitor.temperature_sensors[self._number - 1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self) -> float | None:
|
||||||
"""Return the current temperature being reported by this sensor."""
|
"""Return the current temperature being reported by this sensor."""
|
||||||
if not self._sensor:
|
if not self._sensor:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
assert isinstance(self._sensor.temperature, float)
|
||||||
return self._sensor.temperature
|
return self._sensor.temperature
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self):
|
def native_unit_of_measurement(self) -> str:
|
||||||
"""Return the unit of measurement for this sensor (user specified)."""
|
"""Return the unit of measurement for this sensor (user specified)."""
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
||||||
|
|
||||||
class VoltageSensor(GEMSensor):
|
class VoltageSensor(GEMSensor[greeneye.monitor.Monitor]):
|
||||||
"""Entity showing voltage."""
|
"""Entity showing voltage."""
|
||||||
|
|
||||||
_attr_icon = VOLTAGE_ICON
|
_attr_icon = VOLTAGE_ICON
|
||||||
_attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT
|
_attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT
|
||||||
|
|
||||||
def __init__(self, monitor_serial_number, number, name):
|
def __init__(self, monitor_serial_number: int, number: int, name: str) -> None:
|
||||||
"""Construct the entity."""
|
"""Construct the entity."""
|
||||||
super().__init__(monitor_serial_number, name, "volts", number)
|
super().__init__(monitor_serial_number, name, "volts", number)
|
||||||
|
|
||||||
def _get_sensor(self, monitor):
|
def _get_sensor(
|
||||||
|
self, monitor: greeneye.monitor.Monitor
|
||||||
|
) -> greeneye.monitor.Monitor:
|
||||||
"""Wire the updates to the monitor itself, since there is no voltage element in the API."""
|
"""Wire the updates to the monitor itself, since there is no voltage element in the API."""
|
||||||
return monitor
|
return monitor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self) -> float | None:
|
||||||
"""Return the current voltage being reported by this sensor."""
|
"""Return the current voltage being reported by this sensor."""
|
||||||
if not self._sensor:
|
if not self._sensor:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
assert isinstance(self._sensor.voltage, float)
|
||||||
return self._sensor.voltage
|
return self._sensor.voltage
|
||||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -583,6 +583,17 @@ no_implicit_optional = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.greeneye_monitor.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.group.*]
|
[mypy-homeassistant.components.group.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
Loading…
Add table
Reference in a new issue