Mutesync integration (#49679)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
pull/49793/head
Tom Toor 2021-04-27 13:44:59 -07:00 committed by GitHub
parent 41c6474249
commit a57761103c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 318 additions and 0 deletions

View File

@ -631,6 +631,8 @@ omit =
homeassistant/components/msteams/notify.py homeassistant/components/msteams/notify.py
homeassistant/components/mullvad/__init__.py homeassistant/components/mullvad/__init__.py
homeassistant/components/mullvad/binary_sensor.py homeassistant/components/mullvad/binary_sensor.py
homeassistant/components/mutesync/__init__.py
homeassistant/components/mutesync/binary_sensor.py
homeassistant/components/nest/const.py homeassistant/components/nest/const.py
homeassistant/components/mvglive/sensor.py homeassistant/components/mvglive/sensor.py
homeassistant/components/mychevy/* homeassistant/components/mychevy/*

View File

@ -301,6 +301,7 @@ homeassistant/components/mpd/* @fabaff
homeassistant/components/mqtt/* @emontnemery homeassistant/components/mqtt/* @emontnemery
homeassistant/components/msteams/* @peroyvind homeassistant/components/msteams/* @peroyvind
homeassistant/components/mullvad/* @meichthys homeassistant/components/mullvad/* @meichthys
homeassistant/components/mutesync/* @currentoor
homeassistant/components/my/* @home-assistant/core homeassistant/components/my/* @home-assistant/core
homeassistant/components/myq/* @bdraco homeassistant/components/myq/* @bdraco
homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mysensors/* @MartinHjelmare @functionpointer

View File

@ -0,0 +1,54 @@
"""The mütesync integration."""
from __future__ import annotations
from datetime import timedelta
import logging
import async_timeout
import mutesync
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import update_coordinator
from .const import DOMAIN
PLATFORMS = ["binary_sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up mütesync from a config entry."""
client = mutesync.PyMutesync(
entry.data["token"],
entry.data["host"],
hass.helpers.aiohttp_client.async_get_clientsession(),
)
async def update_data():
"""Update the data."""
async with async_timeout.timeout(5):
return await client.get_state()
coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id
] = update_coordinator.DataUpdateCoordinator(
hass,
logging.getLogger(__name__),
name=DOMAIN,
update_interval=timedelta(seconds=10),
update_method=update_data,
)
await coordinator.async_config_entry_first_refresh()
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,53 @@
"""mütesync binary sensor entities."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.helpers import update_coordinator
from .const import DOMAIN
SENSORS = {
"in_meeting": "In Meeting",
"muted": "Muted",
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the mütesync button."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
[MuteStatus(coordinator, sensor_type) for sensor_type in SENSORS], True
)
class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity):
"""Mütesync binary sensors."""
def __init__(self, coordinator, sensor_type):
"""Initialize our sensor."""
super().__init__(coordinator)
self._sensor_type = sensor_type
@property
def name(self):
"""Return the name of the sensor."""
return SENSORS[self._sensor_type]
@property
def unique_id(self):
"""Return the unique ID of the sensor."""
return f"{self.coordinator.data['user-id']}-{self._sensor_type}"
@property
def is_on(self):
"""Return the state of the sensor."""
return self.coordinator.data[self._sensor_type]
@property
def device_info(self):
"""Return the device info of the sensor."""
return {
"identifiers": {(DOMAIN, self.coordinator.data["user-id"])},
"name": "mutesync",
"manufacturer": "mütesync",
"model": "mutesync app",
"entry_type": "service",
}

View File

@ -0,0 +1,82 @@
"""Config flow for mütesync integration."""
from __future__ import annotations
import asyncio
from typing import Any
import aiohttp
import async_timeout
import mutesync
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultDict
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
STEP_USER_DATA_SCHEMA = vol.Schema({"host": str})
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
session = hass.helpers.aiohttp_client.async_get_clientsession()
try:
async with async_timeout.timeout(10):
token = await mutesync.authenticate(session, data["host"])
except aiohttp.ClientResponseError as error:
if error.status == 403:
raise InvalidAuth from error
raise CannotConnect from error
except (aiohttp.ClientError, asyncio.TimeoutError) as error:
raise CannotConnect from error
return token
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mütesync."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResultDict:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
try:
token = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=user_input["host"],
data={"token": token, "host": user_input["host"]},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,3 @@
"""Constants for the mütesync integration."""
DOMAIN = "mutesync"

View File

@ -0,0 +1,11 @@
{
"domain": "mutesync",
"name": "mutesync",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mutesync",
"requirements": ["mutesync==0.0.1"],
"iot_class": "local_polling",
"codeowners": [
"@currentoor"
]
}

View File

@ -0,0 +1,16 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "Enable authentication in mütesync Preferences > Authentication",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -0,0 +1,16 @@
{
"config": {
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Enable authentication in m\u00fctesync Preferences > Authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host"
}
}
}
}
}

View File

@ -155,6 +155,7 @@ FLOWS = [
"motioneye", "motioneye",
"mqtt", "mqtt",
"mullvad", "mullvad",
"mutesync",
"myq", "myq",
"mysensors", "mysensors",
"neato", "neato",

View File

@ -962,6 +962,9 @@ mullvad-api==1.0.0
# homeassistant.components.tts # homeassistant.components.tts
mutagen==1.45.1 mutagen==1.45.1
# homeassistant.components.mutesync
mutesync==0.0.1
# homeassistant.components.mychevy # homeassistant.components.mychevy
mychevy==2.1.1 mychevy==2.1.1

View File

@ -522,6 +522,9 @@ mullvad-api==1.0.0
# homeassistant.components.tts # homeassistant.components.tts
mutagen==1.45.1 mutagen==1.45.1
# homeassistant.components.mutesync
mutesync==0.0.1
# homeassistant.components.keenetic_ndms2 # homeassistant.components.keenetic_ndms2
ndms2_client==0.1.1 ndms2_client==0.1.1

View File

@ -0,0 +1 @@
"""Tests for the mütesync integration."""

View File

@ -0,0 +1,72 @@
"""Test the mütesync config flow."""
import asyncio
from unittest.mock import patch
import aiohttp
import pytest
from homeassistant import config_entries, setup
from homeassistant.components.mutesync.const import DOMAIN
from homeassistant.core import HomeAssistant
async def test_form(hass: HomeAssistant) -> None:
"""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"] is None
with patch("mutesync.authenticate", return_value="bla",), patch(
"homeassistant.components.mutesync.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "1.1.1.1"
assert result2["data"] == {
"host": "1.1.1.1",
"token": "bla",
}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
"side_effect,error",
[
(Exception, "unknown"),
(aiohttp.ClientResponseError(None, None, status=403), "invalid_auth"),
(aiohttp.ClientResponseError(None, None, status=500), "cannot_connect"),
(asyncio.TimeoutError, "cannot_connect"),
],
)
async def test_form_error(
side_effect: Exception, error: str, hass: HomeAssistant
) -> None:
"""Test we handle error situations."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"mutesync.authenticate",
side_effect=side_effect,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": error}