Add support for Flo "pucks" (#47074)

So far the Flo integration only supports shutoff valves. Add support for Flo leak detector pucks, which measure temperature and humidity in addition to providing leak alerts.
This commit is contained in:
Adam Ernst 2021-03-08 06:36:03 -06:00 committed by GitHub
parent d37fb7d88d
commit ad86eb4be3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 389 additions and 66 deletions

View file

@ -17,7 +17,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
config_entry.entry_id config_entry.entry_id
]["devices"] ]["devices"]
entities = [FloPendingAlertsBinarySensor(device) for device in devices] entities = []
for device in devices:
if device.device_type == "puck_oem":
# Flo "pucks" (leak detectors) *do* support pending alerts.
# However these pending alerts mix unrelated issues like
# low-battery alerts, humidity alerts, & temperature alerts
# in addition to the critical "water detected" alert.
#
# Since there are non-binary sensors for battery, humidity,
# and temperature, the binary sensor should only cover
# water detection. If this sensor trips, you really have
# a problem - vs. battery/temp/humidity which are warnings.
entities.append(FloWaterDetectedBinarySensor(device))
else:
entities.append(FloPendingAlertsBinarySensor(device))
async_add_entities(entities) async_add_entities(entities)
@ -48,3 +62,21 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity):
def device_class(self): def device_class(self):
"""Return the device class for the binary sensor.""" """Return the device class for the binary sensor."""
return DEVICE_CLASS_PROBLEM return DEVICE_CLASS_PROBLEM
class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity):
"""Binary sensor that reports if water is detected (for leak detectors)."""
def __init__(self, device):
"""Initialize the pending alerts binary sensor."""
super().__init__("water_detected", "Water Detected", device)
@property
def is_on(self):
"""Return true if the Flo device is detecting water."""
return self._device.water_detected
@property
def device_class(self):
"""Return the device class for the binary sensor."""
return DEVICE_CLASS_PROBLEM

View file

@ -58,7 +58,9 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
@property @property
def device_name(self) -> str: def device_name(self) -> str:
"""Return device name.""" """Return device name."""
return f"{self.manufacturer} {self.model}" return self._device_information.get(
"nickname", f"{self.manufacturer} {self.model}"
)
@property @property
def manufacturer(self) -> str: def manufacturer(self) -> str:
@ -120,6 +122,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
"""Return the current temperature in degrees F.""" """Return the current temperature in degrees F."""
return self._device_information["telemetry"]["current"]["tempF"] return self._device_information["telemetry"]["current"]["tempF"]
@property
def humidity(self) -> float:
"""Return the current humidity in percent (0-100)."""
return self._device_information["telemetry"]["current"]["humidity"]
@property @property
def consumption_today(self) -> float: def consumption_today(self) -> float:
"""Return the current consumption for today in gallons.""" """Return the current consumption for today in gallons."""
@ -159,6 +166,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
or self.pending_warning_alerts_count or self.pending_warning_alerts_count
) )
@property
def water_detected(self) -> bool:
"""Return whether water is detected, for leak detectors."""
return self._device_information["fwProperties"]["telemetry_water"]
@property @property
def last_known_valve_state(self) -> str: def last_known_valve_state(self) -> str:
"""Return the last known valve state for the device.""" """Return the last known valve state for the device."""
@ -169,6 +181,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
"""Return the target valve state for the device.""" """Return the target valve state for the device."""
return self._device_information["valve"]["target"] return self._device_information["valve"]["target"]
@property
def battery_level(self) -> float:
"""Return the battery level for battery-powered device, e.g. leak detectors."""
return self._device_information["battery"]["level"]
async def async_set_mode_home(self): async def async_set_mode_home(self):
"""Set the Flo location to home mode.""" """Set the Flo location to home mode."""
await self.api_client.location.set_mode_home(self._flo_location_id) await self.api_client.location.set_mode_home(self._flo_location_id)

View file

@ -3,13 +3,15 @@
from typing import List, Optional from typing import List, Optional
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
PRESSURE_PSI, PRESSURE_PSI,
TEMP_CELSIUS, TEMP_FAHRENHEIT,
VOLUME_GALLONS, VOLUME_GALLONS,
) )
from homeassistant.util.temperature import fahrenheit_to_celsius
from .const import DOMAIN as FLO_DOMAIN from .const import DOMAIN as FLO_DOMAIN
from .device import FloDeviceDataUpdateCoordinator from .device import FloDeviceDataUpdateCoordinator
@ -20,8 +22,11 @@ GAUGE_ICON = "mdi:gauge"
NAME_DAILY_USAGE = "Today's Water Usage" NAME_DAILY_USAGE = "Today's Water Usage"
NAME_CURRENT_SYSTEM_MODE = "Current System Mode" NAME_CURRENT_SYSTEM_MODE = "Current System Mode"
NAME_FLOW_RATE = "Water Flow Rate" NAME_FLOW_RATE = "Water Flow Rate"
NAME_TEMPERATURE = "Water Temperature" NAME_WATER_TEMPERATURE = "Water Temperature"
NAME_AIR_TEMPERATURE = "Temperature"
NAME_WATER_PRESSURE = "Water Pressure" NAME_WATER_PRESSURE = "Water Pressure"
NAME_HUMIDITY = "Humidity"
NAME_BATTERY = "Battery"
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
@ -30,11 +35,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config_entry.entry_id config_entry.entry_id
]["devices"] ]["devices"]
entities = [] entities = []
entities.extend([FloDailyUsageSensor(device) for device in devices]) for device in devices:
entities.extend([FloSystemModeSensor(device) for device in devices]) if device.device_type == "puck_oem":
entities.extend([FloCurrentFlowRateSensor(device) for device in devices]) entities.extend(
entities.extend([FloTemperatureSensor(device) for device in devices]) [
entities.extend([FloPressureSensor(device) for device in devices]) FloTemperatureSensor(NAME_AIR_TEMPERATURE, device),
FloHumiditySensor(device),
FloBatterySensor(device),
]
)
else:
entities.extend(
[
FloDailyUsageSensor(device),
FloSystemModeSensor(device),
FloCurrentFlowRateSensor(device),
FloTemperatureSensor(NAME_WATER_TEMPERATURE, device),
FloPressureSensor(device),
]
)
async_add_entities(entities) async_add_entities(entities)
@ -109,9 +128,9 @@ class FloCurrentFlowRateSensor(FloEntity):
class FloTemperatureSensor(FloEntity): class FloTemperatureSensor(FloEntity):
"""Monitors the temperature.""" """Monitors the temperature."""
def __init__(self, device): def __init__(self, name, device):
"""Initialize the temperature sensor.""" """Initialize the temperature sensor."""
super().__init__("temperature", NAME_TEMPERATURE, device) super().__init__("temperature", name, device)
self._state: float = None self._state: float = None
@property @property
@ -119,12 +138,12 @@ class FloTemperatureSensor(FloEntity):
"""Return the current temperature.""" """Return the current temperature."""
if self._device.temperature is None: if self._device.temperature is None:
return None return None
return round(fahrenheit_to_celsius(self._device.temperature), 1) return round(self._device.temperature, 1)
@property @property
def unit_of_measurement(self) -> str: def unit_of_measurement(self) -> str:
"""Return gallons as the unit measurement for water.""" """Return fahrenheit as the unit measurement for temperature."""
return TEMP_CELSIUS return TEMP_FAHRENHEIT
@property @property
def device_class(self) -> Optional[str]: def device_class(self) -> Optional[str]:
@ -132,6 +151,32 @@ class FloTemperatureSensor(FloEntity):
return DEVICE_CLASS_TEMPERATURE return DEVICE_CLASS_TEMPERATURE
class FloHumiditySensor(FloEntity):
"""Monitors the humidity."""
def __init__(self, device):
"""Initialize the humidity sensor."""
super().__init__("humidity", NAME_HUMIDITY, device)
self._state: float = None
@property
def state(self) -> Optional[float]:
"""Return the current humidity."""
if self._device.humidity is None:
return None
return round(self._device.humidity, 1)
@property
def unit_of_measurement(self) -> str:
"""Return percent as the unit measurement for humidity."""
return PERCENTAGE
@property
def device_class(self) -> Optional[str]:
"""Return the device class for this sensor."""
return DEVICE_CLASS_HUMIDITY
class FloPressureSensor(FloEntity): class FloPressureSensor(FloEntity):
"""Monitors the water pressure.""" """Monitors the water pressure."""
@ -156,3 +201,27 @@ class FloPressureSensor(FloEntity):
def device_class(self) -> Optional[str]: def device_class(self) -> Optional[str]:
"""Return the device class for this sensor.""" """Return the device class for this sensor."""
return DEVICE_CLASS_PRESSURE return DEVICE_CLASS_PRESSURE
class FloBatterySensor(FloEntity):
"""Monitors the battery level for battery-powered leak detectors."""
def __init__(self, device):
"""Initialize the battery sensor."""
super().__init__("battery", NAME_BATTERY, device)
self._state: float = None
@property
def state(self) -> Optional[float]:
"""Return the current battery level."""
return self._device.battery_level
@property
def unit_of_measurement(self) -> str:
"""Return percentage as the unit measurement for battery."""
return PERCENTAGE
@property
def device_class(self) -> Optional[str]:
"""Return the device class for this sensor."""
return DEVICE_CLASS_BATTERY

View file

@ -26,7 +26,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
config_entry.entry_id config_entry.entry_id
]["devices"] ]["devices"]
async_add_entities([FloSwitch(device) for device in devices]) entities = []
for device in devices:
if device.device_type != "puck_oem":
entities.append(FloSwitch(device))
async_add_entities(entities)
platform = entity_platform.current_platform.get() platform = entity_platform.current_platform.get()

View file

@ -43,13 +43,19 @@ def aioclient_mock_fixture(aioclient_mock):
headers={"Content-Type": CONTENT_TYPE_JSON}, headers={"Content-Type": CONTENT_TYPE_JSON},
status=200, status=200,
) )
# Mocks the device for flo. # Mocks the devices for flo.
aioclient_mock.get( aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/devices/98765", "https://api-gw.meetflo.com/api/v2/devices/98765",
text=load_fixture("flo/device_info_response.json"), text=load_fixture("flo/device_info_response.json"),
status=200, status=200,
headers={"Content-Type": CONTENT_TYPE_JSON}, headers={"Content-Type": CONTENT_TYPE_JSON},
) )
aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/devices/32839",
text=load_fixture("flo/device_info_response_detector.json"),
status=200,
headers={"Content-Type": CONTENT_TYPE_JSON},
)
# Mocks the water consumption for flo. # Mocks the water consumption for flo.
aioclient_mock.get( aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/water/consumption", "https://api-gw.meetflo.com/api/v2/water/consumption",

View file

@ -4,6 +4,7 @@ from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_USERNAME, CONF_USERNAME,
STATE_OFF,
STATE_ON, STATE_ON,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -19,12 +20,14 @@ async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
# we should have 6 entities for the device valve_state = hass.states.get("binary_sensor.pending_system_alerts")
state = hass.states.get("binary_sensor.pending_system_alerts") assert valve_state.state == STATE_ON
assert state.state == STATE_ON assert valve_state.attributes.get("info") == 0
assert state.attributes.get("info") == 0 assert valve_state.attributes.get("warning") == 2
assert state.attributes.get("warning") == 2 assert valve_state.attributes.get("critical") == 0
assert state.attributes.get("critical") == 0 assert valve_state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts"
detector_state = hass.states.get("binary_sensor.water_detected")
assert detector_state.state == STATE_OFF

View file

@ -13,46 +13,64 @@ from tests.common import async_fire_time_changed
async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock): async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock):
"""Test Flo by Moen device.""" """Test Flo by Moen devices."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await async_setup_component( assert await async_setup_component(
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
device: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][ valve: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][
config_entry.entry_id config_entry.entry_id
]["devices"][0] ]["devices"][0]
assert device.api_client is not None assert valve.api_client is not None
assert device.available assert valve.available
assert device.consumption_today == 3.674 assert valve.consumption_today == 3.674
assert device.current_flow_rate == 0 assert valve.current_flow_rate == 0
assert device.current_psi == 54.20000076293945 assert valve.current_psi == 54.20000076293945
assert device.current_system_mode == "home" assert valve.current_system_mode == "home"
assert device.target_system_mode == "home" assert valve.target_system_mode == "home"
assert device.firmware_version == "6.1.1" assert valve.firmware_version == "6.1.1"
assert device.device_type == "flo_device_v2" assert valve.device_type == "flo_device_v2"
assert device.id == "98765" assert valve.id == "98765"
assert device.last_heard_from_time == "2020-07-24T12:45:00Z" assert valve.last_heard_from_time == "2020-07-24T12:45:00Z"
assert device.location_id == "mmnnoopp" assert valve.location_id == "mmnnoopp"
assert device.hass is not None assert valve.hass is not None
assert device.temperature == 70 assert valve.temperature == 70
assert device.mac_address == "111111111111" assert valve.mac_address == "111111111111"
assert device.model == "flo_device_075_v2" assert valve.model == "flo_device_075_v2"
assert device.manufacturer == "Flo by Moen" assert valve.manufacturer == "Flo by Moen"
assert device.device_name == "Flo by Moen flo_device_075_v2" assert valve.device_name == "Smart Water Shutoff"
assert device.rssi == -47 assert valve.rssi == -47
assert device.pending_info_alerts_count == 0 assert valve.pending_info_alerts_count == 0
assert device.pending_critical_alerts_count == 0 assert valve.pending_critical_alerts_count == 0
assert device.pending_warning_alerts_count == 2 assert valve.pending_warning_alerts_count == 2
assert device.has_alerts is True assert valve.has_alerts is True
assert device.last_known_valve_state == "open" assert valve.last_known_valve_state == "open"
assert device.target_valve_state == "open" assert valve.target_valve_state == "open"
detector: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][
config_entry.entry_id
]["devices"][1]
assert detector.api_client is not None
assert detector.available
assert detector.device_type == "puck_oem"
assert detector.id == "32839"
assert detector.last_heard_from_time == "2021-03-07T14:05:00Z"
assert detector.location_id == "mmnnoopp"
assert detector.hass is not None
assert detector.temperature == 61
assert detector.humidity == 43
assert detector.water_detected is False
assert detector.mac_address == "1a2b3c4d5e6f"
assert detector.model == "puck_v1"
assert detector.manufacturer == "Flo by Moen"
assert detector.device_name == "Kitchen Sink"
call_count = aioclient_mock.call_count call_count = aioclient_mock.call_count
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=90)) async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=90))
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == call_count + 2 assert aioclient_mock.call_count == call_count + 4

View file

@ -13,6 +13,6 @@ async def test_setup_entry(hass, config_entry, aioclient_mock_fixture):
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
assert await hass.config_entries.async_unload(config_entry.entry_id) assert await hass.config_entries.async_unload(config_entry.entry_id)

View file

@ -14,14 +14,19 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
# we should have 5 entities for the device # we should have 5 entities for the valve
assert hass.states.get("sensor.current_system_mode").state == "home" assert hass.states.get("sensor.current_system_mode").state == "home"
assert hass.states.get("sensor.today_s_water_usage").state == "3.7" assert hass.states.get("sensor.today_s_water_usage").state == "3.7"
assert hass.states.get("sensor.water_flow_rate").state == "0" assert hass.states.get("sensor.water_flow_rate").state == "0"
assert hass.states.get("sensor.water_pressure").state == "54.2" assert hass.states.get("sensor.water_pressure").state == "54.2"
assert hass.states.get("sensor.water_temperature").state == "21.1" assert hass.states.get("sensor.water_temperature").state == "21"
# and 3 entities for the detector
assert hass.states.get("sensor.temperature").state == "16"
assert hass.states.get("sensor.humidity").state == "43"
assert hass.states.get("sensor.battery").state == "100"
async def test_manual_update_entity( async def test_manual_update_entity(
@ -34,7 +39,7 @@ async def test_manual_update_entity(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})

View file

@ -25,8 +25,8 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 6
await hass.services.async_call( await hass.services.async_call(
FLO_DOMAIN, FLO_DOMAIN,
@ -35,7 +35,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 5 assert aioclient_mock.call_count == 7
await hass.services.async_call( await hass.services.async_call(
FLO_DOMAIN, FLO_DOMAIN,
@ -44,7 +44,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 6 assert aioclient_mock.call_count == 8
await hass.services.async_call( await hass.services.async_call(
FLO_DOMAIN, FLO_DOMAIN,
@ -53,7 +53,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 7 assert aioclient_mock.call_count == 9
await hass.services.async_call( await hass.services.async_call(
FLO_DOMAIN, FLO_DOMAIN,
@ -66,4 +66,4 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 8 assert aioclient_mock.call_count == 10

View file

@ -15,7 +15,7 @@ async def test_valve_switches(hass, config_entry, aioclient_mock_fixture):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
entity_id = "switch.shutoff_valve" entity_id = "switch.shutoff_valve"
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON

View file

@ -0,0 +1,161 @@
{
"actionRules": [],
"battery": {
"level": 100,
"updated": "2021-03-01T12:05:00Z"
},
"connectivity": {
"ssid": "SOMESSID"
},
"deviceModel": "puck_v1",
"deviceType": "puck_oem",
"fwProperties": {
"alert_battery_active": false,
"alert_humidity_high_active": false,
"alert_humidity_high_count": 0,
"alert_humidity_low_active": false,
"alert_humidity_low_count": 1,
"alert_state": "inactive",
"alert_temperature_high_active": false,
"alert_temperature_high_count": 0,
"alert_temperature_low_active": false,
"alert_temperature_low_count": 0,
"alert_water_active": false,
"alert_water_count": 0,
"ap_mode_count": 1,
"beep_pattern": "off",
"button_click_count": 1,
"date": "2021-03-07T14:00:05.054Z",
"deep_sleep_count": 8229,
"device_boot_count": 25,
"device_boot_reason": "wakeup_timer",
"device_count": 8230,
"device_failed_count": 36,
"device_id": "1a2b3c4d5e6f",
"device_time_total": 405336,
"device_time_up": 1502,
"device_uuid": "32839",
"device_wakeup_count": 8254,
"flosense_shut_off_level": 2,
"fw_name": "1.1.15",
"fw_version": 10115,
"led_pattern": "led_blue_solid",
"limit_ota_battery_min": 30,
"pairing_state": "configured",
"reason": "heartbeat",
"serial_number": "111111111112",
"telemetry_battery_percent": 100,
"telemetry_battery_voltage": 2.9896278381347656,
"telemetry_count": 8224,
"telemetry_failed_count": 27,
"telemetry_humidity": 43.21965408325195,
"telemetry_rssi": 100,
"telemetry_temperature": 61.43144607543945,
"telemetry_water": false,
"timer_alarm_active": 10,
"timer_heartbeat_battery_low": 3600,
"timer_heartbeat_battery_ok": 1740,
"timer_heartbeat_last": 1740,
"timer_heartbeat_not_configured": 10,
"timer_heartbeat_retry_attempts": 3,
"timer_heartbeat_retry_delay": 600,
"timer_water_debounce": 2000,
"timer_wifi_ap_timeout": 600000,
"wifi_ap_ssid": "FloDetector-a123",
"wifi_sta_enc": "wpa2-psk",
"wifi_sta_failed_count": 21,
"wifi_sta_mac": "50:01:01:01:01:44",
"wifi_sta_ssid": "SOMESSID"
},
"fwVersion": "1.1.15",
"hardwareThresholds": {
"battery": {
"maxValue": 100,
"minValue": 0,
"okMax": 100,
"okMin": 20
},
"batteryEnabled": true,
"humidity": {
"maxValue": 100,
"minValue": 0,
"okMax": 85,
"okMin": 15
},
"humidityEnabled": true,
"tempC": {
"maxValue": 60,
"minValue": -17.77777777777778,
"okMax": 37.77777777777778,
"okMin": 0
},
"tempEnabled": true,
"tempF": {
"maxValue": 140,
"minValue": 0,
"okMax": 100,
"okMin": 32
}
},
"id": "32839",
"installStatus": {
"isInstalled": false
},
"isConnected": true,
"isPaired": true,
"lastHeardFromTime": "2021-03-07T14:05:00Z",
"location": {
"id": "mmnnoopp"
},
"macAddress": "1a2b3c4d5e6f",
"nickname": "Kitchen Sink",
"notifications": {
"pending": {
"alarmCount": [],
"critical": {
"count": 0,
"devices": {
"absolute": 0,
"count": 0
}
},
"criticalCount": 0,
"info": {
"count": 0,
"devices": {
"absolute": 0,
"count": 0
}
},
"infoCount": 0,
"warning": {
"count": 0,
"devices": {
"absolute": 0,
"count": 0
}
},
"warningCount": 0
}
},
"puckConfig": {
"configuredAt": "2020-09-01T18:15:12.216Z",
"isConfigured": true
},
"serialNumber": "111111111112",
"shutoff": {
"scheduledAt": "1970-01-01T00:00:00.000Z"
},
"systemMode": {
"isLocked": false,
"shouldInherit": true
},
"telemetry": {
"current": {
"humidity": 43,
"tempF": 61,
"updated": "2021-03-07T14:05:00Z"
}
},
"valve": {}
}

View file

@ -9,6 +9,10 @@
{ {
"id": "98765", "id": "98765",
"macAddress": "123456abcdef" "macAddress": "123456abcdef"
},
{
"id": "32839",
"macAddress": "1a2b3c4d5e6f"
} }
], ],
"userRoles": [ "userRoles": [

View file

@ -19,6 +19,10 @@
{ {
"id": "98765", "id": "98765",
"macAddress": "606405c11e10" "macAddress": "606405c11e10"
},
{
"id": "32839",
"macAddress": "1a2b3c4d5e6f"
} }
], ],
"userRoles": [ "userRoles": [