1
0
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:
Matthew Gibson
2026-05-04 16:15:52 -04:00
committed by GitHub
parent 28d65e987c
commit 8c8a863867
23 changed files with 1880 additions and 0 deletions
+1
View File
@@ -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
View File
@@ -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%]"
}
}
}
+1
View File
@@ -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",
Generated
+10
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+13
View File
@@ -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()
+68
View File
@@ -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
+31
View File
@@ -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
)