Add support for Flo by Moen water shutoff devices (#38171)

This commit is contained in:
David F. Mulcahey 2020-08-10 08:19:38 -04:00 committed by GitHub
parent 07de9deab6
commit f1fd8aa51f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1650 additions and 1 deletions

View file

@ -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

View 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

View 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."""

View file

@ -0,0 +1,3 @@
"""Constants for the flo integration."""
DOMAIN = "flo"

View 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)

View 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))

View 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"]
}

View 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

View 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%]"
}
}
}

View 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"
}

View file

@ -51,6 +51,7 @@ FLOWS = [
"enocean", "enocean",
"esphome", "esphome",
"flick_electric", "flick_electric",
"flo",
"flume", "flume",
"flunearyou", "flunearyou",
"forked_daapd", "forked_daapd",

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
"""Tests for the flo integration."""

View 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"

View 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"},
)

View 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"}

View 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

View 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

View 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"

View 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" }
}

View 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"
}
}

View 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"
}
}

View 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": []
}

View 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": []
}

View 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
}
]
}

View file

@ -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))