mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-09 12:57:58 -05:00
More testing, fixes
This commit is contained in:
38
custom_components/garmin_connect/quality_scale.yaml
Normal file
38
custom_components/garmin_connect/quality_scale.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Garmin Connect Quality Scale
|
||||||
|
|
||||||
|
# This file tracks the integration quality scale for Home Assistant Core.
|
||||||
|
# See: https://developers.home-assistant.io/docs/core/integration-quality-scale/
|
||||||
|
|
||||||
|
rules:
|
||||||
|
# Bronze tier (minimum for Core)
|
||||||
|
config-flow: done
|
||||||
|
unique-config-entry: done
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
appropriate-polling: done
|
||||||
|
action-exceptions: todo
|
||||||
|
test-before-configure: done
|
||||||
|
runtime-data: todo
|
||||||
|
|
||||||
|
# Silver tier
|
||||||
|
reauthentication-flow: done
|
||||||
|
entity-translations: done
|
||||||
|
parallel-updates: todo
|
||||||
|
config-entry-unloading: done
|
||||||
|
log-when-unavailable: todo
|
||||||
|
entity-unavailable: todo
|
||||||
|
|
||||||
|
# Gold tier
|
||||||
|
entity-device-class: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
dynamic-devices: exempt # Single cloud account, not device-based
|
||||||
|
discovery: exempt # Cloud service, not discoverable
|
||||||
|
diagnostics: todo
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: todo
|
||||||
|
stale-devices: exempt # Single device per config entry
|
||||||
|
|
||||||
|
# Platinum tier
|
||||||
|
async-dependency: todo
|
||||||
|
inject-websession: todo
|
||||||
|
strict-typing: todo
|
||||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
EntityCategory,
|
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfMass,
|
UnitOfMass,
|
||||||
UnitOfTime,
|
UnitOfTime,
|
||||||
@@ -46,7 +45,6 @@ ACTIVITY_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
native_unit_of_measurement="steps",
|
native_unit_of_measurement="steps",
|
||||||
icon="mdi:target",
|
icon="mdi:target",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
),
|
),
|
||||||
GarminConnectSensorEntityDescription(
|
GarminConnectSensorEntityDescription(
|
||||||
key="totalDistanceMeters",
|
key="totalDistanceMeters",
|
||||||
@@ -76,7 +74,6 @@ ACTIVITY_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
native_unit_of_measurement="floors",
|
native_unit_of_measurement="floors",
|
||||||
icon="mdi:target",
|
icon="mdi:target",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,7 +99,6 @@ CALORIES_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
native_unit_of_measurement="kcal",
|
native_unit_of_measurement="kcal",
|
||||||
icon="mdi:fire-circle",
|
icon="mdi:fire-circle",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
),
|
),
|
||||||
GarminConnectSensorEntityDescription(
|
GarminConnectSensorEntityDescription(
|
||||||
key="burnedKilocalories",
|
key="burnedKilocalories",
|
||||||
@@ -158,7 +154,6 @@ HEART_RATE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement="bpm",
|
native_unit_of_measurement="bpm",
|
||||||
icon="mdi:heart-pulse",
|
icon="mdi:heart-pulse",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
|
|
||||||
),
|
),
|
||||||
GarminConnectSensorEntityDescription(
|
GarminConnectSensorEntityDescription(
|
||||||
@@ -423,7 +418,6 @@ HYDRATION_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
|
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
|
||||||
icon="mdi:water-check",
|
icon="mdi:water-check",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
),
|
),
|
||||||
GarminConnectSensorEntityDescription(
|
GarminConnectSensorEntityDescription(
|
||||||
key="dailyAverageInML",
|
key="dailyAverageInML",
|
||||||
@@ -506,7 +500,6 @@ INTENSITY_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
icon="mdi:target",
|
icon="mdi:target",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Enter your Garmin Connect login information",
|
"title": "Garmin Connect login",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"title": "Enter your Garmin Connect MFA code",
|
"title": "Garmin Connect MFA code",
|
||||||
"data": {
|
"data": {
|
||||||
"mfa_code": "MFA code (6-digits)"
|
"mfa_code": "MFA code (6-digits)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
"title": "[%key:component::garmin_connect::config::step::user::title%]",
|
"title": "Garmin Connect reauthentication",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Enter your Garmin Connect credentials",
|
"title": "Garmin Connect login",
|
||||||
"data": {
|
"data": {
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
@@ -10,13 +10,13 @@
|
|||||||
"description": "Enter your credentials."
|
"description": "Enter your credentials."
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"title": "Enter your Garmin Connect MFA code",
|
"title": "Garmin Connect MFA code",
|
||||||
"data": {
|
"data": {
|
||||||
"mfa_code": "MFA code (6-digits)"
|
"mfa_code": "MFA code (6-digits)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
"title": "Enter your Garmin Connect credentials",
|
"title": "Garmin Connect reauthentication",
|
||||||
"data": {
|
"data": {
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
|
|||||||
144
docs/garmin_connect.markdown
Normal file
144
docs/garmin_connect.markdown
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
title: Garmin Connect
|
||||||
|
description: Instructions on how to integrate Garmin Connect health data within Home Assistant.
|
||||||
|
ha_category:
|
||||||
|
- Health
|
||||||
|
- Sensor
|
||||||
|
ha_iot_class: Cloud Polling
|
||||||
|
ha_release: "2025.2"
|
||||||
|
ha_domain: garmin_connect
|
||||||
|
ha_platforms:
|
||||||
|
- sensor
|
||||||
|
ha_integration_type: integration
|
||||||
|
ha_codeowners:
|
||||||
|
- '@cyberjunky'
|
||||||
|
ha_config_flow: true
|
||||||
|
---
|
||||||
|
|
||||||
|
The **Garmin Connect** {% term integration %} allows you to expose health and fitness data from [Garmin Connect](https://connect.garmin.com/) to Home Assistant.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You need a Garmin Connect account with at least one Garmin device that syncs data to Garmin Connect.
|
||||||
|
|
||||||
|
{% include integrations/config_flow.md %}
|
||||||
|
|
||||||
|
## Sensors
|
||||||
|
|
||||||
|
This integration provides **97+ sensors** covering various health and fitness metrics. Sensors are grouped into the following categories:
|
||||||
|
|
||||||
|
### Activity & Steps
|
||||||
|
|
||||||
|
- **Total Steps** - Daily step count
|
||||||
|
- **Daily Step Goal** - Your configured step target
|
||||||
|
- **Total Distance** - Distance walked/run
|
||||||
|
- **Floors Ascended/Descended** - Floors climbed
|
||||||
|
|
||||||
|
### Calories
|
||||||
|
|
||||||
|
- **Total Calories** - Total daily calorie burn
|
||||||
|
- **Active Calories** - Calories burned through activity
|
||||||
|
- **BMR Calories** - Basal metabolic rate calories
|
||||||
|
|
||||||
|
### Heart Rate
|
||||||
|
|
||||||
|
- **Resting Heart Rate** - Daily resting HR
|
||||||
|
- **Min/Max Heart Rate** - Daily HR range
|
||||||
|
- **Last 7 Days Avg HR** - Weekly average
|
||||||
|
|
||||||
|
### Stress & Recovery
|
||||||
|
|
||||||
|
- **Avg/Max Stress Level** - Stress measurements (0-100)
|
||||||
|
- **Stress Durations** - Time in rest/activity/low/medium/high stress
|
||||||
|
|
||||||
|
### Sleep
|
||||||
|
|
||||||
|
- **Sleep Score** - Overall sleep quality score
|
||||||
|
- **Sleep Duration** - Time asleep
|
||||||
|
- **Awake Duration** - Time awake during sleep
|
||||||
|
|
||||||
|
### Body Battery
|
||||||
|
|
||||||
|
- **Body Battery** - Current energy level (0-100)
|
||||||
|
- **Charged/Drained** - Energy gained/spent
|
||||||
|
|
||||||
|
### Body Composition
|
||||||
|
|
||||||
|
- **Weight** - Body weight
|
||||||
|
- **BMI** - Body Mass Index
|
||||||
|
- **Body Fat/Water** - Percentage measurements
|
||||||
|
- **Muscle/Bone Mass** - Mass measurements
|
||||||
|
|
||||||
|
### Hydration
|
||||||
|
|
||||||
|
- **Hydration** - Daily water intake
|
||||||
|
- **Hydration Goal** - Target intake
|
||||||
|
- **Sweat Loss** - Estimated fluid loss
|
||||||
|
|
||||||
|
### Health Monitoring
|
||||||
|
|
||||||
|
- **SpO2** - Blood oxygen levels (average, lowest, latest)
|
||||||
|
- **HRV Status** - Heart rate variability
|
||||||
|
- **Respiration Rate** - Breathing measurements
|
||||||
|
|
||||||
|
### Fitness & Performance
|
||||||
|
|
||||||
|
- **Fitness Age** - Estimated fitness age
|
||||||
|
- **Endurance Score** - Overall endurance rating
|
||||||
|
|
||||||
|
### Gear Tracking
|
||||||
|
|
||||||
|
Gear sensors are dynamically created for each piece of equipment registered in Garmin Connect (shoes, bikes, etc.). They track total distance and usage statistics.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
### Add body composition
|
||||||
|
|
||||||
|
Add body composition metrics to Garmin Connect.
|
||||||
|
|
||||||
|
| Data attribute | Required | Description |
|
||||||
|
| ---------------------- | -------- | ----------- |
|
||||||
|
| `weight` | Yes | Weight in kilograms |
|
||||||
|
| `timestamp` | No | ISO format timestamp |
|
||||||
|
| `bmi` | No | Body Mass Index |
|
||||||
|
| `percent_fat` | No | Body fat percentage |
|
||||||
|
| `muscle_mass` | No | Muscle mass in kg |
|
||||||
|
| `bone_mass` | No | Bone mass in kg |
|
||||||
|
| `body_water` | No | Body water percentage |
|
||||||
|
| `physique_rating` | No | Physique rating (1-9) |
|
||||||
|
| `visceral_fat` | No | Visceral fat rating |
|
||||||
|
| `metabolic_age` | No | Metabolic age |
|
||||||
|
|
||||||
|
### Add blood pressure
|
||||||
|
|
||||||
|
Add blood pressure measurements to Garmin Connect.
|
||||||
|
|
||||||
|
| Data attribute | Required | Description |
|
||||||
|
| ---------------------- | -------- | ----------- |
|
||||||
|
| `systolic` | Yes | Systolic pressure (mmHg) |
|
||||||
|
| `diastolic` | Yes | Diastolic pressure (mmHg) |
|
||||||
|
| `pulse` | Yes | Pulse rate (bpm) |
|
||||||
|
| `timestamp` | No | ISO format timestamp |
|
||||||
|
| `notes` | No | Notes about the measurement |
|
||||||
|
|
||||||
|
### Set active gear
|
||||||
|
|
||||||
|
Set a gear item as the default for an activity type.
|
||||||
|
|
||||||
|
| Data attribute | Required | Description |
|
||||||
|
| ---------------------- | -------- | ----------- |
|
||||||
|
| `activity_type` | Yes | Activity type (e.g., running, cycling) |
|
||||||
|
| `setting` | Yes | Setting option (set as default, unset default, set this as default unset others) |
|
||||||
|
|
||||||
|
## Data updates
|
||||||
|
|
||||||
|
Data is polled from Garmin Connect every 5 minutes. Due to API rate limits, more frequent polling is not recommended.
|
||||||
|
|
||||||
|
## MFA Support
|
||||||
|
|
||||||
|
If your Garmin account has Multi-Factor Authentication (MFA) enabled, you will be prompted to enter your MFA code during setup.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- Not all sensors will have data depending on your Garmin devices and connected apps.
|
||||||
|
- API rate limits may cause temporary unavailability during high-traffic periods.
|
||||||
73
tests/test_const.py
Normal file
73
tests/test_const.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""Tests for Garmin Connect constants.
|
||||||
|
|
||||||
|
These tests use mocking to avoid requiring the full Home Assistant stack.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
# Mock homeassistant modules before importing const
|
||||||
|
sys.modules["homeassistant"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.config_entries"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.core"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.const"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.helpers"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.helpers.device_registry"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.helpers.update_coordinator"] = MagicMock()
|
||||||
|
sys.modules["homeassistant.exceptions"] = MagicMock()
|
||||||
|
sys.modules["garminconnect"] = MagicMock()
|
||||||
|
sys.modules["garth"] = MagicMock()
|
||||||
|
sys.modules["garth.exc"] = MagicMock()
|
||||||
|
|
||||||
|
from custom_components.garmin_connect.const import ( # noqa: E402
|
||||||
|
DAY_TO_NUMBER,
|
||||||
|
DEFAULT_UPDATE_INTERVAL,
|
||||||
|
DOMAIN,
|
||||||
|
GEAR_ICONS,
|
||||||
|
LEVEL_POINTS,
|
||||||
|
Gear,
|
||||||
|
ServiceSetting,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_domain():
|
||||||
|
"""Test domain constant."""
|
||||||
|
assert DOMAIN == "garmin_connect"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_update_interval():
|
||||||
|
"""Test default update interval."""
|
||||||
|
assert DEFAULT_UPDATE_INTERVAL.total_seconds() == 300 # 5 minutes
|
||||||
|
|
||||||
|
|
||||||
|
def test_day_to_number():
|
||||||
|
"""Test day to number mapping."""
|
||||||
|
assert DAY_TO_NUMBER["Mo"] == 1
|
||||||
|
assert DAY_TO_NUMBER["Su"] == 7
|
||||||
|
assert len(DAY_TO_NUMBER) >= 7 # Has aliases for days
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_points():
|
||||||
|
"""Test level points mapping."""
|
||||||
|
assert 1 in LEVEL_POINTS
|
||||||
|
assert LEVEL_POINTS[1] == 0
|
||||||
|
assert len(LEVEL_POINTS) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_gear_icons():
|
||||||
|
"""Test gear icons mapping."""
|
||||||
|
assert "Shoes" in GEAR_ICONS
|
||||||
|
assert "Bike" in GEAR_ICONS
|
||||||
|
assert "Other" in GEAR_ICONS
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_setting():
|
||||||
|
"""Test ServiceSetting class."""
|
||||||
|
assert ServiceSetting.DEFAULT == "set as default"
|
||||||
|
assert ServiceSetting.UNSET_DEFAULT == "unset default"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gear_class():
|
||||||
|
"""Test Gear class."""
|
||||||
|
assert Gear.UUID == "uuid"
|
||||||
|
assert Gear.USERPROFILE_ID == "userProfileId"
|
||||||
34
tests/test_coordinator.py
Normal file
34
tests/test_coordinator.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""Tests for Garmin Connect coordinator."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from custom_components.garmin_connect.coordinator import (
|
||||||
|
calculate_next_active_alarms,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_calculate_next_active_alarms_empty():
|
||||||
|
"""Test calculate_next_active_alarms with empty alarms."""
|
||||||
|
result = calculate_next_active_alarms([], "UTC")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_calculate_next_active_alarms_none():
|
||||||
|
"""Test calculate_next_active_alarms with None."""
|
||||||
|
result = calculate_next_active_alarms(None, "UTC")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_calculate_next_active_alarms_off():
|
||||||
|
"""Test calculate_next_active_alarms with alarm mode OFF."""
|
||||||
|
alarms = [{"alarmMode": "OFF", "alarmDays": ["MONDAY"], "alarmTime": 480}]
|
||||||
|
result = calculate_next_active_alarms(alarms, "UTC")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_calculate_next_active_alarms_once():
|
||||||
|
"""Test calculate_next_active_alarms with ONCE alarm."""
|
||||||
|
alarms = [{"alarmMode": "ON", "alarmDays": ["ONCE"], "alarmTime": 480}]
|
||||||
|
result = calculate_next_active_alarms(alarms, "UTC")
|
||||||
|
assert result is not None
|
||||||
|
assert len(result) == 1
|
||||||
47
tests/test_sensor_descriptions.py
Normal file
47
tests/test_sensor_descriptions.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Tests for Garmin Connect sensor descriptions."""
|
||||||
|
|
||||||
|
from custom_components.garmin_connect.sensor_descriptions import (
|
||||||
|
ACTIVITY_SENSORS,
|
||||||
|
ALL_SENSOR_DESCRIPTIONS,
|
||||||
|
CALORIES_SENSORS,
|
||||||
|
HEART_RATE_SENSORS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_sensor_descriptions_not_empty():
|
||||||
|
"""Test that ALL_SENSOR_DESCRIPTIONS is not empty."""
|
||||||
|
assert len(ALL_SENSOR_DESCRIPTIONS) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_sensors_have_key():
|
||||||
|
"""Test that all sensors have a key."""
|
||||||
|
for sensor in ALL_SENSOR_DESCRIPTIONS:
|
||||||
|
assert sensor.key is not None
|
||||||
|
assert len(sensor.key) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_sensors_have_translation_key():
|
||||||
|
"""Test that all sensors have a translation_key."""
|
||||||
|
for sensor in ALL_SENSOR_DESCRIPTIONS:
|
||||||
|
assert sensor.translation_key is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_activity_sensors_exist():
|
||||||
|
"""Test that activity sensors are defined."""
|
||||||
|
assert len(ACTIVITY_SENSORS) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_calories_sensors_exist():
|
||||||
|
"""Test that calories sensors are defined."""
|
||||||
|
assert len(CALORIES_SENSORS) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_heart_rate_sensors_exist():
|
||||||
|
"""Test that heart rate sensors are defined."""
|
||||||
|
assert len(HEART_RATE_SENSORS) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_sensor_count():
|
||||||
|
"""Test that we have the expected number of sensors."""
|
||||||
|
# Should have at least 90+ sensors
|
||||||
|
assert len(ALL_SENSOR_DESCRIPTIONS) >= 90
|
||||||
Reference in New Issue
Block a user