Refactoring, added tests

###Fix
# In light of submitting this as a pull request to the core of HA there have been some refactoring changes to comply with HA's style requirements
#Sensor names now moved to strings file to allow for translations
#Coordinator now properly handles multiple devices
#Spelling of signal strength sensor corrected

###Feature
#Tests added
This commit is contained in:
RyanClark123
2023-05-30 13:30:59 +01:00
parent c6f37493ca
commit 26f81c14bf
21 changed files with 4202 additions and 128 deletions
+31 -26
View File
@@ -1,6 +1,7 @@
"""The Owlet Smart Sock integration."""
from __future__ import annotations
import asyncio
import logging
from pyowletapi.api import OwletAPI
@@ -13,11 +14,7 @@ from pyowletapi.exceptions import (
)
from pyowletapi.sock import Sock
from homeassistant.config_entries import (
ConfigEntry,
ConfigEntryAuthFailed,
ConfigEntryNotReady,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_TOKEN,
CONF_REGION,
@@ -26,8 +23,8 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntry
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, SUPPORTED_VERSIONS
from .coordinator import OwletCoordinator
@@ -50,23 +47,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
try:
token = await owlet_api.authenticate()
if token:
if token := await owlet_api.authenticate():
hass.config_entries.async_update_entry(entry, data={**entry.data, **token})
devices = await owlet_api.get_devices(SUPPORTED_VERSIONS)
if devices["tokens"]:
hass.config_entries.async_update_entry(
entry, data={**entry.data, **devices["tokens"]}
)
socks = {
device["device"]["dsn"]: Sock(owlet_api, device["device"])
for device in devices["response"]
}
except (OwletAuthenticationError, OwletEmailError, OwletPasswordError) as err:
_LOGGER.error("Credentials no longer valid, please setup owlet again")
raise ConfigEntryAuthFailed(
@@ -78,14 +63,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Error connecting to {entry.data[CONF_USERNAME]}"
) from err
coordinators = [
OwletCoordinator(hass, sock, entry.options.get(CONF_SCAN_INTERVAL))
for sock in socks.values()
]
except OwletDevicesError:
_LOGGER.error("No owlet devices found to set up")
return False
for coordinator in coordinators:
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
if devices["tokens"]:
hass.config_entries.async_update_entry(
entry, data={**entry.data, **devices["tokens"]}
)
socks = {
device["device"]["dsn"]: Sock(owlet_api, device["device"])
for device in devices["response"]
}
scan_interval = entry.options.get(CONF_SCAN_INTERVAL)
coordinators = {
serial: OwletCoordinator(hass, sock, scan_interval)
for (serial, sock) in socks.items()
}
await asyncio.gather(
*(
coordinator.async_config_entry_first_refresh()
for coordinator in list(coordinators.values())
)
)
hass.data[DOMAIN][entry.entry_id] = coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+10 -10
View File
@@ -34,61 +34,61 @@ class OwletBinarySensorEntityDescription(
SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = (
OwletBinarySensorEntityDescription(
key="charging",
name="Charging",
translation_key="charging",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
element="charging",
),
OwletBinarySensorEntityDescription(
key="highhr",
name="High heart rate alert",
translation_key="high_hr_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="high_heart_rate_alert",
),
OwletBinarySensorEntityDescription(
key="lowhr",
name="Low Heart Rate Alert",
translation_key="low_hr_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="low_heart_rate_alert",
),
OwletBinarySensorEntityDescription(
key="higho2",
name="High oxygen alert",
translation_key="high_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="high_oxygen_alert",
),
OwletBinarySensorEntityDescription(
key="lowo2",
name="Low oxygen alert",
translation_key="low_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="low_oxygen_alert",
),
OwletBinarySensorEntityDescription(
key="lowbattery",
name="Low Battery alert",
translation_key="low_batt_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="low_battery_alert",
),
OwletBinarySensorEntityDescription(
key="lostpower",
name="Lost power alert",
translation_key="lost_pwr_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="lost_power_alert",
),
OwletBinarySensorEntityDescription(
key="sockdisconnected",
name="Sock disconnected alert",
translation_key="sock_discon_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="sock_disconnected",
),
OwletBinarySensorEntityDescription(
key="sock_off",
name="Sock off",
translation_key="sock_off",
device_class=BinarySensorDeviceClass.POWER,
element="sock_off",
),
OwletBinarySensorEntityDescription(
key="awake",
name="Awake",
translation_key="awake",
element="sleep_state",
icon="mdi:sleep",
),
+32 -25
View File
@@ -1,11 +1,17 @@
"""Config flow for Owlet Smart Sock integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from pyowletapi.api import OwletAPI
from pyowletapi.exceptions import OwletDevicesError, OwletEmailError, OwletPasswordError
from pyowletapi.exceptions import (
OwletCredentialsError,
OwletDevicesError,
OwletEmailError,
OwletPasswordError,
)
from pyowletapi.sock import Sock
import voluptuous as vol
@@ -41,6 +47,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self) -> None:
"""Initialise config flow."""
self._entry: ConfigEntry
self._region: str
self._username: str
@@ -70,29 +77,31 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
token = await owlet_api.authenticate()
try:
await owlet_api.validate_authentication()
return self.async_create_entry(
title=self._username,
data={
CONF_REGION: self._region,
CONF_USERNAME: self._username,
CONF_API_TOKEN: token[CONF_API_TOKEN],
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH],
},
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
)
except OwletDevicesError:
errors["base"] = "no_devices"
await owlet_api.validate_authentication()
except OwletDevicesError:
errors["base"] = "no_devices"
except OwletEmailError:
errors["base"] = "invalid_email"
except OwletPasswordError:
errors["base"] = "invalid_password"
except OwletCredentialsError:
errors["base"] = "invalid_credentials"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=self._username,
data={
CONF_REGION: self._region,
CONF_USERNAME: self._username,
CONF_API_TOKEN: token[CONF_API_TOKEN],
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH],
},
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -104,10 +113,8 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
async def async_step_reauth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reauth"""
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
"""Handle reauth."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
@@ -116,7 +123,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Dialog that informs the user that reauth is required"""
"""Dialog that informs the user that reauth is required."""
assert self.reauth_entry is not None
errors: dict[str, str] = {}
@@ -152,16 +159,16 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a options flow for owlet"""
"""Handle a options flow for owlet."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialise options flow"""
"""Initialise options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle options flow"""
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@@ -178,4 +185,4 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indiciate there is invalud auth"""
"""Error to indicate there is invalid auth."""
+1 -1
View File
@@ -8,4 +8,4 @@ CONF_OWLET_REFRESH = "refresh"
SUPPORTED_VERSIONS = [3]
POLLING_INTERVAL = 5
MANUFACTURER = "Owlet Baby Care"
SLEEP_STATES = {0: "Unknown", 1: "Awake", 8: "Light Sleep", 15: "Deep Sleep"}
SLEEP_STATES = {0: "unknown", 1: "awake", 8: "light_sleep", 15: "deep_sleep"}
+9 -14
View File
@@ -11,14 +11,12 @@ from pyowletapi.exceptions import (
)
from pyowletapi.sock import Sock
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
ConfigEntryAuthFailed,
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, MANUFACTURER
@@ -28,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
class OwletCoordinator(DataUpdateCoordinator):
"""Coordinator is responsible for querying the device at a specified route."""
def __init__(self, hass: HomeAssistant, sock: Sock, interval: int) -> None:
def __init__(self, hass: HomeAssistant, sock: Sock, interval) -> None:
"""Initialise a custom coordinator."""
super().__init__(
hass,
@@ -37,18 +35,15 @@ class OwletCoordinator(DataUpdateCoordinator):
update_interval=timedelta(seconds=interval),
)
assert self.config_entry is not None
self._device_unique_id = sock.serial
self._model = sock.model
self._sw_version = sock.sw_version
self._hw_version = sock.version
self.config_entry: ConfigEntry
self.sock = sock
self.device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_unique_id)},
identifiers={(DOMAIN, sock.serial)},
name="Owlet Baby Care Sock",
manufacturer=MANUFACTURER,
model=self._model,
sw_version=self._sw_version,
hw_version=self._hw_version,
model=sock.model,
sw_version=sock.sw_version,
hw_version=sock.version,
)
async def _async_update_data(self) -> None:
+2 -3
View File
@@ -3,9 +3,8 @@
"name": "Owlet Smart Sock",
"codeowners": ["@ryanbdclark"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/owlet",
"homekit": {},
"iot_class": "cloud_polling",
"requirements": ["pyowletapi==2023.5.28"]
"requirements": ["pyowletapi==2023.5.30"],
"version":"2023.5.6"
}
+28 -19
View File
@@ -27,7 +27,7 @@ from .entity import OwletBaseEntity
@dataclass
class OwletSensorEntityDescriptionMixin:
"""Owlet sensor description mix in"""
"""Owlet sensor description mix in."""
element: str
@@ -42,7 +42,7 @@ class OwletSensorEntityDescription(
SENSORS: tuple[OwletSensorEntityDescription, ...] = (
OwletSensorEntityDescription(
key="batterypercentage",
name="Battery",
translation_key="batterypercent",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
@@ -50,7 +50,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
),
OwletSensorEntityDescription(
key="oxygensaturation",
name="O2 Saturation",
translation_key="o2saturation",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
element="oxygen_saturation",
@@ -58,7 +58,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
),
OwletSensorEntityDescription(
key="oxygensaturation10a",
name="O2 Saturation 10 Minute Average",
translation_key="o2saturation10a",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
element="oxygen_10_av",
@@ -66,7 +66,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
),
OwletSensorEntityDescription(
key="heartrate",
name="Heart rate",
translation_key="heartrate",
native_unit_of_measurement="bpm",
state_class=SensorStateClass.MEASUREMENT,
element="heart_rate",
@@ -74,7 +74,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
),
OwletSensorEntityDescription(
key="batteryminutes",
name="Battery Remaining",
translation_key="batterymin",
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
@@ -82,7 +82,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
),
OwletSensorEntityDescription(
key="signalstrength",
name="Signal Strength",
translation_key="signalstrength",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
@@ -90,7 +90,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
),
OwletSensorEntityDescription(
key="skintemp",
name="Skin Temperature",
translation_key="skintemp",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -106,12 +106,19 @@ async def async_setup_entry(
) -> None:
"""Set up the owlet sensors from config entry."""
coordinator: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinators: list[OwletCoordinator] = list(
hass.data[DOMAIN][config_entry.entry_id].values()
)
entities = [OwletSensor(coordinator, sensor) for sensor in SENSORS]
entities.append(OwletSleepStateSensor(coordinator))
async_add_entities(
OwletSensor(coordinator, sensor)
for coordinator in coordinators
for sensor in SENSORS
)
async_add_entities(entities)
async_add_entities(
OwletSleepStateSensor(coordinator) for coordinator in coordinators
)
class OwletSensor(OwletBaseEntity, SensorEntity):
@@ -124,12 +131,14 @@ class OwletSensor(OwletBaseEntity, SensorEntity):
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = sensor_description
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.name}"
self.entity_description: OwletSensorEntityDescription = sensor_description
self._attr_unique_id = (
f"{self.sock.serial}-{self.entity_description.translation_key}"
)
@property
def native_value(self) -> StateType:
"""Return sensor value"""
"""Return sensor value."""
if (
self.entity_description.element
@@ -158,20 +167,20 @@ class OwletSleepStateSensor(OwletBaseEntity, SensorEntity):
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{self.sock.serial}-Sleep State"
self._attr_unique_id = f"{self.sock.serial}-sleepstate"
self._attr_icon = "mdi:sleep"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_translation_key = "sleepstate"
self._attr_name = "Sleep State"
@property
def native_value(self) -> str:
"""Return sensor value"""
"""Return sensor value."""
if self.sock.properties["charging"]:
return None
return "unknown"
return SLEEP_STATES[self.sock.properties["sleep_state"]]
@property
def options(self) -> list[str]:
"""Set options for sleep state."""
return list(SLEEP_STATES.values())
+78 -10
View File
@@ -1,17 +1,17 @@
{
"config": {
"step": {
"user":{
"user": {
"title": "Enter login details",
"data":{
"data": {
"region": "Region",
"username": "Email",
"password": "Password"
}
},
"reauth_confirm":{
"reauth_confirm": {
"title": "Reauthentiaction required for Owlet",
"data":{
"data": {
"password": "Password"
}
}
@@ -20,20 +20,88 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_email": "Entered email address is incorrect",
"invalid_password": "Entered password is incorrect",
"unknown": "[%key:common::config_flow::error::unknown%]"
"invalid_credentials": "Entered credentials are incorrect",
"unknown": "Unknown error occured"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "Device already configured",
"reauth_successful": "Reauthentication successful"
}
},
"options": {
"step": {
"init":{
"title":"Configure options for Owlet",
"data":{
"init": {
"title": "Configure options for Owlet",
"data": {
"pollinterval": "Polling interval in seconds, min 10"
}
}
}
},
"entity": {
"binary_sensor":{
"charging": {
"name": "Charging"
},
"high_hr_alrt":{
"name": "High Heart Rate Alert"
},
"low_hr_alrt":{
"name":"Low Heart Rate Alert"
},
"high_ox_alrt":{
"name":"High Oxygen Alert"
},
"low_ox_alrt":{
"name":"Low Oxygen Alert"
},
"low_batt_alrt":{
"name": "Low Battery Alert"
},
"lost_pwr_alrt":{
"name": "Lost Power Alert"
},
"sock_discon_alrt":{
"name": "Sock Diconnected Alert"
},
"sock_off":{
"name":"sock_off"
},
"awake":{
"name":"Awake"
}
},
"sensor": {
"batterypercent": {
"name": "Battery Percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
},
"signalstrength": {
"name": "Signal Strength"
},
"skintemp": {
"name": "Skin Temperature"
},
"sleepstate": {
"name": "Sleep State",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light Sleep",
"deep_sleep": "Deep Sleep"
}
}
}
}
}
}
+78 -10
View File
@@ -1,17 +1,17 @@
{
"config": {
"step": {
"user":{
"user": {
"title": "Enter login details",
"data":{
"data": {
"region": "Region",
"username": "Email",
"password": "Password"
}
},
"reauth_confirm":{
"reauth_confirm": {
"title": "Reauthentiaction required for Owlet",
"data":{
"data": {
"password": "Password"
}
}
@@ -20,20 +20,88 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_email": "Entered email address is incorrect",
"invalid_password": "Entered password is incorrect",
"unknown": "[%key:common::config_flow::error::unknown%]"
"invalid_credentials": "Entered credentials are incorrect",
"unknown": "Unknown error occured"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "Device already configured",
"reauth_successful": "Reauthentication successful"
}
},
"options": {
"step": {
"init":{
"title":"Configure options for Owlet",
"data":{
"init": {
"title": "Configure options for Owlet",
"data": {
"pollinterval": "Polling interval in seconds, min 10"
}
}
}
},
"entity": {
"binary_sensor":{
"charging": {
"name": "Charging"
},
"high_hr_alrt":{
"name": "High Heart Rate Alert"
},
"low_hr_alrt":{
"name":"Low Heart Rate Alert"
},
"high_ox_alrt":{
"name":"High Oxygen Alert"
},
"low_ox_alrt":{
"name":"Low Oxygen Alert"
},
"low_batt_alrt":{
"name": "Low Battery Alert"
},
"lost_pwr_alrt":{
"name": "Lost Power Alert"
},
"sock_discon_alrt":{
"name": "Sock Diconnected Alert"
},
"sock_off":{
"name":"sock_off"
},
"awake":{
"name":"Awake"
}
},
"sensor": {
"batterypercent": {
"name": "Battery Percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
},
"signalstrength": {
"name": "Signal Strength"
},
"skintemp": {
"name": "Skin Temperature"
},
"sleepstate": {
"name": "Sleep State",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light Sleep",
"deep_sleep": "Deep Sleep"
}
}
}
}
}
}
+78 -10
View File
@@ -1,17 +1,17 @@
{
"config": {
"step": {
"user":{
"user": {
"title": "Enter login details",
"data":{
"data": {
"region": "Region",
"username": "Email",
"password": "Password"
}
},
"reauth_confirm":{
"reauth_confirm": {
"title": "Reauthentiaction required for Owlet",
"data":{
"data": {
"password": "Password"
}
}
@@ -20,20 +20,88 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_email": "Entered email address is incorrect",
"invalid_password": "Entered password is incorrect",
"unknown": "[%key:common::config_flow::error::unknown%]"
"invalid_credentials": "Entered credentials are incorrect",
"unknown": "Unknown error occured"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "Device already configured",
"reauth_successful": "Reauthentication successful"
}
},
"options": {
"step": {
"init":{
"title":"Configure options for Owlet",
"data":{
"init": {
"title": "Configure options for Owlet",
"data": {
"pollinterval": "Polling interval in seconds, min 10"
}
}
}
},
"entity": {
"binary_sensor":{
"charging": {
"name": "Charging"
},
"high_hr_alrt":{
"name": "High Heart Rate Alert"
},
"low_hr_alrt":{
"name":"Low Heart Rate Alert"
},
"high_ox_alrt":{
"name":"High Oxygen Alert"
},
"low_ox_alrt":{
"name":"Low Oxygen Alert"
},
"low_batt_alrt":{
"name": "Low Battery Alert"
},
"lost_pwr_alrt":{
"name": "Lost Power Alert"
},
"sock_discon_alrt":{
"name": "Sock Diconnected Alert"
},
"sock_off":{
"name":"Sock Off"
},
"awake":{
"name":"Awake"
}
},
"sensor": {
"batterypercent": {
"name": "Battery Percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
},
"signalstrength": {
"name": "Signal Strength"
},
"skintemp": {
"name": "Skin Temperature"
},
"sleepstate": {
"name": "Sleep State",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light Sleep",
"deep_sleep": "Deep Sleep"
}
}
}
}
}
}