mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 17:49:37 +01:00
Add ptdevices Integration (#156307)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -442,6 +442,7 @@ homeassistant.components.private_ble_device.*
|
||||
homeassistant.components.prometheus.*
|
||||
homeassistant.components.proximity.*
|
||||
homeassistant.components.prusalink.*
|
||||
homeassistant.components.ptdevices.*
|
||||
homeassistant.components.pure_energie.*
|
||||
homeassistant.components.purpleair.*
|
||||
homeassistant.components.pushbullet.*
|
||||
|
||||
Generated
+2
@@ -1378,6 +1378,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||
/homeassistant/components/ps4/ @ktnrg45
|
||||
/tests/components/ps4/ @ktnrg45
|
||||
/homeassistant/components/ptdevices/ @ParemTech-Inc @frogman85978
|
||||
/tests/components/ptdevices/ @ParemTech-Inc @frogman85978
|
||||
/homeassistant/components/pterodactyl/ @elmurato
|
||||
/tests/components/pterodactyl/ @elmurato
|
||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""The PTDevices integration."""
|
||||
|
||||
from aioptdevices.configuration import Configuration
|
||||
from aioptdevices.interface import Interface
|
||||
|
||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DEFAULT_URL
|
||||
from .coordinator import PTDevicesConfigEntry, PTDevicesCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: PTDevicesConfigEntry
|
||||
) -> bool:
|
||||
"""Set up PTDevices from a config entry."""
|
||||
auth_token: str = config_entry.data[CONF_API_TOKEN]
|
||||
session = async_get_clientsession(hass)
|
||||
ptdevices_interface = Interface(
|
||||
Configuration(
|
||||
auth_token=auth_token,
|
||||
device_id="*", # Retrieve data for all devices in account
|
||||
url=DEFAULT_URL,
|
||||
session=session,
|
||||
)
|
||||
)
|
||||
|
||||
config_entry.runtime_data = coordinator = PTDevicesCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
ptdevices_interface,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, _PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PTDevicesConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Config flow for PTDevices integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aioptdevices
|
||||
from aioptdevices.configuration import Configuration
|
||||
from aioptdevices.interface import Interface
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DEFAULT_URL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_CONF_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_TOKEN): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
ptdevices_interface = Interface(
|
||||
Configuration(
|
||||
auth_token=data[CONF_API_TOKEN],
|
||||
device_id="*", # Retrieve data for all devices in account
|
||||
url=DEFAULT_URL,
|
||||
session=session,
|
||||
)
|
||||
)
|
||||
|
||||
# Test Connection
|
||||
try:
|
||||
response = await ptdevices_interface.get_data()
|
||||
except aioptdevices.PTDevicesRequestError as err:
|
||||
raise CannotConnect from err
|
||||
|
||||
except aioptdevices.PTDevicesUnauthorizedError as err:
|
||||
raise InvalidAuth from err
|
||||
|
||||
body = response["body"]
|
||||
|
||||
# Ensure the first device exists
|
||||
first_device = next(iter(body.values()), None)
|
||||
if first_device is None:
|
||||
raise NoDevicesFound
|
||||
|
||||
user_name = first_device.get("user_name")
|
||||
user_id = first_device.get("user_id")
|
||||
|
||||
title: str = str(user_name)
|
||||
unique_id: str = str(user_id)
|
||||
|
||||
# Return title to be used for hub name
|
||||
return (title, unique_id)
|
||||
|
||||
|
||||
class PTDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for PTDevices."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
# Test connection when user data is available
|
||||
if user_input is not None:
|
||||
# Test connection
|
||||
try:
|
||||
title, unique_id = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_access_token"
|
||||
except NoDevicesFound:
|
||||
errors["base"] = "no_devices_found"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
# Connection Successful
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
# Show setup form
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=_CONF_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class NoDevicesFound(HomeAssistantError):
|
||||
"""No devices were found in the account."""
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Constants for the PTDevices integration."""
|
||||
|
||||
DOMAIN = "ptdevices"
|
||||
DEFAULT_URL = "https://api.ptdevices.com/token/v1"
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Coordinator for PTDevices integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
import aioptdevices
|
||||
from aioptdevices.interface import Interface, PTDevicesResponseData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
REQUEST_REFRESH_DEFAULT_IMMEDIATE,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REFRESH_COOLDOWN: Final = 30
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
type PTDevicesConfigEntry = ConfigEntry[PTDevicesCoordinator]
|
||||
|
||||
|
||||
class PTDevicesCoordinator(DataUpdateCoordinator[PTDevicesResponseData]):
|
||||
"""Class for interacting with PTDevices get_data."""
|
||||
|
||||
config_entry: PTDevicesConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: PTDevicesConfigEntry,
|
||||
ptdevices_interface: Interface,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass,
|
||||
_LOGGER,
|
||||
immediate=REQUEST_REFRESH_DEFAULT_IMMEDIATE,
|
||||
cooldown=REFRESH_COOLDOWN,
|
||||
),
|
||||
)
|
||||
|
||||
self.interface = ptdevices_interface
|
||||
|
||||
async def _async_update_data(self) -> PTDevicesResponseData:
|
||||
try:
|
||||
data = await self.interface.get_data()
|
||||
except aioptdevices.PTDevicesRequestError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aioptdevices.PTDevicesUnauthorizedError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_access_token",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
# Purge stale devices
|
||||
device_reg = dr.async_get(self.hass)
|
||||
identifiers = {
|
||||
(DOMAIN, f"{device_data['user_id']}_{device_id}")
|
||||
for device_id, device_data in data["body"].items()
|
||||
}
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_reg, self.config_entry.entry_id
|
||||
):
|
||||
if not set(device.identifiers) & identifiers:
|
||||
_LOGGER.debug("Removing stale device entry %s", device.name)
|
||||
device_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=self.config_entry.entry_id
|
||||
)
|
||||
|
||||
return data["body"]
|
||||
@@ -0,0 +1,49 @@
|
||||
"""PTDevices integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PTDevicesCoordinator
|
||||
|
||||
|
||||
class PTDevicesEntity(CoordinatorEntity[PTDevicesCoordinator]):
|
||||
"""Defines a base PTDevices entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PTDevicesCoordinator,
|
||||
sensor_key: str,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self._sensor_key = sensor_key
|
||||
self._device_id = device_id
|
||||
self._user_id = coordinator.data[self._device_id]["user_id"]
|
||||
|
||||
self._attr_unique_id = f"{self._user_id}_{device_id}_{sensor_key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{self._user_id}_{self._device_id}")},
|
||||
connections={(CONNECTION_NETWORK_MAC, self._device_id)},
|
||||
configuration_url=f"https://www.ptdevices.com/device/level/{self.device['id']}",
|
||||
manufacturer="ParemTech Inc.",
|
||||
model=self.device["device_type"],
|
||||
sw_version=str(self.device["version"]),
|
||||
name=self.device["title"],
|
||||
)
|
||||
|
||||
@property
|
||||
def device(self) -> dict[str, Any]:
|
||||
"""Return the device data."""
|
||||
return self.coordinator.data[self._device_id]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return super().available and self._device_id in self.coordinator.data
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"battery_voltage": {
|
||||
"default": "mdi:battery"
|
||||
},
|
||||
"depth_level": {
|
||||
"default": "mdi:water"
|
||||
},
|
||||
"percent_level": {
|
||||
"default": "mdi:water-percent"
|
||||
},
|
||||
"probe_temperature": {
|
||||
"default": "mdi:thermometer"
|
||||
},
|
||||
"status": {
|
||||
"default": "mdi:information-outline"
|
||||
},
|
||||
"tx_signal": {
|
||||
"default": "mdi:wifi"
|
||||
},
|
||||
"volume_level": {
|
||||
"default": "mdi:water"
|
||||
},
|
||||
"wifi_signal": {
|
||||
"default": "mdi:wifi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "ptdevices",
|
||||
"name": "PTDevices",
|
||||
"codeowners": ["@ParemTech-Inc", "@frogman85978"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ptdevices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioptdevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioptdevices==2026.03.2"]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide any actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration do not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide any actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide any additional options.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -0,0 +1,203 @@
|
||||
"""Sensors for PTDevices device."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import cast
|
||||
|
||||
from aioptdevices.interface import PTDevicesStatusStates
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfLength,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import PTDevicesConfigEntry, PTDevicesCoordinator
|
||||
from .entity import PTDevicesEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class PTDevicesSensors(StrEnum):
|
||||
"""Store keys for PTDevices sensors."""
|
||||
|
||||
LEVEL_PERCENT = "percent_level"
|
||||
LEVEL_VOLUME = "volume_level"
|
||||
LEVEL_DEPTH = "depth_level"
|
||||
PROBE_TEMPERATURE = "probe_temperature"
|
||||
DEVICE_STATUS = "status"
|
||||
DEVICE_WIFI_STRENGTH = "wifi_signal"
|
||||
DEVICE_BATTERY_VOLTAGE = "battery_voltage"
|
||||
TX_SIGNAL_STRENGTH = "tx_signal"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class PTDevicesSensorEntityDescription(SensorEntityDescription):
|
||||
"""Description for PTDevices sensor entities."""
|
||||
|
||||
value_fn: Callable[[dict[str, str | int | float | None]], str | int | float | None]
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[PTDevicesSensorEntityDescription, ...] = (
|
||||
# Percent of water in the tank
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.LEVEL_PERCENT,
|
||||
translation_key=PTDevicesSensors.LEVEL_PERCENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: cast(float, data.get(PTDevicesSensors.LEVEL_PERCENT)),
|
||||
),
|
||||
# Volume of water in the tank (Liters)
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.LEVEL_VOLUME,
|
||||
translation_key=PTDevicesSensors.LEVEL_VOLUME,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
device_class=SensorDeviceClass.VOLUME_STORAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: cast(float, data.get(PTDevicesSensors.LEVEL_VOLUME)),
|
||||
),
|
||||
# Depth of water in the tank (Meters)
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.LEVEL_DEPTH,
|
||||
translation_key=PTDevicesSensors.LEVEL_DEPTH,
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: cast(float, data.get(PTDevicesSensors.LEVEL_DEPTH)),
|
||||
suggested_display_precision=3,
|
||||
),
|
||||
# Temperature measured by external temperature probe (Celsius)
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.PROBE_TEMPERATURE,
|
||||
translation_key=PTDevicesSensors.PROBE_TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: cast(float, data.get(PTDevicesSensors.PROBE_TEMPERATURE)),
|
||||
),
|
||||
# Status of the device
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.DEVICE_STATUS,
|
||||
translation_key=PTDevicesSensors.DEVICE_STATUS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
member.value
|
||||
for member in PTDevicesStatusStates
|
||||
if member.value != "unknown"
|
||||
],
|
||||
value_fn=lambda data: (
|
||||
cast(str, data.get(PTDevicesSensors.DEVICE_STATUS))
|
||||
if cast(str, data.get(PTDevicesSensors.DEVICE_STATUS)) != "unknown"
|
||||
else None
|
||||
),
|
||||
),
|
||||
# Wifi signal strength (%)
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.DEVICE_WIFI_STRENGTH,
|
||||
translation_key=PTDevicesSensors.DEVICE_WIFI_STRENGTH,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: cast(
|
||||
int, data.get(PTDevicesSensors.DEVICE_WIFI_STRENGTH)
|
||||
),
|
||||
),
|
||||
# LoRa signal strength (dBm)
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.TX_SIGNAL_STRENGTH,
|
||||
translation_key=PTDevicesSensors.TX_SIGNAL_STRENGTH,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: cast(
|
||||
float, data.get(PTDevicesSensors.TX_SIGNAL_STRENGTH)
|
||||
),
|
||||
),
|
||||
# Battery voltage (Volts)
|
||||
PTDevicesSensorEntityDescription(
|
||||
key=PTDevicesSensors.DEVICE_BATTERY_VOLTAGE,
|
||||
translation_key=PTDevicesSensors.DEVICE_BATTERY_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: cast(
|
||||
float, data.get(PTDevicesSensors.DEVICE_BATTERY_VOLTAGE)
|
||||
),
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: PTDevicesConfigEntry,
|
||||
async_add_entity: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up PTDevices sensors from config entries."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
known_sensors: set[tuple[str, str]] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
for device_id in sorted(coordinator.data):
|
||||
device = coordinator.data[device_id]
|
||||
new_sensors = [
|
||||
sensor
|
||||
for sensor in SENSOR_DESCRIPTIONS
|
||||
if sensor.key in device and (device_id, sensor.key) not in known_sensors
|
||||
]
|
||||
if not new_sensors:
|
||||
continue
|
||||
known_sensors.update((device_id, sensor.key) for sensor in new_sensors)
|
||||
async_add_entity(
|
||||
PTDevicesSensorEntity(config_entry.runtime_data, sensor, device_id)
|
||||
for sensor in new_sensors
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class PTDevicesSensorEntity(PTDevicesEntity, SensorEntity):
|
||||
"""Sensor entity for PTDevices Integration."""
|
||||
|
||||
entity_description: PTDevicesSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PTDevicesCoordinator,
|
||||
description: PTDevicesSensorEntityDescription,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize sensor."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
description.key,
|
||||
device_id,
|
||||
)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.device)
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
||||
"no_devices_found": "No devices are registered to your PTDevices account.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The API token for your PTDevices account."
|
||||
},
|
||||
"description": "Enter the API token for your PTDevices account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"battery_voltage": {
|
||||
"name": "Battery voltage"
|
||||
},
|
||||
"depth_level": {
|
||||
"name": "Level depth"
|
||||
},
|
||||
"percent_level": {
|
||||
"name": "Level percent"
|
||||
},
|
||||
"probe_temperature": {
|
||||
"name": "Probe temperature"
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
"not_connected": "Not connected",
|
||||
"not_connected_yet": "Not connected yet",
|
||||
"power_internet_out_or_receiver_not_working": "Power or internet out or receiver not working",
|
||||
"press_transmitter_connect_button": "Press transmitter connect button",
|
||||
"transmitter_not_reporting": "Transmitter not reporting",
|
||||
"working": "Working"
|
||||
}
|
||||
},
|
||||
"tx_signal": {
|
||||
"name": "LoRa signal strength"
|
||||
},
|
||||
"volume_level": {
|
||||
"name": "Level volume"
|
||||
},
|
||||
"wifi_signal": {
|
||||
"name": "Wi-Fi signal strength"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"invalid_access_token": {
|
||||
"message": "[%key:common::config_flow::error::invalid_access_token%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -579,6 +579,7 @@ FLOWS = {
|
||||
"proxmoxve",
|
||||
"prusalink",
|
||||
"ps4",
|
||||
"ptdevices",
|
||||
"pterodactyl",
|
||||
"pure_energie",
|
||||
"purpleair",
|
||||
|
||||
@@ -5501,6 +5501,12 @@
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
},
|
||||
"ptdevices": {
|
||||
"name": "PTDevices",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"pterodactyl": {
|
||||
"name": "Pterodactyl",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -4175,6 +4175,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.ptdevices.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.pure_energie.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
Generated
+3
@@ -359,6 +359,9 @@ aiopegelonline==0.1.1
|
||||
# homeassistant.components.opnsense
|
||||
aiopnsense==1.0.8
|
||||
|
||||
# homeassistant.components.ptdevices
|
||||
aioptdevices==2026.03.2
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.6
|
||||
|
||||
|
||||
Generated
+3
@@ -344,6 +344,9 @@ aiopegelonline==0.1.1
|
||||
# homeassistant.components.opnsense
|
||||
aiopnsense==1.0.8
|
||||
|
||||
# homeassistant.components.ptdevices
|
||||
aioptdevices==2026.03.2
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.6
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Tests for the PTDevices component."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Method for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Common fixtures for the PTDevices tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aioptdevices.interface import PTDevicesResponse, PTDevicesResponseData
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.ptdevices.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ptdevices_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.ptdevices.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ptdevices_level() -> PTDevicesResponse:
|
||||
"""Mock a PTLevel device."""
|
||||
data = load_json_object_fixture("ptdevices_level.json", DOMAIN)
|
||||
return PTDevicesResponse(
|
||||
code=200,
|
||||
body=cast(PTDevicesResponseData, data),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ptdevices_interface(
|
||||
mock_ptdevices_level: PTDevicesResponse,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock a PTDevices Interface."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.ptdevices.Interface",
|
||||
autospec=True,
|
||||
) as mock_interface,
|
||||
patch(
|
||||
"homeassistant.components.ptdevices.config_flow.Interface",
|
||||
new=mock_interface,
|
||||
),
|
||||
):
|
||||
interface = mock_interface.return_value
|
||||
interface.get_data.return_value = mock_ptdevices_level
|
||||
|
||||
yield interface
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ptdevices_config_entry() -> MockConfigEntry:
|
||||
"""Return a mocked ptdevice configuration entry."""
|
||||
return MockConfigEntry(
|
||||
version=1,
|
||||
domain=DOMAIN,
|
||||
title="User Name",
|
||||
data={
|
||||
CONF_API_TOKEN: "test-api-token",
|
||||
},
|
||||
unique_id="1234",
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"C0FFEEC0FFEE": {
|
||||
"id": 1,
|
||||
"device_id": "C0FFEEC0FFEE",
|
||||
"share_id": "someID",
|
||||
"user_id": 1234,
|
||||
"user_name": "User Name",
|
||||
"user_email": "userEmail@email.com",
|
||||
"device_type": "level",
|
||||
"local_ip": "192.168.1.100",
|
||||
"title": "Home",
|
||||
"version": 208,
|
||||
"lat": 40.62669,
|
||||
"lng": -82.031121,
|
||||
"address": "1234 Test Road, City, Country",
|
||||
"supplier_code": null,
|
||||
"status_number": 2,
|
||||
"status": "working",
|
||||
"delivery_notes": null,
|
||||
"units": "Metric",
|
||||
"reported": "Dec 17th, 9:37 AM",
|
||||
"tx_reported": "Dec 17th, 8:15 AM",
|
||||
"last_updated_on": "1 hour ago",
|
||||
"wifi_signal": 100,
|
||||
"tx_signal": -76.0,
|
||||
"percent_level": 50,
|
||||
"battery_voltage": 5.69,
|
||||
"battery_status": "good",
|
||||
"battery_status_number": 1,
|
||||
"volume_level": 2387.837753,
|
||||
"volume_level_oz": 80742.4,
|
||||
"max_volume": 1269,
|
||||
"max_volume_oz": 162432,
|
||||
"enclosure_temperature": -0.3,
|
||||
"depth": 6,
|
||||
"power_x": 30,
|
||||
"power_y": 7,
|
||||
"power_z": 12,
|
||||
"shape": "vertical cylinder",
|
||||
"diameter": 6,
|
||||
"width": null,
|
||||
"length": null,
|
||||
"temperature_units": "C",
|
||||
"depth_level": 0.909066
|
||||
},
|
||||
"C0FFEFC0FFEF": {
|
||||
"id": 2,
|
||||
"device_id": "C0FFEFC0FFEF",
|
||||
"share_id": "someID",
|
||||
"created": "Jan 1st 1970, 0:00 AM",
|
||||
"user_id": 1234,
|
||||
"user_name": "User Name",
|
||||
"user_email": "userEmail@email.com",
|
||||
"device_type": "level",
|
||||
"local_ip": "192.168.1.101",
|
||||
"title": "Garden rain barrel",
|
||||
"version": "407",
|
||||
"lat": 43.147985,
|
||||
"lng": -29.17074,
|
||||
"address": "1234 Test Road, City, Country",
|
||||
"supplier_code": null,
|
||||
"status_number": 2,
|
||||
"status": "working",
|
||||
"delivery_notes": null,
|
||||
"units": "US Imperial",
|
||||
"reported": "Dec 17th, 9:38 AM",
|
||||
"tx_reported": "Dec 17th, 9:38 AM",
|
||||
"last_updated_on": "13 seconds ago",
|
||||
"wifi_signal": 42,
|
||||
"tx_signal": 0.0,
|
||||
"percent_level": 141,
|
||||
"volume_level": 4.542494,
|
||||
"volume_level_oz": 153.6,
|
||||
"max_volume": 1.2,
|
||||
"max_volume_oz": 153.6,
|
||||
"enclosure_temperature": 37.3,
|
||||
"probe_temperature": 18.9,
|
||||
"depth": 0.85,
|
||||
"shape": "vertical cylinder",
|
||||
"diameter": 0.5,
|
||||
"width": null,
|
||||
"length": null,
|
||||
"temperature_units": "C",
|
||||
"depth_level": 0.363474
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,811 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_level_depth-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.garden_rain_barrel_level_depth',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Level depth',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Level depth',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.LEVEL_DEPTH: 'depth_level'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_depth_level',
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_level_depth-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'Garden rain barrel Level depth',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_level_depth',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.363474',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_level_percent-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.garden_rain_barrel_level_percent',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Level percent',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Level percent',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.LEVEL_PERCENT: 'percent_level'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_percent_level',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_level_percent-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Garden rain barrel Level percent',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_level_percent',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '141',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_level_volume-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.garden_rain_barrel_level_volume',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Level volume',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLUME_STORAGE: 'volume_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Level volume',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.LEVEL_VOLUME: 'volume_level'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_volume_level',
|
||||
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_level_volume-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_storage',
|
||||
'friendly_name': 'Garden rain barrel Level volume',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_level_volume',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4.542494',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_lora_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_lora_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'LoRa signal strength',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'LoRa signal strength',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.TX_SIGNAL_STRENGTH: 'tx_signal'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_tx_signal',
|
||||
'unit_of_measurement': 'dBm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_lora_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'signal_strength',
|
||||
'friendly_name': 'Garden rain barrel LoRa signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'dBm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_lora_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_probe_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.garden_rain_barrel_probe_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Probe temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Probe temperature',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.PROBE_TEMPERATURE: 'probe_temperature'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_probe_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_probe_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Garden rain barrel Probe temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_probe_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '18.9',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'working',
|
||||
'not_connected_yet',
|
||||
'not_connected',
|
||||
'transmitter_not_reporting',
|
||||
'press_transmitter_connect_button',
|
||||
'power_internet_out_or_receiver_not_working',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.garden_rain_barrel_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.DEVICE_STATUS: 'status'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Garden rain barrel Status',
|
||||
'options': list([
|
||||
'working',
|
||||
'not_connected_yet',
|
||||
'not_connected',
|
||||
'transmitter_not_reporting',
|
||||
'press_transmitter_connect_button',
|
||||
'power_internet_out_or_receiver_not_working',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'working',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_wi_fi_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_wi_fi_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Wi-Fi signal strength',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wi-Fi signal strength',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.DEVICE_WIFI_STRENGTH: 'wifi_signal'>,
|
||||
'unique_id': '1234_C0FFEFC0FFEF_wifi_signal',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.garden_rain_barrel_wi_fi_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Garden rain barrel Wi-Fi signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.garden_rain_barrel_wi_fi_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '42',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_battery_voltage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.home_battery_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Battery voltage',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery voltage',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.DEVICE_BATTERY_VOLTAGE: 'battery_voltage'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_battery_voltage',
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_battery_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Home Battery voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_battery_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.69',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_level_depth-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_level_depth',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Level depth',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Level depth',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.LEVEL_DEPTH: 'depth_level'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_depth_level',
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_level_depth-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'Home Level depth',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_level_depth',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.909066',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_level_percent-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_level_percent',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Level percent',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Level percent',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.LEVEL_PERCENT: 'percent_level'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_percent_level',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_level_percent-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Level percent',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_level_percent',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '50',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_level_volume-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_level_volume',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Level volume',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLUME_STORAGE: 'volume_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Level volume',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.LEVEL_VOLUME: 'volume_level'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_volume_level',
|
||||
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_level_volume-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_storage',
|
||||
'friendly_name': 'Home Level volume',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_level_volume',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2387.837753',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_lora_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.home_lora_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'LoRa signal strength',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'LoRa signal strength',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.TX_SIGNAL_STRENGTH: 'tx_signal'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_tx_signal',
|
||||
'unit_of_measurement': 'dBm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_lora_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'signal_strength',
|
||||
'friendly_name': 'Home LoRa signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'dBm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_lora_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-76.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'working',
|
||||
'not_connected_yet',
|
||||
'not_connected',
|
||||
'transmitter_not_reporting',
|
||||
'press_transmitter_connect_button',
|
||||
'power_internet_out_or_receiver_not_working',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.DEVICE_STATUS: 'status'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Home Status',
|
||||
'options': list([
|
||||
'working',
|
||||
'not_connected_yet',
|
||||
'not_connected',
|
||||
'transmitter_not_reporting',
|
||||
'press_transmitter_connect_button',
|
||||
'power_internet_out_or_receiver_not_working',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'working',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_wi_fi_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.home_wi_fi_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Wi-Fi signal strength',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wi-Fi signal strength',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesSensors.DEVICE_WIFI_STRENGTH: 'wifi_signal'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_wifi_signal',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.home_wi_fi_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Wi-Fi signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_wi_fi_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Test the PTDevices config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioptdevices import PTDevicesRequestError, PTDevicesUnauthorizedError
|
||||
from aioptdevices.interface import PTDevicesResponse
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.ptdevices.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_flow_success(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a successful creation of config entries via user configuration."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_TOKEN: "test-api-token"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "User Name"
|
||||
assert result["result"].unique_id == "1234"
|
||||
assert result["data"] == {
|
||||
CONF_API_TOKEN: "test-api-token",
|
||||
}
|
||||
|
||||
assert len(mock_ptdevices_interface.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_flow_duplicate_device(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_setup_entry: AsyncMock,
|
||||
mock_ptdevices_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a duplicate config flow."""
|
||||
mock_ptdevices_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_TOKEN: "test-api-token"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(PTDevicesUnauthorizedError, "invalid_access_token"),
|
||||
(PTDevicesRequestError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_setup_entry: AsyncMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test flow errors."""
|
||||
mock_ptdevices_interface.get_data.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_TOKEN: "test-api-token"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_ptdevices_interface.get_data.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_TOKEN: "test-api-token"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_flow_no_devices(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_setup_entry: AsyncMock,
|
||||
mock_ptdevices_level: PTDevicesResponse,
|
||||
) -> None:
|
||||
"""Test A flow with no devices in the account."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# No devices
|
||||
mock_ptdevices_interface.get_data.return_value = PTDevicesResponse(
|
||||
code=200,
|
||||
body={},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_TOKEN: "test-api-token"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_devices_found"}
|
||||
|
||||
# Reset the mock to the default return value
|
||||
mock_ptdevices_interface.get_data.return_value = mock_ptdevices_level
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_TOKEN: "test-api-token"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Test for PTDevices sensors."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.ptdevices._PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_ptdevices_config_entry)
|
||||
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, mock_ptdevices_config_entry.entry_id
|
||||
)
|
||||
Reference in New Issue
Block a user