mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Add SensorPush Cloud integration (#134223)
This commit is contained in:
parent
0d8c449ff4
commit
73442e8443
@ -438,6 +438,7 @@ homeassistant.components.select.*
|
|||||||
homeassistant.components.sensibo.*
|
homeassistant.components.sensibo.*
|
||||||
homeassistant.components.sensirion_ble.*
|
homeassistant.components.sensirion_ble.*
|
||||||
homeassistant.components.sensor.*
|
homeassistant.components.sensor.*
|
||||||
|
homeassistant.components.sensorpush_cloud.*
|
||||||
homeassistant.components.sensoterra.*
|
homeassistant.components.sensoterra.*
|
||||||
homeassistant.components.senz.*
|
homeassistant.components.senz.*
|
||||||
homeassistant.components.sfr_box.*
|
homeassistant.components.sfr_box.*
|
||||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1342,6 +1342,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sensorpro/ @bdraco
|
/tests/components/sensorpro/ @bdraco
|
||||||
/homeassistant/components/sensorpush/ @bdraco
|
/homeassistant/components/sensorpush/ @bdraco
|
||||||
/tests/components/sensorpush/ @bdraco
|
/tests/components/sensorpush/ @bdraco
|
||||||
|
/homeassistant/components/sensorpush_cloud/ @sstallion
|
||||||
|
/tests/components/sensorpush_cloud/ @sstallion
|
||||||
/homeassistant/components/sensoterra/ @markruys
|
/homeassistant/components/sensoterra/ @markruys
|
||||||
/tests/components/sensoterra/ @markruys
|
/tests/components/sensoterra/ @markruys
|
||||||
/homeassistant/components/sentry/ @dcramer @frenck
|
/homeassistant/components/sentry/ @dcramer @frenck
|
||||||
|
5
homeassistant/brands/sensorpush.json
Normal file
5
homeassistant/brands/sensorpush.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "sensorpush",
|
||||||
|
"name": "SensorPush",
|
||||||
|
"integrations": ["sensorpush", "sensorpush_cloud"]
|
||||||
|
}
|
28
homeassistant/components/sensorpush_cloud/__init__.py
Normal file
28
homeassistant/components/sensorpush_cloud/__init__.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""The SensorPush Cloud integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import SensorPushCloudConfigEntry, SensorPushCloudCoordinator
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: SensorPushCloudConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up SensorPush Cloud from a config entry."""
|
||||||
|
coordinator = SensorPushCloudCoordinator(hass, entry)
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: SensorPushCloudConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
64
homeassistant/components/sensorpush_cloud/config_flow.py
Normal file
64
homeassistant/components/sensorpush_cloud/config_flow.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""Config flow for the SensorPush Cloud integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sensorpush_ha import SensorPushCloudApi, SensorPushCloudAuthError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
TextSelector,
|
||||||
|
TextSelectorConfig,
|
||||||
|
TextSelectorType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPushCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for SensorPush Cloud."""
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
email, password = user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||||
|
await self.async_set_unique_id(email)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
clientsession = async_get_clientsession(self.hass)
|
||||||
|
api = SensorPushCloudApi(email, password, clientsession)
|
||||||
|
try:
|
||||||
|
await api.async_authorize()
|
||||||
|
except SensorPushCloudAuthError:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected error")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(title=email, data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_EMAIL): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.EMAIL, autocomplete="username"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Required(CONF_PASSWORD): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.PASSWORD,
|
||||||
|
autocomplete="current-password",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
12
homeassistant/components/sensorpush_cloud/const.py
Normal file
12
homeassistant/components/sensorpush_cloud/const.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"""Constants for the SensorPush Cloud integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
DOMAIN: Final = "sensorpush_cloud"
|
||||||
|
|
||||||
|
UPDATE_INTERVAL: Final = timedelta(seconds=60)
|
||||||
|
MAX_TIME_BETWEEN_UPDATES: Final = UPDATE_INTERVAL * 60
|
45
homeassistant/components/sensorpush_cloud/coordinator.py
Normal file
45
homeassistant/components/sensorpush_cloud/coordinator.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"""Coordinator for the SensorPush Cloud integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sensorpush_ha import (
|
||||||
|
SensorPushCloudApi,
|
||||||
|
SensorPushCloudData,
|
||||||
|
SensorPushCloudError,
|
||||||
|
SensorPushCloudHelper,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import LOGGER, UPDATE_INTERVAL
|
||||||
|
|
||||||
|
type SensorPushCloudConfigEntry = ConfigEntry[SensorPushCloudCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPushCloudCoordinator(DataUpdateCoordinator[dict[str, SensorPushCloudData]]):
|
||||||
|
"""SensorPush Cloud coordinator."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: SensorPushCloudConfigEntry) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name=entry.title,
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
config_entry=entry,
|
||||||
|
)
|
||||||
|
email, password = entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD]
|
||||||
|
clientsession = async_get_clientsession(hass)
|
||||||
|
api = SensorPushCloudApi(email, password, clientsession)
|
||||||
|
self.helper = SensorPushCloudHelper(api)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, SensorPushCloudData]:
|
||||||
|
"""Fetch data from API endpoints."""
|
||||||
|
try:
|
||||||
|
return await self.helper.async_get_data()
|
||||||
|
except SensorPushCloudError as e:
|
||||||
|
raise UpdateFailed(e) from e
|
11
homeassistant/components/sensorpush_cloud/manifest.json
Normal file
11
homeassistant/components/sensorpush_cloud/manifest.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"domain": "sensorpush_cloud",
|
||||||
|
"name": "SensorPush Cloud",
|
||||||
|
"codeowners": ["@sstallion"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/sensorpush_cloud",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["sensorpush_api", "sensorpush_ha"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["sensorpush-api==2.1.1", "sensorpush-ha==1.3.2"]
|
||||||
|
}
|
68
homeassistant/components/sensorpush_cloud/quality_scale.yaml
Normal file
68
homeassistant/components/sensorpush_cloud/quality_scale.yaml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom 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: Integration does not register custom actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup: done
|
||||||
|
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: Integration does not register custom actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not support options flow.
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
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: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: todo
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: todo
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
158
homeassistant/components/sensorpush_cloud/sensor.py
Normal file
158
homeassistant/components/sensorpush_cloud/sensor.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"""Support for SensorPush Cloud sensors."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
UnitOfElectricPotential,
|
||||||
|
UnitOfLength,
|
||||||
|
UnitOfPressure,
|
||||||
|
UnitOfTemperature,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DOMAIN, MAX_TIME_BETWEEN_UPDATES
|
||||||
|
from .coordinator import SensorPushCloudConfigEntry, SensorPushCloudCoordinator
|
||||||
|
|
||||||
|
ATTR_ALTITUDE: Final = "altitude"
|
||||||
|
ATTR_ATMOSPHERIC_PRESSURE: Final = "atmospheric_pressure"
|
||||||
|
ATTR_BATTERY_VOLTAGE: Final = "battery_voltage"
|
||||||
|
ATTR_DEWPOINT: Final = "dewpoint"
|
||||||
|
ATTR_HUMIDITY: Final = "humidity"
|
||||||
|
ATTR_SIGNAL_STRENGTH: Final = "signal_strength"
|
||||||
|
ATTR_VAPOR_PRESSURE: Final = "vapor_pressure"
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
SENSORS: Final[tuple[SensorEntityDescription, ...]] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_ALTITUDE,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
translation_key="altitude",
|
||||||
|
native_unit_of_measurement=UnitOfLength.FEET,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_ATMOSPHERIC_PRESSURE,
|
||||||
|
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_BATTERY_VOLTAGE,
|
||||||
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
translation_key="battery_voltage",
|
||||||
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_DEWPOINT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
translation_key="dewpoint",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_HUMIDITY,
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_SIGNAL_STRENGTH,
|
||||||
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_TEMPERATURE,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=ATTR_VAPOR_PRESSURE,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
translation_key="vapor_pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SensorPushCloudConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up SensorPush Cloud sensors."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
SensorPushCloudSensor(coordinator, entity_description, device_id)
|
||||||
|
for entity_description in SENSORS
|
||||||
|
for device_id in coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPushCloudSensor(
|
||||||
|
CoordinatorEntity[SensorPushCloudCoordinator], SensorEntity
|
||||||
|
):
|
||||||
|
"""SensorPush Cloud sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SensorPushCloudCoordinator,
|
||||||
|
entity_description: SensorEntityDescription,
|
||||||
|
device_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self.device_id = device_id
|
||||||
|
|
||||||
|
device = coordinator.data[device_id]
|
||||||
|
self._attr_unique_id = f"{device.device_id}_{entity_description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device.device_id)},
|
||||||
|
manufacturer=device.manufacturer,
|
||||||
|
model=device.model,
|
||||||
|
name=device.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return true if entity is available."""
|
||||||
|
if self.device_id in self.coordinator.data:
|
||||||
|
last_update = self.coordinator.data[self.device_id].last_update
|
||||||
|
if dt_util.utcnow() >= (last_update + MAX_TIME_BETWEEN_UPDATES):
|
||||||
|
return False
|
||||||
|
return super().available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the value reported by the sensor."""
|
||||||
|
return self.coordinator.data[self.device_id][self.entity_description.key]
|
40
homeassistant/components/sensorpush_cloud/strings.json
Normal file
40
homeassistant/components/sensorpush_cloud/strings.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "To activate API access, log in to the [Gateway Cloud Dashboard](https://dashboard.sensorpush.com/) and agree to the terms of service. Devices are not available until activated with the SensorPush app on iOS or Android.",
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"email": "The email address used to log in to the SensorPush Gateway Cloud Dashboard",
|
||||||
|
"password": "The password used to log in to the SensorPush Gateway Cloud Dashboard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"altitude": {
|
||||||
|
"name": "Altitude"
|
||||||
|
},
|
||||||
|
"battery_voltage": {
|
||||||
|
"name": "Battery voltage"
|
||||||
|
},
|
||||||
|
"dewpoint": {
|
||||||
|
"name": "Dew point"
|
||||||
|
},
|
||||||
|
"vapor_pressure": {
|
||||||
|
"name": "Vapor pressure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -546,6 +546,7 @@ FLOWS = {
|
|||||||
"sensirion_ble",
|
"sensirion_ble",
|
||||||
"sensorpro",
|
"sensorpro",
|
||||||
"sensorpush",
|
"sensorpush",
|
||||||
|
"sensorpush_cloud",
|
||||||
"sensoterra",
|
"sensoterra",
|
||||||
"sentry",
|
"sentry",
|
||||||
"senz",
|
"senz",
|
||||||
|
@ -5587,9 +5587,20 @@
|
|||||||
},
|
},
|
||||||
"sensorpush": {
|
"sensorpush": {
|
||||||
"name": "SensorPush",
|
"name": "SensorPush",
|
||||||
"integration_type": "hub",
|
"integrations": {
|
||||||
"config_flow": true,
|
"sensorpush": {
|
||||||
"iot_class": "local_push"
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_push",
|
||||||
|
"name": "SensorPush"
|
||||||
|
},
|
||||||
|
"sensorpush_cloud": {
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"name": "SensorPush Cloud"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sensoterra": {
|
"sensoterra": {
|
||||||
"name": "Sensoterra",
|
"name": "Sensoterra",
|
||||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@ -4136,6 +4136,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.sensorpush_cloud.*]
|
||||||
|
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.sensoterra.*]
|
[mypy-homeassistant.components.sensoterra.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
6
requirements_all.txt
generated
6
requirements_all.txt
generated
@ -2696,9 +2696,15 @@ sensirion-ble==0.1.1
|
|||||||
# homeassistant.components.sensorpro
|
# homeassistant.components.sensorpro
|
||||||
sensorpro-ble==0.5.3
|
sensorpro-ble==0.5.3
|
||||||
|
|
||||||
|
# homeassistant.components.sensorpush_cloud
|
||||||
|
sensorpush-api==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.sensorpush
|
# homeassistant.components.sensorpush
|
||||||
sensorpush-ble==1.7.1
|
sensorpush-ble==1.7.1
|
||||||
|
|
||||||
|
# homeassistant.components.sensorpush_cloud
|
||||||
|
sensorpush-ha==1.3.2
|
||||||
|
|
||||||
# homeassistant.components.sensoterra
|
# homeassistant.components.sensoterra
|
||||||
sensoterra==2.0.1
|
sensoterra==2.0.1
|
||||||
|
|
||||||
|
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@ -2175,9 +2175,15 @@ sensirion-ble==0.1.1
|
|||||||
# homeassistant.components.sensorpro
|
# homeassistant.components.sensorpro
|
||||||
sensorpro-ble==0.5.3
|
sensorpro-ble==0.5.3
|
||||||
|
|
||||||
|
# homeassistant.components.sensorpush_cloud
|
||||||
|
sensorpush-api==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.sensorpush
|
# homeassistant.components.sensorpush
|
||||||
sensorpush-ble==1.7.1
|
sensorpush-ble==1.7.1
|
||||||
|
|
||||||
|
# homeassistant.components.sensorpush_cloud
|
||||||
|
sensorpush-ha==1.3.2
|
||||||
|
|
||||||
# homeassistant.components.sensoterra
|
# homeassistant.components.sensoterra
|
||||||
sensoterra==2.0.1
|
sensoterra==2.0.1
|
||||||
|
|
||||||
|
1
tests/components/sensorpush_cloud/__init__.py
Normal file
1
tests/components/sensorpush_cloud/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the SensorPush Cloud integration."""
|
60
tests/components/sensorpush_cloud/conftest.py
Normal file
60
tests/components/sensorpush_cloud/conftest.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""Common fixtures for the SensorPush Cloud tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sensorpush_ha import SensorPushCloudApi
|
||||||
|
|
||||||
|
from homeassistant.components.sensorpush_cloud.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_EMAIL
|
||||||
|
|
||||||
|
from .const import CONF_DATA, MOCK_DATA
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_api() -> Generator[AsyncMock]:
|
||||||
|
"""Override SensorPushCloudApi."""
|
||||||
|
mock_api = AsyncMock(SensorPushCloudApi)
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.sensorpush_cloud.config_flow.SensorPushCloudApi",
|
||||||
|
return_value=mock_api,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
yield mock_api
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_helper() -> Generator[AsyncMock]:
|
||||||
|
"""Override SensorPushCloudHelper."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.sensorpush_cloud.coordinator.SensorPushCloudHelper",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_helper,
|
||||||
|
):
|
||||||
|
helper = mock_helper.return_value
|
||||||
|
helper.async_get_data.return_value = MOCK_DATA
|
||||||
|
yield helper
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""ConfigEntry mock."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN, data=CONF_DATA, unique_id=CONF_DATA[CONF_EMAIL]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.sensorpush_cloud.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
32
tests/components/sensorpush_cloud/const.py
Normal file
32
tests/components/sensorpush_cloud/const.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Constants for the SensorPush Cloud tests."""
|
||||||
|
|
||||||
|
from sensorpush_ha import SensorPushCloudData
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
CONF_DATA = {
|
||||||
|
CONF_EMAIL: "test@example.com",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
}
|
||||||
|
|
||||||
|
NUM_MOCK_DEVICES = 3
|
||||||
|
|
||||||
|
MOCK_DATA = {
|
||||||
|
f"test-sensor-device-id-{i}": SensorPushCloudData(
|
||||||
|
device_id=f"test-sensor-device-id-{i}",
|
||||||
|
manufacturer=f"test-sensor-manufacturer-{i}",
|
||||||
|
model=f"test-sensor-model-{i}",
|
||||||
|
name=f"test-sensor-name-{i}",
|
||||||
|
altitude=0.0,
|
||||||
|
atmospheric_pressure=0.0,
|
||||||
|
battery_voltage=0.0,
|
||||||
|
dewpoint=0.0,
|
||||||
|
humidity=0.0,
|
||||||
|
last_update=dt_util.utcnow(),
|
||||||
|
signal_strength=0.0,
|
||||||
|
temperature=0.0,
|
||||||
|
vapor_pressure=0.0,
|
||||||
|
)
|
||||||
|
for i in range(NUM_MOCK_DEVICES)
|
||||||
|
}
|
1267
tests/components/sensorpush_cloud/snapshots/test_sensor.ambr
Normal file
1267
tests/components/sensorpush_cloud/snapshots/test_sensor.ambr
Normal file
File diff suppressed because it is too large
Load Diff
95
tests/components/sensorpush_cloud/test_config_flow.py
Normal file
95
tests/components/sensorpush_cloud/test_config_flow.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""Test the SensorPush Cloud config flow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sensorpush_ha import SensorPushCloudAuthError
|
||||||
|
|
||||||
|
from homeassistant.components.sensorpush_cloud.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from .const import CONF_DATA, CONF_EMAIL
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_api: AsyncMock,
|
||||||
|
mock_helper: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test user initialized flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
CONF_DATA,
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "test@example.com"
|
||||||
|
assert result["data"] == CONF_DATA
|
||||||
|
assert result["result"].unique_id == CONF_DATA[CONF_EMAIL]
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_already_configured(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_api: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test we fail on a duplicate entry in the user flow."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("error", "expected"),
|
||||||
|
[(SensorPushCloudAuthError, "invalid_auth"), (Exception, "unknown")],
|
||||||
|
)
|
||||||
|
async def test_user_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_api: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
error: Exception,
|
||||||
|
expected: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test we display errors in the user flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
mock_api.async_authorize.side_effect = error
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], CONF_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {"base": expected}
|
||||||
|
|
||||||
|
# Show we can recover from errors:
|
||||||
|
mock_api.async_authorize.side_effect = None
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], CONF_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "test@example.com"
|
||||||
|
assert result["data"] == CONF_DATA
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
29
tests/components/sensorpush_cloud/test_sensor.py
Normal file
29
tests/components/sensorpush_cloud/test_sensor.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""Test SensorPush Cloud sensors."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: EntityRegistry,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_helper: AsyncMock,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test we can read sensors."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
Loading…
x
Reference in New Issue
Block a user