Add support for Flo by Moen water shutoff devices (#38171)
This commit is contained in:
parent
07de9deab6
commit
f1fd8aa51f
27 changed files with 1650 additions and 1 deletions
|
@ -134,6 +134,7 @@ homeassistant/components/filter/* @dgomes
|
||||||
homeassistant/components/firmata/* @DaAwesomeP
|
homeassistant/components/firmata/* @DaAwesomeP
|
||||||
homeassistant/components/fixer/* @fabaff
|
homeassistant/components/fixer/* @fabaff
|
||||||
homeassistant/components/flick_electric/* @ZephireNZ
|
homeassistant/components/flick_electric/* @ZephireNZ
|
||||||
|
homeassistant/components/flo/* @dmulcahey
|
||||||
homeassistant/components/flock/* @fabaff
|
homeassistant/components/flock/* @fabaff
|
||||||
homeassistant/components/flume/* @ChrisMandich @bdraco
|
homeassistant/components/flume/* @ChrisMandich @bdraco
|
||||||
homeassistant/components/flunearyou/* @bachya
|
homeassistant/components/flunearyou/* @bachya
|
||||||
|
|
76
homeassistant/components/flo/__init__.py
Normal file
76
homeassistant/components/flo/__init__.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""The flo integration."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioflo import async_get_api
|
||||||
|
from aioflo.errors import RequestError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .device import FloDeviceDataUpdateCoordinator
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = ["sensor"]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the flo component."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up flo from a config entry."""
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {}
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
try:
|
||||||
|
hass.data[DOMAIN][entry.entry_id]["client"] = client = await async_get_api(
|
||||||
|
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session
|
||||||
|
)
|
||||||
|
except RequestError:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
user_info = await client.user.get_info(include_location_info=True)
|
||||||
|
|
||||||
|
_LOGGER.debug("Flo user information with locations: %s", user_info)
|
||||||
|
|
||||||
|
hass.data[DOMAIN]["devices"] = devices = [
|
||||||
|
FloDeviceDataUpdateCoordinator(hass, client, location["id"], device["id"])
|
||||||
|
for location in user_info["locations"]
|
||||||
|
for device in location["devices"]
|
||||||
|
]
|
||||||
|
|
||||||
|
tasks = [device.async_refresh() for device in devices]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
67
homeassistant/components/flo/config_flow.py
Normal file
67
homeassistant/components/flo/config_flow.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
"""Config flow for flo integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioflo import async_get_api
|
||||||
|
from aioflo.errors import RequestError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema({"username": str, "password": str})
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
try:
|
||||||
|
api = await async_get_api(
|
||||||
|
data[CONF_USERNAME], data[CONF_PASSWORD], session=session
|
||||||
|
)
|
||||||
|
except RequestError:
|
||||||
|
raise CannotConnect
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
user_info = await api.user.get_info()
|
||||||
|
a_location_id = user_info["locations"][0]["id"]
|
||||||
|
location_info = await api.location.get_info(a_location_id)
|
||||||
|
return {"title": location_info["nickname"]}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for flo."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
info = await validate_input(self.hass, user_input)
|
||||||
|
|
||||||
|
return self.async_create_entry(title=info["title"], data=user_input)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
3
homeassistant/components/flo/const.py
Normal file
3
homeassistant/components/flo/const.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
"""Constants for the flo integration."""
|
||||||
|
|
||||||
|
DOMAIN = "flo"
|
156
homeassistant/components/flo/device.py
Normal file
156
homeassistant/components/flo/device.py
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
"""Flo device object."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from aioflo.api import API
|
||||||
|
from aioflo.errors import RequestError
|
||||||
|
from async_timeout import timeout
|
||||||
|
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from .const import DOMAIN as FLO_DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Flo device object."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistantType, api_client: API, location_id: str, device_id: str
|
||||||
|
):
|
||||||
|
"""Initialize the device."""
|
||||||
|
self.hass: HomeAssistantType = hass
|
||||||
|
self.api_client: API = api_client
|
||||||
|
self._flo_location_id: str = location_id
|
||||||
|
self._flo_device_id: str = device_id
|
||||||
|
self._manufacturer: str = "Flo by Moen"
|
||||||
|
self._device_information: Optional[Dict[str, Any]] = None
|
||||||
|
self._water_usage: Optional[Dict[str, Any]] = None
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=f"{FLO_DOMAIN}-{device_id}",
|
||||||
|
update_interval=timedelta(seconds=60),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self):
|
||||||
|
"""Update data via library."""
|
||||||
|
try:
|
||||||
|
async with timeout(10):
|
||||||
|
await asyncio.gather(
|
||||||
|
*[self._update_device(), self._update_consumption_data()]
|
||||||
|
)
|
||||||
|
except (RequestError) as error:
|
||||||
|
raise UpdateFailed(error)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_id(self) -> str:
|
||||||
|
"""Return Flo location id."""
|
||||||
|
return self._flo_location_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self) -> str:
|
||||||
|
"""Return Flo device id."""
|
||||||
|
return self._flo_device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_name(self) -> str:
|
||||||
|
"""Return device name."""
|
||||||
|
return f"{self.manufacturer} {self.model}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manufacturer(self) -> str:
|
||||||
|
"""Return manufacturer for device."""
|
||||||
|
return self._manufacturer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac_address(self) -> str:
|
||||||
|
"""Return ieee address for device."""
|
||||||
|
return self._device_information["macAddress"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> str:
|
||||||
|
"""Return model for device."""
|
||||||
|
return self._device_information["deviceModel"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rssi(self) -> float:
|
||||||
|
"""Return rssi for device."""
|
||||||
|
return self._device_information["connectivity"]["rssi"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_heard_from_time(self) -> str:
|
||||||
|
"""Return lastHeardFromTime for device."""
|
||||||
|
return self._device_information["lastHeardFromTime"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_type(self) -> str:
|
||||||
|
"""Return the device type for the device."""
|
||||||
|
return self._device_information["deviceType"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if device is available."""
|
||||||
|
return self.last_update_success and self._device_information["isConnected"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_system_mode(self) -> str:
|
||||||
|
"""Return the current system mode."""
|
||||||
|
return self._device_information["systemMode"]["lastKnown"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_system_mode(self) -> str:
|
||||||
|
"""Return the target system mode."""
|
||||||
|
return self._device_information["systemMode"]["target"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_flow_rate(self) -> float:
|
||||||
|
"""Return current flow rate in gpm."""
|
||||||
|
return self._device_information["telemetry"]["current"]["gpm"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_psi(self) -> float:
|
||||||
|
"""Return the current pressure in psi."""
|
||||||
|
return self._device_information["telemetry"]["current"]["psi"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature(self) -> float:
|
||||||
|
"""Return the current temperature in degrees F."""
|
||||||
|
return self._device_information["telemetry"]["current"]["tempF"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def consumption_today(self) -> float:
|
||||||
|
"""Return the current consumption for today in gallons."""
|
||||||
|
return self._water_usage["aggregations"]["sumTotalGallonsConsumed"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def firmware_version(self) -> str:
|
||||||
|
"""Return the firmware version for the device."""
|
||||||
|
return self._device_information["fwVersion"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_number(self) -> str:
|
||||||
|
"""Return the serial number for the device."""
|
||||||
|
return self._device_information["serialNumber"]
|
||||||
|
|
||||||
|
async def _update_device(self, *_) -> None:
|
||||||
|
"""Update the device information from the API."""
|
||||||
|
self._device_information = await self.api_client.device.get_info(
|
||||||
|
self._flo_device_id
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Flo device data: %s", self._device_information)
|
||||||
|
|
||||||
|
async def _update_consumption_data(self, *_) -> None:
|
||||||
|
"""Update water consumption data from the API."""
|
||||||
|
today = dt_util.now().date()
|
||||||
|
start_date = datetime(today.year, today.month, today.day, 0, 0)
|
||||||
|
end_date = datetime(today.year, today.month, today.day, 23, 59, 59, 999000)
|
||||||
|
self._water_usage = await self.api_client.water.get_consumption_info(
|
||||||
|
self._flo_location_id, start_date, end_date
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Updated Flo consumption data: %s", self._water_usage)
|
70
homeassistant/components/flo/entity.py
Normal file
70
homeassistant/components/flo/entity.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""Base entity class for Flo entities."""
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DOMAIN as FLO_DOMAIN
|
||||||
|
from .device import FloDeviceDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class FloEntity(Entity):
|
||||||
|
"""A base class for Flo entities."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
entity_type: str,
|
||||||
|
name: str,
|
||||||
|
device: FloDeviceDataUpdateCoordinator,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Init Flo entity."""
|
||||||
|
self._unique_id: str = f"{device.mac_address}_{entity_type}"
|
||||||
|
self._name: str = name
|
||||||
|
self._device: FloDeviceDataUpdateCoordinator = device
|
||||||
|
self._state: Any = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return Entity's default name."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return a device description for device registry."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(FLO_DOMAIN, self._device.id)},
|
||||||
|
"connections": {(CONNECTION_NETWORK_MAC, self._device.mac_address)},
|
||||||
|
"manufacturer": self._device.manufacturer,
|
||||||
|
"model": self._device.model,
|
||||||
|
"name": self._device.device_name,
|
||||||
|
"sw_version": self._device.firmware_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if device is available."""
|
||||||
|
return self._device.available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def force_update(self) -> bool:
|
||||||
|
"""Force update this entity."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Poll state from device."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update Flo entity."""
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""When entity is added to hass."""
|
||||||
|
self.async_on_remove(self._device.async_add_listener(self.async_write_ha_state))
|
12
homeassistant/components/flo/manifest.json
Normal file
12
homeassistant/components/flo/manifest.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"domain": "flo",
|
||||||
|
"name": "Flo",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/flo",
|
||||||
|
"requirements": ["aioflo==0.4.0"],
|
||||||
|
"ssdp": [],
|
||||||
|
"zeroconf": [],
|
||||||
|
"homekit": {},
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": ["@dmulcahey"]
|
||||||
|
}
|
158
homeassistant/components/flo/sensor.py
Normal file
158
homeassistant/components/flo/sensor.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
"""Support for Flo Water Monitor sensors."""
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
DEVICE_CLASS_PRESSURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
PRESSURE_PSI,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
VOLUME_GALLONS,
|
||||||
|
)
|
||||||
|
from homeassistant.util.temperature import fahrenheit_to_celsius
|
||||||
|
|
||||||
|
from .const import DOMAIN as FLO_DOMAIN
|
||||||
|
from .device import FloDeviceDataUpdateCoordinator
|
||||||
|
from .entity import FloEntity
|
||||||
|
|
||||||
|
DEPENDENCIES = ["flo"]
|
||||||
|
|
||||||
|
WATER_ICON = "mdi:water"
|
||||||
|
GAUGE_ICON = "mdi:gauge"
|
||||||
|
NAME_DAILY_USAGE = "Today's Water Usage"
|
||||||
|
NAME_CURRENT_SYSTEM_MODE = "Current System Mode"
|
||||||
|
NAME_FLOW_RATE = "Water Flow Rate"
|
||||||
|
NAME_TEMPERATURE = "Water Temperature"
|
||||||
|
NAME_WATER_PRESSURE = "Water Pressure"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up the Flo sensors from config entry."""
|
||||||
|
devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"]
|
||||||
|
entities = []
|
||||||
|
entities.extend([FloDailyUsageSensor(device) for device in devices])
|
||||||
|
entities.extend([FloSystemModeSensor(device) for device in devices])
|
||||||
|
entities.extend([FloCurrentFlowRateSensor(device) for device in devices])
|
||||||
|
entities.extend([FloTemperatureSensor(device) for device in devices])
|
||||||
|
entities.extend([FloPressureSensor(device) for device in devices])
|
||||||
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
|
class FloDailyUsageSensor(FloEntity):
|
||||||
|
"""Monitors the daily water usage."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the daily water usage sensor."""
|
||||||
|
super().__init__("daily_consumption", NAME_DAILY_USAGE, device)
|
||||||
|
self._state: float = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the daily usage icon."""
|
||||||
|
return WATER_ICON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[float]:
|
||||||
|
"""Return the current daily usage."""
|
||||||
|
if self._device.consumption_today is None:
|
||||||
|
return None
|
||||||
|
return round(self._device.consumption_today, 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> str:
|
||||||
|
"""Return gallons as the unit measurement for water."""
|
||||||
|
return VOLUME_GALLONS
|
||||||
|
|
||||||
|
|
||||||
|
class FloSystemModeSensor(FloEntity):
|
||||||
|
"""Monitors the current Flo system mode."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the system mode sensor."""
|
||||||
|
super().__init__("current_system_mode", NAME_CURRENT_SYSTEM_MODE, device)
|
||||||
|
self._state: str = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[str]:
|
||||||
|
"""Return the current system mode."""
|
||||||
|
if not self._device.current_system_mode:
|
||||||
|
return None
|
||||||
|
return self._device.current_system_mode
|
||||||
|
|
||||||
|
|
||||||
|
class FloCurrentFlowRateSensor(FloEntity):
|
||||||
|
"""Monitors the current water flow rate."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the flow rate sensor."""
|
||||||
|
super().__init__("current_flow_rate", NAME_FLOW_RATE, device)
|
||||||
|
self._state: float = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the daily usage icon."""
|
||||||
|
return GAUGE_ICON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[float]:
|
||||||
|
"""Return the current flow rate."""
|
||||||
|
if self._device.current_flow_rate is None:
|
||||||
|
return None
|
||||||
|
return round(self._device.current_flow_rate, 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> str:
|
||||||
|
"""Return the unit measurement."""
|
||||||
|
return "gpm"
|
||||||
|
|
||||||
|
|
||||||
|
class FloTemperatureSensor(FloEntity):
|
||||||
|
"""Monitors the temperature."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the temperature sensor."""
|
||||||
|
super().__init__("temperature", NAME_TEMPERATURE, device)
|
||||||
|
self._state: float = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[float]:
|
||||||
|
"""Return the current temperature."""
|
||||||
|
if self._device.temperature is None:
|
||||||
|
return None
|
||||||
|
return round(fahrenheit_to_celsius(self._device.temperature), 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> str:
|
||||||
|
"""Return gallons as the unit measurement for water."""
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> Optional[str]:
|
||||||
|
"""Return the device class for this sensor."""
|
||||||
|
return DEVICE_CLASS_TEMPERATURE
|
||||||
|
|
||||||
|
|
||||||
|
class FloPressureSensor(FloEntity):
|
||||||
|
"""Monitors the water pressure."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the pressure sensor."""
|
||||||
|
super().__init__("water_pressure", NAME_WATER_PRESSURE, device)
|
||||||
|
self._state: float = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[float]:
|
||||||
|
"""Return the current water pressure."""
|
||||||
|
if self._device.current_psi is None:
|
||||||
|
return None
|
||||||
|
return round(self._device.current_psi, 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> str:
|
||||||
|
"""Return gallons as the unit measurement for water."""
|
||||||
|
return PRESSURE_PSI
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> Optional[str]:
|
||||||
|
"""Return the device class for this sensor."""
|
||||||
|
return DEVICE_CLASS_PRESSURE
|
22
homeassistant/components/flo/strings.json
Normal file
22
homeassistant/components/flo/strings.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"title": "flo",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"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_device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
homeassistant/components/flo/translations/en.json
Normal file
22
homeassistant/components/flo/translations/en.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "flo"
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ FLOWS = [
|
||||||
"enocean",
|
"enocean",
|
||||||
"esphome",
|
"esphome",
|
||||||
"flick_electric",
|
"flick_electric",
|
||||||
|
"flo",
|
||||||
"flume",
|
"flume",
|
||||||
"flunearyou",
|
"flunearyou",
|
||||||
"forked_daapd",
|
"forked_daapd",
|
||||||
|
|
|
@ -160,6 +160,9 @@ aiodns==2.0.0
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==2.6.1
|
aioesphomeapi==2.6.1
|
||||||
|
|
||||||
|
# homeassistant.components.flo
|
||||||
|
aioflo==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.freebox
|
# homeassistant.components.freebox
|
||||||
aiofreepybox==0.0.8
|
aiofreepybox==0.0.8
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,9 @@ aiodns==2.0.0
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==2.6.1
|
aioesphomeapi==2.6.1
|
||||||
|
|
||||||
|
# homeassistant.components.flo
|
||||||
|
aioflo==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.freebox
|
# homeassistant.components.freebox
|
||||||
aiofreepybox==0.0.8
|
aiofreepybox==0.0.8
|
||||||
|
|
||||||
|
|
1
tests/components/flo/__init__.py
Normal file
1
tests/components/flo/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the flo integration."""
|
12
tests/components/flo/common.py
Normal file
12
tests/components/flo/common.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""Define common test utilities."""
|
||||||
|
TEST_ACCOUNT_ID = "aabbccdd"
|
||||||
|
TEST_DEVICE_ID = "98765"
|
||||||
|
TEST_EMAIL_ADDRESS = "email@address.com"
|
||||||
|
TEST_FIRST_NAME = "Tom"
|
||||||
|
TEST_LAST_NAME = "Jones"
|
||||||
|
TEST_LOCATION_ID = "mmnnoopp"
|
||||||
|
TEST_MAC_ADDRESS = "12:34:56:ab:cd:ef"
|
||||||
|
TEST_PASSWORD = "password"
|
||||||
|
TEST_PHONE_NUMBER = "+1 123-456-7890"
|
||||||
|
TEST_TOKEN = "123abc"
|
||||||
|
TEST_USER_ID = "12345abcde"
|
83
tests/components/flo/conftest.py
Normal file
83
tests/components/flo/conftest.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
"""Define fixtures available for all tests."""
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_entry(hass):
|
||||||
|
"""Config entry version 1 fixture."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=FLO_DOMAIN,
|
||||||
|
data={CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD},
|
||||||
|
version=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def aioclient_mock_fixture(aioclient_mock):
|
||||||
|
"""Fixture to provide a aioclient mocker."""
|
||||||
|
|
||||||
|
now = round(time.time())
|
||||||
|
|
||||||
|
# Mocks the login response for flo.
|
||||||
|
aioclient_mock.post(
|
||||||
|
"https://api.meetflo.com/api/v1/users/auth",
|
||||||
|
text=json.dumps(
|
||||||
|
{
|
||||||
|
"token": TEST_TOKEN,
|
||||||
|
"tokenPayload": {
|
||||||
|
"user": {"user_id": TEST_USER_ID, "email": TEST_EMAIL_ADDRESS},
|
||||||
|
"timestamp": now,
|
||||||
|
},
|
||||||
|
"tokenExpiration": 86400,
|
||||||
|
"timeNow": now,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
# Mocks the device for flo.
|
||||||
|
aioclient_mock.get(
|
||||||
|
"https://api-gw.meetflo.com/api/v2/devices/98765",
|
||||||
|
text=load_fixture("flo/device_info_response.json"),
|
||||||
|
status=200,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
# Mocks the water consumption for flo.
|
||||||
|
aioclient_mock.get(
|
||||||
|
"https://api-gw.meetflo.com/api/v2/water/consumption",
|
||||||
|
text=load_fixture("flo/water_consumption_info_response.json"),
|
||||||
|
status=200,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
# Mocks the location info for flo.
|
||||||
|
aioclient_mock.get(
|
||||||
|
"https://api-gw.meetflo.com/api/v2/locations/mmnnoopp",
|
||||||
|
text=load_fixture("flo/location_info_expand_devices_response.json"),
|
||||||
|
status=200,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
# Mocks the user info for flo.
|
||||||
|
aioclient_mock.get(
|
||||||
|
"https://api-gw.meetflo.com/api/v2/users/12345abcde",
|
||||||
|
text=load_fixture("flo/user_info_expand_locations_response.json"),
|
||||||
|
status=200,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
params={"expand": "locations"},
|
||||||
|
)
|
||||||
|
# Mocks the user info for flo.
|
||||||
|
aioclient_mock.get(
|
||||||
|
"https://api-gw.meetflo.com/api/v2/users/12345abcde",
|
||||||
|
text=load_fixture("flo/user_info_expand_locations_response.json"),
|
||||||
|
status=200,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
46
tests/components/flo/test_config_flow.py
Normal file
46
tests/components/flo/test_config_flow.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""Test the flo config flow."""
|
||||||
|
from homeassistant import config_entries, setup
|
||||||
|
from homeassistant.components.flo.const import DOMAIN
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass, aioclient_mock_fixture):
|
||||||
|
"""Test we get the form."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.flo.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.flo.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"username": "test-username", "password": "test-password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "Home"
|
||||||
|
assert result2["data"] == {"username": "test-username", "password": "test-password"}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass, aioclient_mock):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"username": "test-username", "password": "test-password"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
50
tests/components/flo/test_device.py
Normal file
50
tests/components/flo/test_device.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"""Define tests for device-related endpoints."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN
|
||||||
|
from homeassistant.components.flo.device import FloDeviceDataUpdateCoordinator
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt
|
||||||
|
|
||||||
|
from .common import TEST_PASSWORD, TEST_USER_ID
|
||||||
|
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock):
|
||||||
|
"""Test Flo by Moen device."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.data[FLO_DOMAIN]["devices"]) == 1
|
||||||
|
|
||||||
|
device: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN]["devices"][0]
|
||||||
|
assert device.api_client is not None
|
||||||
|
assert device.available
|
||||||
|
assert device.consumption_today == 3.674
|
||||||
|
assert device.current_flow_rate == 0
|
||||||
|
assert device.current_psi == 54.20000076293945
|
||||||
|
assert device.current_system_mode == "home"
|
||||||
|
assert device.target_system_mode == "home"
|
||||||
|
assert device.firmware_version == "6.1.1"
|
||||||
|
assert device.device_type == "flo_device_v2"
|
||||||
|
assert device.id == "98765"
|
||||||
|
assert device.last_heard_from_time == "2020-07-24T12:45:00Z"
|
||||||
|
assert device.location_id == "mmnnoopp"
|
||||||
|
assert device.hass is not None
|
||||||
|
assert device.temperature == 70
|
||||||
|
assert device.mac_address == "111111111111"
|
||||||
|
assert device.model == "flo_device_075_v2"
|
||||||
|
assert device.manufacturer == "Flo by Moen"
|
||||||
|
assert device.device_name == "Flo by Moen flo_device_075_v2"
|
||||||
|
assert device.rssi == -47
|
||||||
|
|
||||||
|
call_count = aioclient_mock.call_count
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=90))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert aioclient_mock.call_count == call_count + 2
|
16
tests/components/flo/test_init.py
Normal file
16
tests/components/flo/test_init.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""Test init."""
|
||||||
|
from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .common import TEST_PASSWORD, TEST_USER_ID
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry(hass, config_entry, aioclient_mock_fixture):
|
||||||
|
"""Test migration of config entry from v1."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.data[FLO_DOMAIN]["devices"]) == 1
|
24
tests/components/flo/test_sensor.py
Normal file
24
tests/components/flo/test_sensor.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
"""Test Flo by Moen sensor entities."""
|
||||||
|
from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .common import TEST_PASSWORD, TEST_USER_ID
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors(hass, config_entry, aioclient_mock_fixture):
|
||||||
|
"""Test Flo by Moen sensors."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.data[FLO_DOMAIN]["devices"]) == 1
|
||||||
|
|
||||||
|
# we should have 5 entities for the device
|
||||||
|
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.water_flow_rate").state == "0"
|
||||||
|
assert hass.states.get("sensor.water_pressure").state == "54.2"
|
||||||
|
assert hass.states.get("sensor.water_temperature").state == "21.1"
|
238
tests/fixtures/flo/device_info_response.json
vendored
Normal file
238
tests/fixtures/flo/device_info_response.json
vendored
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
{
|
||||||
|
"isConnected": true,
|
||||||
|
"fwVersion": "6.1.1",
|
||||||
|
"lastHeardFromTime": "2020-07-24T12:45:00Z",
|
||||||
|
"fwProperties": {
|
||||||
|
"alarm_away_high_flow_rate_shut_off_enabled": true,
|
||||||
|
"alarm_away_high_water_use_shut_off_enabled": true,
|
||||||
|
"alarm_away_long_flow_event_shut_off_enabled": true,
|
||||||
|
"alarm_away_v2_shut_off_enabled": true,
|
||||||
|
"alarm_home_high_flow_rate_shut_off_deferment": 300,
|
||||||
|
"alarm_home_high_flow_rate_shut_off_enabled": true,
|
||||||
|
"alarm_home_high_water_use_shut_off_deferment": 300,
|
||||||
|
"alarm_home_high_water_use_shut_off_enabled": true,
|
||||||
|
"alarm_home_long_flow_event_shut_off_deferment": 300,
|
||||||
|
"alarm_home_long_flow_event_shut_off_enabled": true,
|
||||||
|
"alarm_shut_off_enabled": true,
|
||||||
|
"alarm_shutoff_id": "",
|
||||||
|
"alarm_shutoff_time_epoch_sec": -1,
|
||||||
|
"alarm_snooze_enabled": true,
|
||||||
|
"alarm_suppress_duplicate_duration": 300,
|
||||||
|
"alarm_suppress_until_event_end": false,
|
||||||
|
"data_flosense_force_retrain": 1,
|
||||||
|
"data_flosense_min_flodetect_sec": 0,
|
||||||
|
"data_flosense_min_irr_sec": 180,
|
||||||
|
"data_flosense_status_interval": 1200,
|
||||||
|
"data_flosense_verbosity": 1,
|
||||||
|
"device_data_free_mb": 1465,
|
||||||
|
"device_installed": true,
|
||||||
|
"device_mem_available_kb": 339456,
|
||||||
|
"device_rootfs_free_kb": 711504,
|
||||||
|
"device_uptime_sec": 867190,
|
||||||
|
"feature_mode": "default",
|
||||||
|
"flodetect_post_enabled": true,
|
||||||
|
"flodetect_post_frequency": 0,
|
||||||
|
"flodetect_storage_days": 60,
|
||||||
|
"flosense_action": "",
|
||||||
|
"flosense_deployment_result": "success",
|
||||||
|
"flosense_link": "",
|
||||||
|
"flosense_shut_off_enabled": true,
|
||||||
|
"flosense_shut_off_level": 3,
|
||||||
|
"flosense_state": "active",
|
||||||
|
"flosense_version_app": "2.5.3",
|
||||||
|
"flosense_version_model": "2.5.0",
|
||||||
|
"fw_ver": "6.1.1",
|
||||||
|
"fw_ver_a": "6.1.1",
|
||||||
|
"fw_ver_b": "6.0.3",
|
||||||
|
"heartbeat_frequency": 1800,
|
||||||
|
"ht_attempt_interval": 60000,
|
||||||
|
"ht_check_window_max_pressure_decay_limit": 0.1,
|
||||||
|
"ht_check_window_width": 30000,
|
||||||
|
"ht_controller": "ultima",
|
||||||
|
"ht_max_open_closed_pressure_decay_pct_limit": 2,
|
||||||
|
"ht_max_pressure_growth_limit": 3,
|
||||||
|
"ht_max_pressure_growth_pct_limit": 3,
|
||||||
|
"ht_max_valve_closures_per_24h": 0,
|
||||||
|
"ht_min_computable_point_limit": 3,
|
||||||
|
"ht_min_pressure_limit": 10,
|
||||||
|
"ht_min_r_squared_limit": 0.9,
|
||||||
|
"ht_min_slope_limit": -0.6,
|
||||||
|
"ht_phase_1_max_pressure_decay_limit": 6,
|
||||||
|
"ht_phase_1_max_pressure_decay_pct_limit": 10,
|
||||||
|
"ht_phase_1_time_index": 12000,
|
||||||
|
"ht_phase_2_max_pressure_decay_limit": 6,
|
||||||
|
"ht_phase_2_max_pressure_decay_pct_limit": 10,
|
||||||
|
"ht_phase_2_time_index": 30000,
|
||||||
|
"ht_phase_3_max_pressure_decay_limit": 3,
|
||||||
|
"ht_phase_3_max_pressure_decay_pct_limit": 5,
|
||||||
|
"ht_phase_3_time_index": 240000,
|
||||||
|
"ht_phase_4_max_pressure_decay_limit": 1.5,
|
||||||
|
"ht_phase_4_max_pressure_decay_pct_limit": 5,
|
||||||
|
"ht_phase_4_time_index": 480000,
|
||||||
|
"ht_pre_delay": 0,
|
||||||
|
"ht_recent_flow_event_cool_down": 1000,
|
||||||
|
"ht_retry_on_fail_interval": 900000,
|
||||||
|
"ht_scheduler": "flosense",
|
||||||
|
"ht_scheduler_end": "08:00",
|
||||||
|
"ht_scheduler_start": "06:00",
|
||||||
|
"ht_scheduler_ultima_allotted_time_1": "06:00",
|
||||||
|
"ht_scheduler_ultima_allotted_time_2": "07:00",
|
||||||
|
"ht_scheduler_ultima_allotted_time_3": "",
|
||||||
|
"ht_times_per_day": 1,
|
||||||
|
"log_bytes_sent": 0,
|
||||||
|
"log_enabled": true,
|
||||||
|
"log_frequency": 3600,
|
||||||
|
"log_send": false,
|
||||||
|
"mender_check": false,
|
||||||
|
"mender_host": "https://mender.flotech.co",
|
||||||
|
"mender_parts_link": "",
|
||||||
|
"mender_ping_delay": 300,
|
||||||
|
"mender_signature": "20200610",
|
||||||
|
"motor_delay_close": 175,
|
||||||
|
"motor_delay_open": 0,
|
||||||
|
"motor_retry_count": 2,
|
||||||
|
"motor_timeout": 5000,
|
||||||
|
"mqtt_host": "mqtt.flosecurecloud.com",
|
||||||
|
"mqtt_port": 8884,
|
||||||
|
"pes_away_max_duration": 1505,
|
||||||
|
"pes_away_max_pressure": 150,
|
||||||
|
"pes_away_max_temperature": 226,
|
||||||
|
"pes_away_max_volume": 91.8913240498193,
|
||||||
|
"pes_away_min_pressure": 20,
|
||||||
|
"pes_away_min_pressure_duration": 5,
|
||||||
|
"pes_away_min_temperature": 36,
|
||||||
|
"pes_away_min_temperature_duration": 10,
|
||||||
|
"pes_away_v1_high_flow_rate": 7.825131772346,
|
||||||
|
"pes_away_v1_high_flow_rate_duration": 5,
|
||||||
|
"pes_away_v2_high_flow_rate": 0.5,
|
||||||
|
"pes_away_v2_high_flow_rate_duration": 5,
|
||||||
|
"pes_home_high_flow_rate": 1000,
|
||||||
|
"pes_home_high_flow_rate_duration": 20,
|
||||||
|
"pes_home_max_duration": 7431,
|
||||||
|
"pes_home_max_pressure": 150,
|
||||||
|
"pes_home_max_temperature": 226,
|
||||||
|
"pes_home_max_volume": 185.56459045410156,
|
||||||
|
"pes_home_min_pressure": 20,
|
||||||
|
"pes_home_min_pressure_duration": 5,
|
||||||
|
"pes_home_min_temperature": 36,
|
||||||
|
"pes_home_min_temperature_duration": 10,
|
||||||
|
"pes_moderately_high_pressure": 80,
|
||||||
|
"pes_moderately_high_pressure_count": 43200,
|
||||||
|
"pes_moderately_high_pressure_delay": 300,
|
||||||
|
"pes_moderately_high_pressure_period": 10,
|
||||||
|
"player_action": "disabled",
|
||||||
|
"player_flow": 0,
|
||||||
|
"player_min_pressure": 40,
|
||||||
|
"player_pressure": 60,
|
||||||
|
"player_temperature": 50,
|
||||||
|
"power_downtime_last_24h": 91,
|
||||||
|
"power_downtime_last_7days": 91,
|
||||||
|
"power_downtime_last_reboot": 91,
|
||||||
|
"pt_state": "ok",
|
||||||
|
"reboot_count": 26,
|
||||||
|
"reboot_count_7days": 1,
|
||||||
|
"reboot_reason": "power_cycle",
|
||||||
|
"s3_bucket_host": "api-bulk.meetflo.com",
|
||||||
|
"serial_number": "111111111111",
|
||||||
|
"system_mode": 2,
|
||||||
|
"tag": "",
|
||||||
|
"telemetry_batched_enabled": true,
|
||||||
|
"telemetry_batched_hf_enabled": true,
|
||||||
|
"telemetry_batched_hf_interval": 10800,
|
||||||
|
"telemetry_batched_hf_poll_rate": 100,
|
||||||
|
"telemetry_batched_interval": 300,
|
||||||
|
"telemetry_batched_pending_storage": 30,
|
||||||
|
"telemetry_batched_sent_storage": 30,
|
||||||
|
"telemetry_flow_rate": 0,
|
||||||
|
"telemetry_pressure": 42.4,
|
||||||
|
"telemetry_realtime_change_gpm": 0,
|
||||||
|
"telemetry_realtime_change_psi": 0,
|
||||||
|
"telemetry_realtime_enabled": true,
|
||||||
|
"telemetry_realtime_interval": 1,
|
||||||
|
"telemetry_realtime_packet_uptime": 0,
|
||||||
|
"telemetry_realtime_session_last_epoch": 1595555701518,
|
||||||
|
"telemetry_realtime_sessions_7days": 25,
|
||||||
|
"telemetry_realtime_storage": 7,
|
||||||
|
"telemetry_realtime_timeout": 300,
|
||||||
|
"telemetry_temperature": 68,
|
||||||
|
"valve_actuation_count": 906,
|
||||||
|
"valve_actuation_timeout_count": 0,
|
||||||
|
"valve_state": 1,
|
||||||
|
"vpn_enabled": false,
|
||||||
|
"vpn_ip": "",
|
||||||
|
"water_event_enabled": false,
|
||||||
|
"water_event_min_duration": 2,
|
||||||
|
"water_event_min_gallons": 0.1,
|
||||||
|
"wifi_bytes_received": 24164,
|
||||||
|
"wifi_bytes_sent": 18319,
|
||||||
|
"wifi_disconnections": 76,
|
||||||
|
"wifi_rssi": -50,
|
||||||
|
"wifi_sta_enc": "psk2",
|
||||||
|
"wifi_sta_ip": "192.168.1.1",
|
||||||
|
"wifi_sta_ssid": "SOMESSID",
|
||||||
|
"zit_auto_count": 2363,
|
||||||
|
"zit_manual_count": 0
|
||||||
|
},
|
||||||
|
"id": "98765",
|
||||||
|
"macAddress": "111111111111",
|
||||||
|
"nickname": "Smart Water Shutoff",
|
||||||
|
"isPaired": true,
|
||||||
|
"deviceModel": "flo_device_075_v2",
|
||||||
|
"deviceType": "flo_device_v2",
|
||||||
|
"irrigationType": "sprinklers",
|
||||||
|
"systemMode": {
|
||||||
|
"isLocked": false,
|
||||||
|
"shouldInherit": true,
|
||||||
|
"lastKnown": "home",
|
||||||
|
"target": "home"
|
||||||
|
},
|
||||||
|
"valve": { "target": "open", "lastKnown": "open" },
|
||||||
|
"installStatus": {
|
||||||
|
"isInstalled": true,
|
||||||
|
"installDate": "2019-05-04T13:50:04.758Z"
|
||||||
|
},
|
||||||
|
"learning": { "outOfLearningDate": "2019-05-10T21:45:48.916Z" },
|
||||||
|
"notifications": {
|
||||||
|
"pending": {
|
||||||
|
"infoCount": 0,
|
||||||
|
"warningCount": 2,
|
||||||
|
"criticalCount": 0,
|
||||||
|
"alarmCount": [
|
||||||
|
{ "id": 30, "severity": "warning", "count": 1 },
|
||||||
|
{ "id": 31, "severity": "warning", "count": 1 }
|
||||||
|
],
|
||||||
|
"info": { "count": 0, "devices": { "count": 0, "absolute": 0 } },
|
||||||
|
"warning": { "count": 2, "devices": { "count": 1, "absolute": 1 } },
|
||||||
|
"critical": { "count": 0, "devices": { "count": 0, "absolute": 0 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hardwareThresholds": {
|
||||||
|
"gpm": { "okMin": 0, "okMax": 29, "minValue": 0, "maxValue": 35 },
|
||||||
|
"psi": { "okMin": 30, "okMax": 80, "minValue": 0, "maxValue": 100 },
|
||||||
|
"lpm": { "okMin": 0, "okMax": 110, "minValue": 0, "maxValue": 130 },
|
||||||
|
"kPa": { "okMin": 210, "okMax": 550, "minValue": 0, "maxValue": 700 },
|
||||||
|
"tempF": { "okMin": 50, "okMax": 80, "minValue": 0, "maxValue": 100 },
|
||||||
|
"tempC": { "okMin": 10, "okMax": 30, "minValue": 0, "maxValue": 40 }
|
||||||
|
},
|
||||||
|
"serialNumber": "111111111111",
|
||||||
|
"connectivity": { "rssi": -47, "ssid": "SOMESSID" },
|
||||||
|
"telemetry": {
|
||||||
|
"current": {
|
||||||
|
"gpm": 0,
|
||||||
|
"psi": 54.20000076293945,
|
||||||
|
"tempF": 70,
|
||||||
|
"updated": "2020-07-24T12:20:58Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthTest": {
|
||||||
|
"config": {
|
||||||
|
"enabled": true,
|
||||||
|
"timesPerDay": 1,
|
||||||
|
"start": "02:00",
|
||||||
|
"end": "04:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutoff": { "scheduledAt": "1970-01-01T00:00:00.000Z" },
|
||||||
|
"actionRules": [],
|
||||||
|
"location": { "id": "mmnnoopp" }
|
||||||
|
}
|
89
tests/fixtures/flo/location_info_base_response.json
vendored
Normal file
89
tests/fixtures/flo/location_info_base_response.json
vendored
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"id": "mmnnoopp",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "12345abcde"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"id": "98765",
|
||||||
|
"macAddress": "123456abcdef"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"userRoles": [
|
||||||
|
{
|
||||||
|
"userId": "12345abcde",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"address": "123 Main Street",
|
||||||
|
"city": "Boston",
|
||||||
|
"state": "MA",
|
||||||
|
"country": "us",
|
||||||
|
"postalCode": "12345",
|
||||||
|
"timezone": "US/Easter",
|
||||||
|
"gallonsPerDayGoal": 240,
|
||||||
|
"occupants": 2,
|
||||||
|
"stories": 2,
|
||||||
|
"isProfileComplete": true,
|
||||||
|
"nickname": "Home",
|
||||||
|
"irrigationSchedule": {
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"systemMode": {
|
||||||
|
"target": "home"
|
||||||
|
},
|
||||||
|
"locationType": "sfh",
|
||||||
|
"locationSize": "lte_4000_sq_ft",
|
||||||
|
"waterShutoffKnown": "unsure",
|
||||||
|
"indoorAmenities": [],
|
||||||
|
"outdoorAmenities": [],
|
||||||
|
"plumbingAppliances": [
|
||||||
|
"exp_tank"
|
||||||
|
],
|
||||||
|
"notifications": {
|
||||||
|
"pending": {
|
||||||
|
"infoCount": 0,
|
||||||
|
"warningCount": 1,
|
||||||
|
"criticalCount": 0,
|
||||||
|
"alarmCount": [
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"severity": "warning",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"areas": {
|
||||||
|
"default": [
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Attic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Basement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Garage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Main Floor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Upstairs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom": []
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "aabbccdd"
|
||||||
|
}
|
||||||
|
}
|
308
tests/fixtures/flo/location_info_expand_devices_response.json
vendored
Normal file
308
tests/fixtures/flo/location_info_expand_devices_response.json
vendored
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
{
|
||||||
|
"id": "mmnnoopp",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "12345abcde"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"isConnected": true,
|
||||||
|
"fwVersion": "4.2.4",
|
||||||
|
"lastHeardFromTime": "2020-01-16T19:42:06Z",
|
||||||
|
"fwProperties": {
|
||||||
|
"alarm_home_high_flow_rate_shut_off_deferment": 300,
|
||||||
|
"alarm_home_high_water_use_shut_off_deferment": 300,
|
||||||
|
"alarm_home_long_flow_event_shut_off_deferment": 300,
|
||||||
|
"alarm_shutoff_time_epoch_sec": -1,
|
||||||
|
"alarm_snooze_enabled": true,
|
||||||
|
"alarm_suppress_duplicate_duration": 300,
|
||||||
|
"alarm_suppress_until_event_end": false,
|
||||||
|
"data_flosense_force_retrain": 0,
|
||||||
|
"data_flosense_status_interval": 1200,
|
||||||
|
"data_flosense_verbosity": 1,
|
||||||
|
"device_data_free_mb": 1464,
|
||||||
|
"device_installed": true,
|
||||||
|
"device_mem_available_kb": 292780,
|
||||||
|
"device_rootfs_free_kb": 802604,
|
||||||
|
"device_uptime_sec": 334862,
|
||||||
|
"flosense_action": "start",
|
||||||
|
"flosense_deployment_result": "success",
|
||||||
|
"flosense_link": "",
|
||||||
|
"flosense_shut_off_enabled": true,
|
||||||
|
"flosense_shut_off_level": 2,
|
||||||
|
"flosense_state": "active",
|
||||||
|
"flosense_version_app": "2.0.0",
|
||||||
|
"flosense_version_model": "2.0.0",
|
||||||
|
"fw_ver": "4.2.4",
|
||||||
|
"fw_ver_a": "4.1.5",
|
||||||
|
"fw_ver_b": "4.2.4",
|
||||||
|
"ht_attempt_interval": 60000,
|
||||||
|
"ht_check_window_max_pressure_decay_limit": 0.1,
|
||||||
|
"ht_check_window_width": 30000,
|
||||||
|
"ht_max_open_closed_pressure_decay_pct_limit": 2,
|
||||||
|
"ht_max_pressure_growth_limit": 3,
|
||||||
|
"ht_max_pressure_growth_pct_limit": 3,
|
||||||
|
"ht_min_computable_point_limit": 3,
|
||||||
|
"ht_min_pressure_limit": 10,
|
||||||
|
"ht_min_r_squared_limit": 0.9,
|
||||||
|
"ht_min_slope_limit": -0.6,
|
||||||
|
"ht_phase_1_max_pressure_decay_limit": 6,
|
||||||
|
"ht_phase_1_max_pressure_decay_pct_limit": 10,
|
||||||
|
"ht_phase_1_time_index": 12000,
|
||||||
|
"ht_phase_2_max_pressure_decay_limit": 6,
|
||||||
|
"ht_phase_2_max_pressure_decay_pct_limit": 10,
|
||||||
|
"ht_phase_2_time_index": 30000,
|
||||||
|
"ht_phase_3_max_pressure_decay_limit": 3,
|
||||||
|
"ht_phase_3_max_pressure_decay_pct_limit": 5,
|
||||||
|
"ht_phase_3_time_index": 240000,
|
||||||
|
"ht_phase_4_max_pressure_decay_limit": 1.5,
|
||||||
|
"ht_phase_4_max_pressure_decay_pct_limit": 5,
|
||||||
|
"ht_phase_4_time_index": 480000,
|
||||||
|
"ht_pre_delay": 0,
|
||||||
|
"ht_recent_flow_event_cool_down": 1000,
|
||||||
|
"ht_retry_on_fail_interval": 900000,
|
||||||
|
"ht_times_per_day": 1,
|
||||||
|
"log_bytes_sent": 176255,
|
||||||
|
"log_frequency": 3600,
|
||||||
|
"mender_host": "https://mender.flotech.co",
|
||||||
|
"motor_delay_close": 175,
|
||||||
|
"motor_delay_open": 0,
|
||||||
|
"motor_retry_count": 2,
|
||||||
|
"motor_timeout": 5000,
|
||||||
|
"pes_away_max_duration": 3600,
|
||||||
|
"pes_away_max_pressure": 150,
|
||||||
|
"pes_away_max_temperature": 226,
|
||||||
|
"pes_away_max_volume": 50,
|
||||||
|
"pes_away_min_pressure": 20,
|
||||||
|
"pes_away_min_temperature": 36,
|
||||||
|
"pes_away_v1_high_flow_rate": 8,
|
||||||
|
"pes_away_v1_high_flow_rate_duration": 5,
|
||||||
|
"pes_away_v2_high_flow_rate": 0.5,
|
||||||
|
"pes_away_v2_high_flow_rate_duration": 5,
|
||||||
|
"pes_home_high_flow_rate": 9.902778339386035,
|
||||||
|
"pes_home_high_flow_rate_duration": 20,
|
||||||
|
"pes_home_max_duration": 1738,
|
||||||
|
"pes_home_max_pressure": 150,
|
||||||
|
"pes_home_max_temperature": 226,
|
||||||
|
"pes_home_max_volume": 33.851015281677256,
|
||||||
|
"pes_home_min_pressure": 20,
|
||||||
|
"pes_home_min_temperature": 36,
|
||||||
|
"pes_moderately_high_pressure": 80,
|
||||||
|
"pes_moderately_high_pressure_count": 43200,
|
||||||
|
"pes_moderately_high_pressure_delay": 300,
|
||||||
|
"pes_moderately_high_pressure_period": 10,
|
||||||
|
"player_action": "disabled",
|
||||||
|
"player_flow": 0,
|
||||||
|
"player_min_pressure": 40,
|
||||||
|
"player_pressure": 60,
|
||||||
|
"player_temperature": 50,
|
||||||
|
"power_downtime_last_24h": 0,
|
||||||
|
"power_downtime_last_7days": 69,
|
||||||
|
"power_downtime_last_reboot": 0,
|
||||||
|
"reboot_count": 27,
|
||||||
|
"reboot_count_7days": 2,
|
||||||
|
"reboot_reason": "power_cycle",
|
||||||
|
"s3_bucket_host": "api-bulk.meetflo.com",
|
||||||
|
"serial_number": "294215640115",
|
||||||
|
"system_mode": 2,
|
||||||
|
"telemetry_batched_enabled": true,
|
||||||
|
"telemetry_batched_interval": 300,
|
||||||
|
"telemetry_batched_pending_storage": 30,
|
||||||
|
"telemetry_batched_sent_storage": 30,
|
||||||
|
"telemetry_flow_rate": 0,
|
||||||
|
"telemetry_pressure": 78.07500375373304,
|
||||||
|
"telemetry_realtime_change_gpm": 0,
|
||||||
|
"telemetry_realtime_change_psi": 0,
|
||||||
|
"telemetry_realtime_interval": 1,
|
||||||
|
"telemetry_realtime_session_last_epoch": 0,
|
||||||
|
"telemetry_realtime_sessions_7days": 0,
|
||||||
|
"telemetry_realtime_storage": 7,
|
||||||
|
"telemetry_realtime_timeout": 299,
|
||||||
|
"telemetry_temperature": 57.00000047232966,
|
||||||
|
"valve_actuation_count": 3465,
|
||||||
|
"valve_actuation_timeout_count": 0,
|
||||||
|
"valve_state": 1,
|
||||||
|
"wifi_bytes_received": 145018827,
|
||||||
|
"wifi_bytes_sent": 80891494,
|
||||||
|
"wifi_disconnections": 423,
|
||||||
|
"wifi_rssi": -61,
|
||||||
|
"wifi_sta_enc": "psk2",
|
||||||
|
"wifi_sta_ssid": "IP freely",
|
||||||
|
"zit_auto_count": 233,
|
||||||
|
"zit_manual_count": 0
|
||||||
|
},
|
||||||
|
"id": "98765",
|
||||||
|
"macAddress": "123456abcdef",
|
||||||
|
"nickname": "Smart Water Shutoff",
|
||||||
|
"isPaired": true,
|
||||||
|
"deviceModel": "flo_device_075_v2",
|
||||||
|
"deviceType": "flo_device_v2",
|
||||||
|
"irrigationType": "sprinklers",
|
||||||
|
"systemMode": {
|
||||||
|
"isLocked": false,
|
||||||
|
"shouldInherit": true,
|
||||||
|
"lastKnown": "home",
|
||||||
|
"target": "home"
|
||||||
|
},
|
||||||
|
"valve": {
|
||||||
|
"target": "open",
|
||||||
|
"lastKnown": "open"
|
||||||
|
},
|
||||||
|
"installStatus": {
|
||||||
|
"isInstalled": true,
|
||||||
|
"installDate": "2018-08-16T02:07:39.483Z"
|
||||||
|
},
|
||||||
|
"learning": {
|
||||||
|
"outOfLearningDate": "2018-08-16T02:07:39.483Z"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"pending": {
|
||||||
|
"infoCount": 0,
|
||||||
|
"warningCount": 1,
|
||||||
|
"criticalCount": 0,
|
||||||
|
"alarmCount": [
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"severity": "warning",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hardwareThresholds": {
|
||||||
|
"gpm": {
|
||||||
|
"okMin": 0,
|
||||||
|
"okMax": 29,
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 35
|
||||||
|
},
|
||||||
|
"psi": {
|
||||||
|
"okMin": 30,
|
||||||
|
"okMax": 80,
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 100
|
||||||
|
},
|
||||||
|
"lpm": {
|
||||||
|
"okMin": 0,
|
||||||
|
"okMax": 110,
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 130
|
||||||
|
},
|
||||||
|
"kPa": {
|
||||||
|
"okMin": 210,
|
||||||
|
"okMax": 550,
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 700
|
||||||
|
},
|
||||||
|
"tempF": {
|
||||||
|
"okMin": 50,
|
||||||
|
"okMax": 80,
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 100
|
||||||
|
},
|
||||||
|
"tempC": {
|
||||||
|
"okMin": 10,
|
||||||
|
"okMax": 30,
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 40
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serialNumber": "xxxxx",
|
||||||
|
"connectivity": {
|
||||||
|
"rssi": -61,
|
||||||
|
"ssid": "IP freely"
|
||||||
|
},
|
||||||
|
"telemetry": {
|
||||||
|
"current": {
|
||||||
|
"gpm": 0,
|
||||||
|
"psi": 78.9000015258789,
|
||||||
|
"tempF": 57,
|
||||||
|
"updated": "2020-01-16T19:01:59Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutoff": {
|
||||||
|
"scheduledAt": "1970-01-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
"actionRules": [],
|
||||||
|
"location": {
|
||||||
|
"id": "mmnnoopp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"userRoles": [
|
||||||
|
{
|
||||||
|
"userId": "12345abcde",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"address": "123 Main Street",
|
||||||
|
"city": "Boston",
|
||||||
|
"state": "MA",
|
||||||
|
"country": "us",
|
||||||
|
"postalCode": "12345",
|
||||||
|
"timezone": "US/Eastern",
|
||||||
|
"gallonsPerDayGoal": 240,
|
||||||
|
"occupants": 2,
|
||||||
|
"stories": 2,
|
||||||
|
"isProfileComplete": true,
|
||||||
|
"nickname": "Home",
|
||||||
|
"irrigationSchedule": {
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"systemMode": {
|
||||||
|
"target": "home"
|
||||||
|
},
|
||||||
|
"locationType": "sfh",
|
||||||
|
"locationSize": "lte_4000_sq_ft",
|
||||||
|
"waterShutoffKnown": "unsure",
|
||||||
|
"indoorAmenities": [],
|
||||||
|
"outdoorAmenities": [],
|
||||||
|
"plumbingAppliances": [
|
||||||
|
"exp_tank"
|
||||||
|
],
|
||||||
|
"notifications": {
|
||||||
|
"pending": {
|
||||||
|
"infoCount": 0,
|
||||||
|
"warningCount": 1,
|
||||||
|
"criticalCount": 0,
|
||||||
|
"alarmCount": [
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"severity": "warning",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"areas": {
|
||||||
|
"default": [
|
||||||
|
{
|
||||||
|
"id": "xxxx",
|
||||||
|
"name": "Attic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxx",
|
||||||
|
"name": "Basement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxx",
|
||||||
|
"name": "Garage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxx",
|
||||||
|
"name": "Main Floor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxx",
|
||||||
|
"name": "Upstairs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom": []
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "aabbccdd"
|
||||||
|
}
|
||||||
|
}
|
34
tests/fixtures/flo/user_info_base_response.json
vendored
Normal file
34
tests/fixtures/flo/user_info_base_response.json
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"id": "12345abcde",
|
||||||
|
"email": "email@address.com",
|
||||||
|
"isActive": true,
|
||||||
|
"firstName": "Tom",
|
||||||
|
"lastName": "Jones",
|
||||||
|
"unitSystem": "imperial_us",
|
||||||
|
"phoneMobile": "+1 123-456-7890",
|
||||||
|
"locale": "en-US",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "mmnnoopp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alarmSettings": [],
|
||||||
|
"locationRoles": [
|
||||||
|
{
|
||||||
|
"locationId": "mmnnoopp",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accountRole": {
|
||||||
|
"accountId": "aabbccdd",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "aabbccdd"
|
||||||
|
},
|
||||||
|
"enabledFeatures": []
|
||||||
|
}
|
120
tests/fixtures/flo/user_info_expand_locations_response.json
vendored
Normal file
120
tests/fixtures/flo/user_info_expand_locations_response.json
vendored
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{
|
||||||
|
"id": "12345abcde",
|
||||||
|
"email": "email@address.com",
|
||||||
|
"isActive": true,
|
||||||
|
"firstName": "Tom",
|
||||||
|
"lastName": "Jones",
|
||||||
|
"unitSystem": "imperial_us",
|
||||||
|
"phoneMobile": "+1 123-456-7890",
|
||||||
|
"locale": "en-US",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"id": "mmnnoopp",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "12345abcde"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"id": "98765",
|
||||||
|
"macAddress": "606405c11e10"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"userRoles": [
|
||||||
|
{
|
||||||
|
"userId": "12345abcde",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"address": "123 Main Stree",
|
||||||
|
"city": "Boston",
|
||||||
|
"state": "MA",
|
||||||
|
"country": "us",
|
||||||
|
"postalCode": "12345",
|
||||||
|
"timezone": "US/Easter",
|
||||||
|
"gallonsPerDayGoal": 240,
|
||||||
|
"occupants": 2,
|
||||||
|
"stories": 2,
|
||||||
|
"isProfileComplete": true,
|
||||||
|
"nickname": "Home",
|
||||||
|
"irrigationSchedule": {
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"systemMode": {
|
||||||
|
"target": "home"
|
||||||
|
},
|
||||||
|
"locationType": "sfh",
|
||||||
|
"locationSize": "lte_4000_sq_ft",
|
||||||
|
"waterShutoffKnown": "unsure",
|
||||||
|
"indoorAmenities": [],
|
||||||
|
"outdoorAmenities": [],
|
||||||
|
"plumbingAppliances": [
|
||||||
|
"exp_tank"
|
||||||
|
],
|
||||||
|
"notifications": {
|
||||||
|
"pending": {
|
||||||
|
"infoCount": 0,
|
||||||
|
"warningCount": 1,
|
||||||
|
"criticalCount": 0,
|
||||||
|
"alarmCount": [
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"severity": "warning",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"areas": {
|
||||||
|
"default": [
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Attic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Basement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Garage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Main Floor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xxxxx",
|
||||||
|
"name": "Upstairs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom": []
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "aabbccdd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alarmSettings": [],
|
||||||
|
"locationRoles": [
|
||||||
|
{
|
||||||
|
"locationId": "mmnnoopp",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accountRole": {
|
||||||
|
"accountId": "aabbccdd",
|
||||||
|
"roles": [
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "aabbccdd"
|
||||||
|
},
|
||||||
|
"enabledFeatures": []
|
||||||
|
}
|
34
tests/fixtures/flo/water_consumption_info_response.json
vendored
Normal file
34
tests/fixtures/flo/water_consumption_info_response.json
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"startDate": "2020-01-16T07:00:00.000Z",
|
||||||
|
"endDate": "2020-01-17T06:59:59.999Z",
|
||||||
|
"interval": "1h",
|
||||||
|
"tz": "US/Mountain",
|
||||||
|
"locationId": "mmnnoopp"
|
||||||
|
},
|
||||||
|
"aggregations": {
|
||||||
|
"sumTotalGallonsConsumed": 3.674
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"time": "2020-01-16T00:00:00-07:00",
|
||||||
|
"gallonsConsumed": 0.04
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "2020-01-16T01:00:00-07:00",
|
||||||
|
"gallonsConsumed": 0.477
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "2020-01-16T03:00:00-07:00",
|
||||||
|
"gallonsConsumed": 0.442
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "2020-01-16T07:00:00-07:00",
|
||||||
|
"gallonsConsumed": 1.216
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "2020-01-16T08:00:00-07:00",
|
||||||
|
"gallonsConsumed": 1.499
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -249,7 +249,7 @@ class AiohttpClientMockResponse:
|
||||||
"""Return mock response as a string."""
|
"""Return mock response as a string."""
|
||||||
return self.response.decode(encoding)
|
return self.response.decode(encoding)
|
||||||
|
|
||||||
async def json(self, encoding="utf-8"):
|
async def json(self, encoding="utf-8", content_type=None):
|
||||||
"""Return mock response as a json."""
|
"""Return mock response as a json."""
|
||||||
return _json.loads(self.response.decode(encoding))
|
return _json.loads(self.response.decode(encoding))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue