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
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",
|
||||
"esphome",
|
||||
"flick_electric",
|
||||
"flo",
|
||||
"flume",
|
||||
"flunearyou",
|
||||
"forked_daapd",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue