System Bridge 3.x.x (#71218)
* Change to new package and tcp * Rework integration pt1 * Show by default * Handle auth error * Use const * New version avaliable (to be replaced in future by update entity) * Remove visible * Version latest * Filesystem space use * Dev package * Fix sensor * Add services * Update package * Add temperature and voltage * GPU * Bump package version * Update config flow * Add displays * Fix displays connected * Round to whole number * GPU fan speed in RPM * Handle disconnections * Update package * Fix * Update tests * Handle more errors * Check submodule and return missing uuid in test * Handle auth error on config flow * Fix test * Bump package version * Handle key errors * Update package to release version * Client session in config flow * Log * Increase timeout and use similar logic in config flow to init * 30 secs * Add test for timeout error * Cleanup logs Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/system_bridge/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * uuid raise specific error * Type * Lambda to functions for complex logic * Unknown error test * Bump package to 3.0.5 * Bump package to 3.0.6 * Use typings from package and pydantic * Use dict() * Use data listener function and map to models * Use passed module handler * Use lists from models * Update to 3.1.0 * Update coordinator to use passed module * Improve coordinator * Add debug * Bump package and avaliable -> available * Add version check Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
05296fb86e
commit
2ba45a9f99
12 changed files with 892 additions and 731 deletions
|
@ -3,102 +3,114 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
import async_timeout
|
||||
from systembridge import Bridge
|
||||
from systembridge.client import BridgeClient
|
||||
from systembridge.exceptions import BridgeAuthenticationException
|
||||
from systembridge.objects.command.response import CommandResponse
|
||||
from systembridge.objects.keyboard.payload import KeyboardPayload
|
||||
from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
)
|
||||
from systembridgeconnector.version import SUPPORTED_VERSION, Version
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COMMAND,
|
||||
CONF_HOST,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
CONF_URL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN
|
||||
from .const import DOMAIN, MODULES
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
CONF_ARGUMENTS = "arguments"
|
||||
CONF_BRIDGE = "bridge"
|
||||
CONF_KEY = "key"
|
||||
CONF_MODIFIERS = "modifiers"
|
||||
CONF_TEXT = "text"
|
||||
CONF_WAIT = "wait"
|
||||
|
||||
SERVICE_SEND_COMMAND = "send_command"
|
||||
SERVICE_OPEN = "open"
|
||||
SERVICE_OPEN_PATH = "open_path"
|
||||
SERVICE_OPEN_URL = "open_url"
|
||||
SERVICE_SEND_KEYPRESS = "send_keypress"
|
||||
SERVICE_SEND_TEXT = "send_text"
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up System Bridge from a config entry."""
|
||||
bridge = Bridge(
|
||||
BridgeClient(aiohttp_client.async_get_clientsession(hass)),
|
||||
f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}",
|
||||
entry.data[CONF_API_KEY],
|
||||
)
|
||||
|
||||
# Check version before initialising
|
||||
version = Version(
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PORT],
|
||||
entry.data[CONF_API_KEY],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
await bridge.async_get_information()
|
||||
except BridgeAuthenticationException as exception:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Authentication failed for {entry.title} ({entry.data[CONF_HOST]})"
|
||||
) from exception
|
||||
except BRIDGE_CONNECTION_ERRORS as exception:
|
||||
if not await version.check_supported():
|
||||
raise ConfigEntryNotReady(
|
||||
f"You are not running a supported version of System Bridge. Please update to {SUPPORTED_VERSION} or higher."
|
||||
)
|
||||
except AuthenticationException as exception:
|
||||
_LOGGER.error("Authentication failed for %s: %s", entry.title, exception)
|
||||
raise ConfigEntryAuthFailed from exception
|
||||
except (ConnectionClosedException, ConnectionErrorException) as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})."
|
||||
) from exception
|
||||
except asyncio.TimeoutError as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})."
|
||||
) from exception
|
||||
|
||||
coordinator = SystemBridgeDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
entry=entry,
|
||||
)
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
await coordinator.async_get_data(MODULES)
|
||||
except AuthenticationException as exception:
|
||||
_LOGGER.error("Authentication failed for %s: %s", entry.title, exception)
|
||||
raise ConfigEntryAuthFailed from exception
|
||||
except (ConnectionClosedException, ConnectionErrorException) as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})."
|
||||
) from exception
|
||||
except asyncio.TimeoutError as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})."
|
||||
) from exception
|
||||
|
||||
coordinator = SystemBridgeDataUpdateCoordinator(hass, bridge, _LOGGER, entry=entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Wait for initial data
|
||||
_LOGGER.debug("Data: %s", coordinator.data)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(60):
|
||||
while (
|
||||
coordinator.bridge.battery is None
|
||||
or coordinator.bridge.cpu is None
|
||||
or coordinator.bridge.display is None
|
||||
or coordinator.bridge.filesystem is None
|
||||
or coordinator.bridge.graphics is None
|
||||
or coordinator.bridge.information is None
|
||||
or coordinator.bridge.memory is None
|
||||
or coordinator.bridge.network is None
|
||||
or coordinator.bridge.os is None
|
||||
or coordinator.bridge.processes is None
|
||||
or coordinator.bridge.system is None
|
||||
# Wait for initial data
|
||||
async with async_timeout.timeout(30):
|
||||
while coordinator.data is None or all(
|
||||
getattr(coordinator.data, module) is None for module in MODULES
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Waiting for initial data from %s (%s)",
|
||||
"Waiting for initial data from %s (%s): %s",
|
||||
entry.title,
|
||||
entry.data[CONF_HOST],
|
||||
coordinator.data,
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.TimeoutError as exception:
|
||||
|
@ -111,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND):
|
||||
if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL):
|
||||
return True
|
||||
|
||||
def valid_device(device: str):
|
||||
|
@ -129,104 +141,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
raise vol.Invalid from exception
|
||||
raise vol.Invalid(f"Device {device} does not exist")
|
||||
|
||||
async def handle_send_command(call: ServiceCall) -> None:
|
||||
"""Handle the send_command service call."""
|
||||
async def handle_open_path(call: ServiceCall) -> None:
|
||||
"""Handle the open path service call."""
|
||||
_LOGGER.info("Open: %s", call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
bridge: Bridge = coordinator.bridge
|
||||
await coordinator.websocket_client.open_path(call.data[CONF_PATH])
|
||||
|
||||
command = call.data[CONF_COMMAND]
|
||||
arguments = shlex.split(call.data[CONF_ARGUMENTS])
|
||||
|
||||
_LOGGER.debug(
|
||||
"Command payload: %s",
|
||||
{CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False},
|
||||
)
|
||||
try:
|
||||
response: CommandResponse = await bridge.async_send_command(
|
||||
{CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False}
|
||||
)
|
||||
if not response.success:
|
||||
raise HomeAssistantError(
|
||||
f"Error sending command. Response message was: {response.message}"
|
||||
)
|
||||
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
|
||||
raise HomeAssistantError("Error sending command") from exception
|
||||
_LOGGER.debug("Sent command. Response message was: %s", response.message)
|
||||
|
||||
async def handle_open(call: ServiceCall) -> None:
|
||||
"""Handle the open service call."""
|
||||
async def handle_open_url(call: ServiceCall) -> None:
|
||||
"""Handle the open url service call."""
|
||||
_LOGGER.info("Open: %s", call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
bridge: Bridge = coordinator.bridge
|
||||
|
||||
path = call.data[CONF_PATH]
|
||||
|
||||
_LOGGER.debug("Open payload: %s", {CONF_PATH: path})
|
||||
try:
|
||||
await bridge.async_open({CONF_PATH: path})
|
||||
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
|
||||
raise HomeAssistantError("Error sending") from exception
|
||||
_LOGGER.debug("Sent open request")
|
||||
await coordinator.websocket_client.open_url(call.data[CONF_URL])
|
||||
|
||||
async def handle_send_keypress(call: ServiceCall) -> None:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
bridge: Bridge = coordinator.data
|
||||
|
||||
keyboard_payload: KeyboardPayload = {
|
||||
CONF_KEY: call.data[CONF_KEY],
|
||||
CONF_MODIFIERS: shlex.split(call.data.get(CONF_MODIFIERS, "")),
|
||||
}
|
||||
|
||||
_LOGGER.debug("Keypress payload: %s", keyboard_payload)
|
||||
try:
|
||||
await bridge.async_send_keypress(keyboard_payload)
|
||||
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
|
||||
raise HomeAssistantError("Error sending") from exception
|
||||
_LOGGER.debug("Sent keypress request")
|
||||
await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY])
|
||||
|
||||
async def handle_send_text(call: ServiceCall) -> None:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
bridge: Bridge = coordinator.data
|
||||
|
||||
keyboard_payload: KeyboardPayload = {CONF_TEXT: call.data[CONF_TEXT]}
|
||||
|
||||
_LOGGER.debug("Text payload: %s", keyboard_payload)
|
||||
try:
|
||||
await bridge.async_send_keypress(keyboard_payload)
|
||||
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
|
||||
raise HomeAssistantError("Error sending") from exception
|
||||
_LOGGER.debug("Sent text request")
|
||||
await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
handle_send_command,
|
||||
SERVICE_OPEN_PATH,
|
||||
handle_open_path,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
vol.Optional(CONF_ARGUMENTS, ""): cv.string,
|
||||
vol.Required(CONF_PATH): cv.string,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_OPEN,
|
||||
handle_open,
|
||||
SERVICE_OPEN_URL,
|
||||
handle_open_url,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_PATH): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -239,7 +203,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
vol.Optional(CONF_MODIFIERS): cv.string,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -271,15 +234,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
]
|
||||
|
||||
# Ensure disconnected and cleanup stop sub
|
||||
await coordinator.bridge.async_close_websocket()
|
||||
await coordinator.websocket_client.close()
|
||||
if coordinator.unsub:
|
||||
coordinator.unsub()
|
||||
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_COMMAND)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_OPEN)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_OPEN_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_KEYPRESS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_TEXT)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
@ -295,20 +260,21 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]):
|
|||
def __init__(
|
||||
self,
|
||||
coordinator: SystemBridgeDataUpdateCoordinator,
|
||||
api_port: int,
|
||||
key: str,
|
||||
name: str | None,
|
||||
) -> None:
|
||||
"""Initialize the System Bridge entity."""
|
||||
super().__init__(coordinator)
|
||||
bridge: Bridge = coordinator.data
|
||||
self._key = f"{bridge.information.host}_{key}"
|
||||
self._name = f"{bridge.information.host} {name}"
|
||||
self._configuration_url = bridge.get_configuration_url()
|
||||
self._hostname = bridge.information.host
|
||||
self._mac = bridge.information.mac
|
||||
self._manufacturer = bridge.system.system.manufacturer
|
||||
self._model = bridge.system.system.model
|
||||
self._version = bridge.system.system.version
|
||||
|
||||
self._hostname = coordinator.data.system.hostname
|
||||
self._key = f"{self._hostname}_{key}"
|
||||
self._name = f"{self._hostname} {name}"
|
||||
self._configuration_url = (
|
||||
f"http://{self._hostname}:{api_port}/app/settings.html"
|
||||
)
|
||||
self._mac_address = coordinator.data.system.mac_address
|
||||
self._version = coordinator.data.system.version
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
|
@ -329,9 +295,7 @@ class SystemBridgeDeviceEntity(SystemBridgeEntity):
|
|||
"""Return device information about this System Bridge instance."""
|
||||
return DeviceInfo(
|
||||
configuration_url=self._configuration_url,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._mac)},
|
||||
manufacturer=self._manufacturer,
|
||||
model=self._model,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_address)},
|
||||
name=self._hostname,
|
||||
sw_version=self._version,
|
||||
)
|
||||
|
|
|
@ -4,14 +4,13 @@ from __future__ import annotations
|
|||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from systembridge import Bridge
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
|
@ -32,7 +31,7 @@ BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...]
|
|||
key="version_available",
|
||||
name="New Version Available",
|
||||
device_class=BinarySensorDeviceClass.UPDATE,
|
||||
value=lambda bridge: bridge.information.updates.available,
|
||||
value=lambda data: data.system.version_newer_available,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -41,7 +40,7 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ..
|
|||
key="battery_is_charging",
|
||||
name="Battery Is Charging",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
value=lambda bridge: bridge.battery.isCharging,
|
||||
value=lambda data: data.battery.is_charging,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -51,15 +50,24 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up System Bridge binary sensor based on a config entry."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
bridge: Bridge = coordinator.data
|
||||
|
||||
entities = []
|
||||
for description in BASE_BINARY_SENSOR_TYPES:
|
||||
entities.append(SystemBridgeBinarySensor(coordinator, description))
|
||||
entities.append(
|
||||
SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT])
|
||||
)
|
||||
|
||||
if bridge.battery and bridge.battery.hasBattery:
|
||||
if (
|
||||
coordinator.data.battery
|
||||
and coordinator.data.battery.percentage
|
||||
and coordinator.data.battery.percentage > -1
|
||||
):
|
||||
for description in BATTERY_BINARY_SENSOR_TYPES:
|
||||
entities.append(SystemBridgeBinarySensor(coordinator, description))
|
||||
entities.append(
|
||||
SystemBridgeBinarySensor(
|
||||
coordinator, description, entry.data[CONF_PORT]
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
@ -73,10 +81,12 @@ class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity):
|
|||
self,
|
||||
coordinator: SystemBridgeDataUpdateCoordinator,
|
||||
description: SystemBridgeBinarySensorEntityDescription,
|
||||
api_port: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
api_port,
|
||||
description.key,
|
||||
description.name,
|
||||
)
|
||||
|
@ -85,5 +95,4 @@ class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity):
|
|||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the boolean state of the binary sensor."""
|
||||
bridge: Bridge = self.coordinator.data
|
||||
return self.entity_description.value(bridge)
|
||||
return self.entity_description.value(self.coordinator.data)
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
"""Config flow for System Bridge integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
from systembridge import Bridge
|
||||
from systembridge.client import BridgeClient
|
||||
from systembridge.exceptions import BridgeAuthenticationException
|
||||
from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE
|
||||
from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
)
|
||||
from systembridgeconnector.websocket_client import WebSocketClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
|
@ -15,9 +20,10 @@ from homeassistant.components import zeroconf
|
|||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -31,39 +37,84 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
|
||||
async def validate_input(
|
||||
hass: HomeAssistant,
|
||||
data: dict[str, Any],
|
||||
) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
bridge = Bridge(
|
||||
BridgeClient(aiohttp_client.async_get_clientsession(hass)),
|
||||
f"http://{data[CONF_HOST]}:{data[CONF_PORT]}",
|
||||
host = data[CONF_HOST]
|
||||
|
||||
websocket_client = WebSocketClient(
|
||||
host,
|
||||
data[CONF_PORT],
|
||||
data[CONF_API_KEY],
|
||||
)
|
||||
|
||||
hostname = data[CONF_HOST]
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
await bridge.async_get_information()
|
||||
if (
|
||||
bridge.information is not None
|
||||
and bridge.information.host is not None
|
||||
and bridge.information.uuid is not None
|
||||
):
|
||||
hostname = bridge.information.host
|
||||
uuid = bridge.information.uuid
|
||||
except BridgeAuthenticationException as exception:
|
||||
_LOGGER.info(exception)
|
||||
await websocket_client.connect(session=async_get_clientsession(hass))
|
||||
await websocket_client.get_data(["system"])
|
||||
while True:
|
||||
message = await websocket_client.receive_message()
|
||||
_LOGGER.debug("Message: %s", message)
|
||||
if (
|
||||
message[EVENT_TYPE] == TYPE_DATA_UPDATE
|
||||
and message[EVENT_MODULE] == "system"
|
||||
):
|
||||
break
|
||||
except AuthenticationException as exception:
|
||||
_LOGGER.warning(
|
||||
"Authentication error when connecting to %s: %s", data[CONF_HOST], exception
|
||||
)
|
||||
raise InvalidAuth from exception
|
||||
except BRIDGE_CONNECTION_ERRORS as exception:
|
||||
_LOGGER.info(exception)
|
||||
except (
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
) as exception:
|
||||
_LOGGER.warning(
|
||||
"Connection error when connecting to %s: %s", data[CONF_HOST], exception
|
||||
)
|
||||
raise CannotConnect from exception
|
||||
except asyncio.TimeoutError as exception:
|
||||
_LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception)
|
||||
raise CannotConnect from exception
|
||||
|
||||
return {"hostname": hostname, "uuid": uuid}
|
||||
_LOGGER.debug("%s Message: %s", TYPE_DATA_UPDATE, message)
|
||||
|
||||
if "uuid" not in message["data"]:
|
||||
error = "No UUID in result!"
|
||||
raise CannotConnect(error)
|
||||
|
||||
return {"hostname": host, "uuid": message["data"]["uuid"]}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def _async_get_info(
|
||||
hass: HomeAssistant,
|
||||
user_input: dict[str, Any],
|
||||
) -> tuple[dict[str, str], dict[str, str] | None]:
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await validate_input(hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return errors, info
|
||||
|
||||
return errors, None
|
||||
|
||||
|
||||
class ConfigFlow(
|
||||
config_entries.ConfigFlow,
|
||||
domain=DOMAIN,
|
||||
):
|
||||
"""Handle a config flow for System Bridge."""
|
||||
|
||||
VERSION = 1
|
||||
|
@ -74,25 +125,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
self._input: dict[str, Any] = {}
|
||||
self._reauth = False
|
||||
|
||||
async def _async_get_info(
|
||||
self, user_input: dict[str, Any]
|
||||
) -> tuple[dict[str, str], dict[str, str] | None]:
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = 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
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return errors, info
|
||||
|
||||
return errors, None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
|
@ -102,7 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors, info = await self._async_get_info(user_input)
|
||||
errors, info = await _async_get_info(self.hass, user_input)
|
||||
if not errors and info is not None:
|
||||
# Check if already configured
|
||||
await self.async_set_unique_id(info["uuid"], raise_on_progress=False)
|
||||
|
@ -122,7 +154,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
if user_input is not None:
|
||||
user_input = {**self._input, **user_input}
|
||||
errors, info = await self._async_get_info(user_input)
|
||||
errors, info = await _async_get_info(self.hass, user_input)
|
||||
if not errors and info is not None:
|
||||
# Check if already configured
|
||||
existing_entry = await self.async_set_unique_id(info["uuid"])
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
"""Constants for the System Bridge integration."""
|
||||
import asyncio
|
||||
|
||||
from aiohttp.client_exceptions import (
|
||||
ClientConnectionError,
|
||||
ClientConnectorError,
|
||||
ClientResponseError,
|
||||
)
|
||||
from systembridge.exceptions import BridgeException
|
||||
|
||||
DOMAIN = "system_bridge"
|
||||
|
||||
BRIDGE_CONNECTION_ERRORS = (
|
||||
asyncio.TimeoutError,
|
||||
BridgeException,
|
||||
ClientConnectionError,
|
||||
ClientConnectorError,
|
||||
ClientResponseError,
|
||||
OSError,
|
||||
)
|
||||
MODULES = [
|
||||
"battery",
|
||||
"cpu",
|
||||
"disk",
|
||||
"display",
|
||||
"gpu",
|
||||
"memory",
|
||||
"system",
|
||||
]
|
||||
|
|
|
@ -6,40 +6,71 @@ from collections.abc import Callable
|
|||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from systembridge import Bridge
|
||||
from systembridge.exceptions import (
|
||||
BridgeAuthenticationException,
|
||||
BridgeConnectionClosedException,
|
||||
BridgeException,
|
||||
import async_timeout
|
||||
from pydantic import BaseModel # pylint: disable=no-name-in-module
|
||||
from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
)
|
||||
from systembridge.objects.events import Event
|
||||
from systembridgeconnector.models.battery import Battery
|
||||
from systembridgeconnector.models.cpu import Cpu
|
||||
from systembridgeconnector.models.disk import Disk
|
||||
from systembridgeconnector.models.display import Display
|
||||
from systembridgeconnector.models.gpu import Gpu
|
||||
from systembridgeconnector.models.memory import Memory
|
||||
from systembridgeconnector.models.system import System
|
||||
from systembridgeconnector.websocket_client import WebSocketClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN
|
||||
from .const import DOMAIN, MODULES
|
||||
|
||||
|
||||
class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]):
|
||||
class SystemBridgeCoordinatorData(BaseModel):
|
||||
"""System Bridge Coordianator Data."""
|
||||
|
||||
battery: Battery = None
|
||||
cpu: Cpu = None
|
||||
disk: Disk = None
|
||||
display: Display = None
|
||||
gpu: Gpu = None
|
||||
memory: Memory = None
|
||||
system: System = None
|
||||
|
||||
|
||||
class SystemBridgeDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[SystemBridgeCoordinatorData]
|
||||
):
|
||||
"""Class to manage fetching System Bridge data from single endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
bridge: Bridge,
|
||||
LOGGER: logging.Logger,
|
||||
*,
|
||||
entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize global System Bridge data updater."""
|
||||
self.bridge = bridge
|
||||
self.title = entry.title
|
||||
self.host = entry.data[CONF_HOST]
|
||||
self.unsub: Callable | None = None
|
||||
|
||||
self.systembridge_data = SystemBridgeCoordinatorData()
|
||||
self.websocket_client = WebSocketClient(
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PORT],
|
||||
entry.data[CONF_API_KEY],
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30)
|
||||
)
|
||||
|
@ -49,97 +80,117 @@ class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]):
|
|||
for update_callback in self._listeners:
|
||||
update_callback()
|
||||
|
||||
async def async_handle_event(self, event: Event):
|
||||
"""Handle System Bridge events from the WebSocket."""
|
||||
# No need to update anything, as everything is updated in the caller
|
||||
self.logger.debug(
|
||||
"New event from %s (%s): %s", self.title, self.host, event.name
|
||||
)
|
||||
self.async_set_updated_data(self.bridge)
|
||||
async def async_get_data(
|
||||
self,
|
||||
modules: list[str],
|
||||
) -> None:
|
||||
"""Get data from WebSocket."""
|
||||
if not self.websocket_client.connected:
|
||||
await self._setup_websocket()
|
||||
|
||||
async def _listen_for_events(self) -> None:
|
||||
await self.websocket_client.get_data(modules)
|
||||
|
||||
async def async_handle_module(
|
||||
self,
|
||||
module_name: str,
|
||||
module,
|
||||
) -> None:
|
||||
"""Handle data from the WebSocket client."""
|
||||
self.logger.debug("Set new data for: %s", module_name)
|
||||
setattr(self.systembridge_data, module_name, module)
|
||||
self.async_set_updated_data(self.systembridge_data)
|
||||
|
||||
async def _listen_for_data(self) -> None:
|
||||
"""Listen for events from the WebSocket."""
|
||||
|
||||
try:
|
||||
await self.bridge.async_send_event(
|
||||
"get-data",
|
||||
[
|
||||
{"service": "battery", "method": "findAll", "observe": True},
|
||||
{"service": "cpu", "method": "findAll", "observe": True},
|
||||
{"service": "display", "method": "findAll", "observe": True},
|
||||
{"service": "filesystem", "method": "findSizes", "observe": True},
|
||||
{"service": "graphics", "method": "findAll", "observe": True},
|
||||
{"service": "memory", "method": "findAll", "observe": True},
|
||||
{"service": "network", "method": "findAll", "observe": True},
|
||||
{"service": "os", "method": "findAll", "observe": False},
|
||||
{
|
||||
"service": "processes",
|
||||
"method": "findCurrentLoad",
|
||||
"observe": True,
|
||||
},
|
||||
{"service": "system", "method": "findAll", "observe": False},
|
||||
],
|
||||
)
|
||||
await self.bridge.listen_for_events(callback=self.async_handle_event)
|
||||
except BridgeConnectionClosedException as exception:
|
||||
await self.websocket_client.register_data_listener(MODULES)
|
||||
await self.websocket_client.listen(callback=self.async_handle_module)
|
||||
except AuthenticationException as exception:
|
||||
self.last_update_success = False
|
||||
self.logger.info(
|
||||
"Websocket Connection Closed for %s (%s). Will retry: %s",
|
||||
self.title,
|
||||
self.host,
|
||||
exception,
|
||||
)
|
||||
except BridgeException as exception:
|
||||
self.logger.error("Authentication failed for %s: %s", self.title, exception)
|
||||
if self.unsub:
|
||||
self.unsub()
|
||||
self.unsub = None
|
||||
self.last_update_success = False
|
||||
self.update_listeners()
|
||||
self.logger.warning(
|
||||
"Exception occurred for %s (%s). Will retry: %s",
|
||||
except (ConnectionClosedException, ConnectionResetError) as exception:
|
||||
self.logger.info(
|
||||
"Websocket connection closed for %s. Will retry: %s",
|
||||
self.title,
|
||||
self.host,
|
||||
exception,
|
||||
)
|
||||
if self.unsub:
|
||||
self.unsub()
|
||||
self.unsub = None
|
||||
self.last_update_success = False
|
||||
self.update_listeners()
|
||||
except ConnectionErrorException as exception:
|
||||
self.logger.warning(
|
||||
"Connection error occurred for %s. Will retry: %s",
|
||||
self.title,
|
||||
exception,
|
||||
)
|
||||
if self.unsub:
|
||||
self.unsub()
|
||||
self.unsub = None
|
||||
self.last_update_success = False
|
||||
self.update_listeners()
|
||||
|
||||
async def _setup_websocket(self) -> None:
|
||||
"""Use WebSocket for updates."""
|
||||
|
||||
try:
|
||||
self.logger.debug(
|
||||
"Connecting to ws://%s:%s",
|
||||
self.host,
|
||||
self.bridge.information.websocketPort,
|
||||
)
|
||||
await self.bridge.async_connect_websocket(
|
||||
self.host, self.bridge.information.websocketPort
|
||||
)
|
||||
except BridgeAuthenticationException as exception:
|
||||
async with async_timeout.timeout(20):
|
||||
await self.websocket_client.connect(
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
except AuthenticationException as exception:
|
||||
self.last_update_success = False
|
||||
self.logger.error("Authentication failed for %s: %s", self.title, exception)
|
||||
if self.unsub:
|
||||
self.unsub()
|
||||
self.unsub = None
|
||||
raise ConfigEntryAuthFailed() from exception
|
||||
except (*BRIDGE_CONNECTION_ERRORS, ConnectionRefusedError) as exception:
|
||||
if self.unsub:
|
||||
self.unsub()
|
||||
self.unsub = None
|
||||
raise UpdateFailed(
|
||||
f"Could not connect to {self.title} ({self.host})."
|
||||
) from exception
|
||||
asyncio.create_task(self._listen_for_events())
|
||||
self.last_update_success = False
|
||||
self.update_listeners()
|
||||
except ConnectionErrorException as exception:
|
||||
self.logger.warning(
|
||||
"Connection error occurred for %s. Will retry: %s",
|
||||
self.title,
|
||||
exception,
|
||||
)
|
||||
self.last_update_success = False
|
||||
self.update_listeners()
|
||||
except asyncio.TimeoutError as exception:
|
||||
self.logger.warning(
|
||||
"Timed out waiting for %s. Will retry: %s",
|
||||
self.title,
|
||||
exception,
|
||||
)
|
||||
self.last_update_success = False
|
||||
self.update_listeners()
|
||||
|
||||
self.hass.async_create_task(self._listen_for_data())
|
||||
self.last_update_success = True
|
||||
self.update_listeners()
|
||||
|
||||
async def close_websocket(_) -> None:
|
||||
"""Close WebSocket connection."""
|
||||
await self.bridge.async_close_websocket()
|
||||
await self.websocket_client.close()
|
||||
|
||||
# Clean disconnect WebSocket on Home Assistant shutdown
|
||||
self.unsub = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, close_websocket
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> Bridge:
|
||||
async def _async_update_data(self) -> SystemBridgeCoordinatorData:
|
||||
"""Update System Bridge data from WebSocket."""
|
||||
self.logger.debug(
|
||||
"_async_update_data - WebSocket Connected: %s",
|
||||
self.bridge.websocket_connected,
|
||||
self.websocket_client.connected,
|
||||
)
|
||||
if not self.bridge.websocket_connected:
|
||||
if not self.websocket_client.connected:
|
||||
await self._setup_websocket()
|
||||
|
||||
return self.bridge
|
||||
self.logger.debug("_async_update_data done")
|
||||
|
||||
return self.systembridge_data
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
"name": "System Bridge",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/system_bridge",
|
||||
"requirements": ["systembridge==2.3.1"],
|
||||
"requirements": ["systembridgeconnector==3.1.3"],
|
||||
"codeowners": ["@timmo001"],
|
||||
"zeroconf": ["_system-bridge._udp.local."],
|
||||
"zeroconf": ["_system-bridge._tcp.local."],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["systembridge"]
|
||||
"loggers": ["systembridgeconnector"]
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|||
from datetime import datetime, timedelta
|
||||
from typing import Final, cast
|
||||
|
||||
from systembridge import Bridge
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
|
@ -16,6 +14,7 @@ from homeassistant.components.sensor import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
DATA_GIGABYTES,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
FREQUENCY_GIGAHERTZ,
|
||||
|
@ -28,10 +27,11 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import SystemBridgeDeviceEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator
|
||||
|
||||
ATTR_AVAILABLE: Final = "available"
|
||||
ATTR_FILESYSTEM: Final = "filesystem"
|
||||
|
@ -41,6 +41,7 @@ ATTR_TYPE: Final = "type"
|
|||
ATTR_USED: Final = "used"
|
||||
|
||||
PIXELS: Final = "px"
|
||||
RPM: Final = "RPM"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -50,21 +51,87 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription):
|
|||
value: Callable = round
|
||||
|
||||
|
||||
def battery_time_remaining(data: SystemBridgeCoordinatorData) -> datetime | None:
|
||||
"""Return the battery time remaining."""
|
||||
if data.battery.sensors_secsleft is not None:
|
||||
return utcnow() + timedelta(seconds=data.battery.sensors_secsleft)
|
||||
return None
|
||||
|
||||
|
||||
def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None:
|
||||
"""Return the CPU speed."""
|
||||
if data.cpu.frequency_current is not None:
|
||||
return round(data.cpu.frequency_current / 1000, 2)
|
||||
return None
|
||||
|
||||
|
||||
def gpu_core_clock_speed(data: SystemBridgeCoordinatorData, key: str) -> float | None:
|
||||
"""Return the GPU core clock speed."""
|
||||
if getattr(data.gpu, f"{key}_core_clock") is not None:
|
||||
return round(getattr(data.gpu, f"{key}_core_clock"))
|
||||
return None
|
||||
|
||||
|
||||
def gpu_memory_clock_speed(data: SystemBridgeCoordinatorData, key: str) -> float | None:
|
||||
"""Return the GPU memory clock speed."""
|
||||
if getattr(data.gpu, f"{key}_memory_clock") is not None:
|
||||
return round(getattr(data.gpu, f"{key}_memory_clock"))
|
||||
return None
|
||||
|
||||
|
||||
def gpu_memory_free(data: SystemBridgeCoordinatorData, key: str) -> float | None:
|
||||
"""Return the free GPU memory."""
|
||||
if getattr(data.gpu, f"{key}_memory_free") is not None:
|
||||
return round(getattr(data.gpu, f"{key}_memory_free") / 10**3, 2)
|
||||
return None
|
||||
|
||||
|
||||
def gpu_memory_used(data: SystemBridgeCoordinatorData, key: str) -> float | None:
|
||||
"""Return the used GPU memory."""
|
||||
if getattr(data.gpu, f"{key}_memory_used") is not None:
|
||||
return round(getattr(data.gpu, f"{key}_memory_used") / 10**3, 2)
|
||||
return None
|
||||
|
||||
|
||||
def gpu_memory_used_percentage(
|
||||
data: SystemBridgeCoordinatorData, key: str
|
||||
) -> float | None:
|
||||
"""Return the used GPU memory percentage."""
|
||||
if (
|
||||
getattr(data.gpu, f"{key}_memory_used") is not None
|
||||
and getattr(data.gpu, f"{key}_memory_total") is not None
|
||||
):
|
||||
return round(
|
||||
getattr(data.gpu, f"{key}_memory_used")
|
||||
/ getattr(data.gpu, f"{key}_memory_total")
|
||||
* 100,
|
||||
2,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def memory_free(data: SystemBridgeCoordinatorData) -> float | None:
|
||||
"""Return the free memory."""
|
||||
if data.memory.virtual_free is not None:
|
||||
return round(data.memory.virtual_free / 1000**3, 2)
|
||||
return None
|
||||
|
||||
|
||||
def memory_used(data: SystemBridgeCoordinatorData) -> float | None:
|
||||
"""Return the used memory."""
|
||||
if data.memory.virtual_used is not None:
|
||||
return round(data.memory.virtual_used / 1000**3, 2)
|
||||
return None
|
||||
|
||||
|
||||
BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="bios_version",
|
||||
name="BIOS Version",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:chip",
|
||||
value=lambda bridge: bridge.system.bios.version,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="cpu_speed",
|
||||
name="CPU Speed",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=FREQUENCY_GIGAHERTZ,
|
||||
icon="mdi:speedometer",
|
||||
value=lambda bridge: bridge.cpu.currentSpeed.avg,
|
||||
value=cpu_speed,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="cpu_temperature",
|
||||
|
@ -73,7 +140,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda bridge: bridge.cpu.temperature.main,
|
||||
value=lambda data: data.cpu.temperature,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="cpu_voltage",
|
||||
|
@ -82,21 +149,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
value=lambda bridge: bridge.cpu.cpu.voltage,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="displays_connected",
|
||||
name="Displays Connected",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:monitor",
|
||||
value=lambda bridge: len(bridge.display.displays),
|
||||
value=lambda data: data.cpu.voltage,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="kernel",
|
||||
name="Kernel",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:devices",
|
||||
value=lambda bridge: bridge.os.kernel,
|
||||
value=lambda data: data.system.platform,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="memory_free",
|
||||
|
@ -104,7 +164,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:memory",
|
||||
value=lambda bridge: round(bridge.memory.free / 1000**3, 2),
|
||||
value=memory_free,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="memory_used_percentage",
|
||||
|
@ -112,7 +172,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:memory",
|
||||
value=lambda bridge: round((bridge.memory.used / bridge.memory.total) * 100, 2),
|
||||
value=lambda data: data.memory.virtual_percent,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="memory_used",
|
||||
|
@ -121,14 +181,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:memory",
|
||||
value=lambda bridge: round(bridge.memory.used / 1000**3, 2),
|
||||
value=memory_used,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="os",
|
||||
name="Operating System",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:devices",
|
||||
value=lambda bridge: f"{bridge.os.distro} {bridge.os.release}",
|
||||
value=lambda data: f"{data.system.platform} {data.system.platform_version}",
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="processes_load",
|
||||
|
@ -136,46 +196,19 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge: round(bridge.processes.load.currentLoad, 2),
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="processes_load_idle",
|
||||
name="Idle Load",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge: round(bridge.processes.load.currentLoadIdle, 2),
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="processes_load_system",
|
||||
name="System Load",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge: round(bridge.processes.load.currentLoadSystem, 2),
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="processes_load_user",
|
||||
name="User Load",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge: round(bridge.processes.load.currentLoadUser, 2),
|
||||
value=lambda data: data.cpu.usage,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="version",
|
||||
name="Version",
|
||||
icon="mdi:counter",
|
||||
value=lambda bridge: bridge.information.version,
|
||||
value=lambda data: data.system.version,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="version_latest",
|
||||
name="Latest Version",
|
||||
icon="mdi:counter",
|
||||
value=lambda bridge: bridge.information.updates.version.new,
|
||||
value=lambda data: data.system.version_latest,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -186,238 +219,270 @@ BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value=lambda bridge: bridge.battery.percent,
|
||||
value=lambda data: data.battery.percentage,
|
||||
),
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="battery_time_remaining",
|
||||
name="Battery Time Remaining",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda bridge: str(
|
||||
datetime.now() + timedelta(minutes=bridge.battery.timeRemaining)
|
||||
),
|
||||
value=battery_time_remaining,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up System Bridge sensor based on a config entry."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities = []
|
||||
for description in BASE_SENSOR_TYPES:
|
||||
entities.append(SystemBridgeSensor(coordinator, description))
|
||||
entities.append(
|
||||
SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT])
|
||||
)
|
||||
|
||||
for key, _ in coordinator.data.filesystem.fsSize.items():
|
||||
uid = key.replace(":", "")
|
||||
for partition in coordinator.data.disk.partitions:
|
||||
entities.append(
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"filesystem_{uid}",
|
||||
name=f"{key} Space Used",
|
||||
key=f"filesystem_{partition.replace(':', '')}",
|
||||
name=f"{partition} Space Used",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:harddisk",
|
||||
value=lambda bridge, i=key: round(
|
||||
bridge.filesystem.fsSize[i]["use"], 2
|
||||
value=lambda data, p=partition: getattr(
|
||||
data.disk, f"usage_{p}_percent"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
)
|
||||
)
|
||||
|
||||
if coordinator.data.battery.hasBattery:
|
||||
if (
|
||||
coordinator.data.battery
|
||||
and coordinator.data.battery.percentage
|
||||
and coordinator.data.battery.percentage > -1
|
||||
):
|
||||
for description in BATTERY_SENSOR_TYPES:
|
||||
entities.append(SystemBridgeSensor(coordinator, description))
|
||||
entities.append(
|
||||
SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT])
|
||||
)
|
||||
|
||||
for index, _ in enumerate(coordinator.data.display.displays):
|
||||
name = index + 1
|
||||
displays = []
|
||||
for display in coordinator.data.display.displays:
|
||||
displays.append(
|
||||
{
|
||||
"key": display,
|
||||
"name": getattr(coordinator.data.display, f"{display}_name").replace(
|
||||
"Display ", ""
|
||||
),
|
||||
},
|
||||
)
|
||||
display_count = len(displays)
|
||||
|
||||
entities.append(
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key="displays_connected",
|
||||
name="Displays Connected",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:monitor",
|
||||
value=lambda _, count=display_count: count,
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
)
|
||||
)
|
||||
|
||||
for _, display in enumerate(displays):
|
||||
entities = [
|
||||
*entities,
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"display_{name}_resolution_x",
|
||||
name=f"Display {name} Resolution X",
|
||||
key=f"display_{display['name']}_resolution_x",
|
||||
name=f"Display {display['name']} Resolution X",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PIXELS,
|
||||
icon="mdi:monitor",
|
||||
value=lambda bridge, i=index: bridge.display.displays[
|
||||
i
|
||||
].resolutionX,
|
||||
value=lambda data, k=display["key"]: getattr(
|
||||
data.display, f"{k}_resolution_horizontal"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"display_{name}_resolution_y",
|
||||
name=f"Display {name} Resolution Y",
|
||||
key=f"display_{display['name']}_resolution_y",
|
||||
name=f"Display {display['name']} Resolution Y",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PIXELS,
|
||||
icon="mdi:monitor",
|
||||
value=lambda bridge, i=index: bridge.display.displays[
|
||||
i
|
||||
].resolutionY,
|
||||
value=lambda data, k=display["key"]: getattr(
|
||||
data.display, f"{k}_resolution_vertical"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"display_{name}_refresh_rate",
|
||||
name=f"Display {name} Refresh Rate",
|
||||
key=f"display_{display['name']}_refresh_rate",
|
||||
name=f"Display {display['name']} Refresh Rate",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=FREQUENCY_HERTZ,
|
||||
icon="mdi:monitor",
|
||||
value=lambda bridge, i=index: bridge.display.displays[
|
||||
i
|
||||
].currentRefreshRate,
|
||||
value=lambda data, k=display["key"]: getattr(
|
||||
data.display, f"{k}_refresh_rate"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
]
|
||||
|
||||
for index, _ in enumerate(coordinator.data.graphics.controllers):
|
||||
if coordinator.data.graphics.controllers[index].name is not None:
|
||||
# Remove vendor from name
|
||||
name = (
|
||||
coordinator.data.graphics.controllers[index]
|
||||
.name.replace(coordinator.data.graphics.controllers[index].vendor, "")
|
||||
.strip()
|
||||
)
|
||||
entities = [
|
||||
*entities,
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_core_clock_speed",
|
||||
name=f"{name} Clock Speed",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
||||
icon="mdi:speedometer",
|
||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
||||
i
|
||||
].clockCore,
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_clock_speed",
|
||||
name=f"{name} Memory Clock Speed",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
||||
icon="mdi:speedometer",
|
||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
||||
i
|
||||
].clockMemory,
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_free",
|
||||
name=f"{name} Memory Free",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:memory",
|
||||
value=lambda bridge, i=index: round(
|
||||
bridge.graphics.controllers[i].memoryFree / 10**3, 2
|
||||
),
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_used_percentage",
|
||||
name=f"{name} Memory Used %",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:memory",
|
||||
value=lambda bridge, i=index: round(
|
||||
(
|
||||
bridge.graphics.controllers[i].memoryUsed
|
||||
/ bridge.graphics.controllers[i].memoryTotal
|
||||
)
|
||||
* 100,
|
||||
2,
|
||||
),
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_used",
|
||||
name=f"{name} Memory Used",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:memory",
|
||||
value=lambda bridge, i=index: round(
|
||||
bridge.graphics.controllers[i].memoryUsed / 10**3, 2
|
||||
),
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_fan_speed",
|
||||
name=f"{name} Fan Speed",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:fan",
|
||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
||||
i
|
||||
].fanSpeed,
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_power_usage",
|
||||
name=f"{name} Power Usage",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
||||
i
|
||||
].powerDraw,
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_temperature",
|
||||
name=f"{name} Temperature",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
||||
i
|
||||
].temperatureGpu,
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_usage_percentage",
|
||||
name=f"{name} Usage %",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
||||
i
|
||||
].utilizationGpu,
|
||||
),
|
||||
),
|
||||
]
|
||||
gpus = []
|
||||
for gpu in coordinator.data.gpu.gpus:
|
||||
gpus.append(
|
||||
{
|
||||
"key": gpu,
|
||||
"name": getattr(coordinator.data.gpu, f"{gpu}_name"),
|
||||
},
|
||||
)
|
||||
|
||||
for index, _ in enumerate(coordinator.data.processes.load.cpus):
|
||||
for index, gpu in enumerate(gpus):
|
||||
entities = [
|
||||
*entities,
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_core_clock_speed",
|
||||
name=f"{gpu['name']} Clock Speed",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
||||
icon="mdi:speedometer",
|
||||
value=lambda data, k=gpu["key"]: gpu_core_clock_speed(data, k),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_clock_speed",
|
||||
name=f"{gpu['name']} Memory Clock Speed",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
||||
icon="mdi:speedometer",
|
||||
value=lambda data, k=gpu["key"]: gpu_memory_clock_speed(data, k),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_free",
|
||||
name=f"{gpu['name']} Memory Free",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:memory",
|
||||
value=lambda data, k=gpu["key"]: gpu_memory_free(data, k),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_used_percentage",
|
||||
name=f"{gpu['name']} Memory Used %",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:memory",
|
||||
value=lambda data, k=gpu["key"]: gpu_memory_used_percentage(
|
||||
data, k
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_memory_used",
|
||||
name=f"{gpu['name']} Memory Used",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:memory",
|
||||
value=lambda data, k=gpu["key"]: gpu_memory_used(data, k),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_fan_speed",
|
||||
name=f"{gpu['name']} Fan Speed",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=RPM,
|
||||
icon="mdi:fan",
|
||||
value=lambda data, k=gpu["key"]: getattr(
|
||||
data.gpu, f"{k}_fan_speed"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_power_usage",
|
||||
name=f"{gpu['name']} Power Usage",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
value=lambda data, k=gpu["key"]: getattr(data.gpu, f"{k}_power"),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_temperature",
|
||||
name=f"{gpu['name']} Temperature",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda data, k=gpu["key"]: getattr(
|
||||
data.gpu, f"{k}_temperature"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"gpu_{index}_usage_percentage",
|
||||
name=f"{gpu['name']} Usage %",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda data, k=gpu["key"]: getattr(
|
||||
data.gpu, f"{k}_core_load"
|
||||
),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
]
|
||||
|
||||
for index in range(coordinator.data.cpu.count):
|
||||
entities = [
|
||||
*entities,
|
||||
SystemBridgeSensor(
|
||||
|
@ -429,52 +494,9 @@ async def async_setup_entry(
|
|||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge, index=index: round(
|
||||
bridge.processes.load.cpus[index].load, 2
|
||||
),
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"processes_load_cpu_{index}_idle",
|
||||
name=f"Idle Load CPU {index}",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge, index=index: round(
|
||||
bridge.processes.load.cpus[index].loadIdle, 2
|
||||
),
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"processes_load_cpu_{index}_system",
|
||||
name=f"System Load CPU {index}",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge, index=index: round(
|
||||
bridge.processes.load.cpus[index].loadSystem, 2
|
||||
),
|
||||
),
|
||||
),
|
||||
SystemBridgeSensor(
|
||||
coordinator,
|
||||
SystemBridgeSensorEntityDescription(
|
||||
key=f"processes_load_cpu_{index}_user",
|
||||
name=f"User Load CPU {index}",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
value=lambda bridge, index=index: round(
|
||||
bridge.processes.load.cpus[index].loadUser, 2
|
||||
),
|
||||
value=lambda data, k=index: getattr(data.cpu, f"usage_{k}"),
|
||||
),
|
||||
entry.data[CONF_PORT],
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -490,10 +512,12 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity):
|
|||
self,
|
||||
coordinator: SystemBridgeDataUpdateCoordinator,
|
||||
description: SystemBridgeSensorEntityDescription,
|
||||
api_port: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
api_port,
|
||||
description.key,
|
||||
description.name,
|
||||
)
|
||||
|
@ -502,8 +526,7 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity):
|
|||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state."""
|
||||
bridge: Bridge = self.coordinator.data
|
||||
try:
|
||||
return cast(StateType, self.entity_description.value(bridge))
|
||||
return cast(StateType, self.entity_description.value(self.coordinator.data))
|
||||
except TypeError:
|
||||
return None
|
||||
|
|
|
@ -1,32 +1,6 @@
|
|||
send_command:
|
||||
name: Send Command
|
||||
description: Sends a command to the server to run.
|
||||
fields:
|
||||
bridge:
|
||||
name: Bridge
|
||||
description: The server to send the command to.
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: system_bridge
|
||||
command:
|
||||
name: Command
|
||||
description: Command to send to the server.
|
||||
required: true
|
||||
example: "echo"
|
||||
selector:
|
||||
text:
|
||||
arguments:
|
||||
name: Arguments
|
||||
description: Arguments to send to the server.
|
||||
required: false
|
||||
default: ""
|
||||
example: "hello"
|
||||
selector:
|
||||
text:
|
||||
open:
|
||||
name: Open Path/URL
|
||||
description: Open a URL or file on the server using the default application.
|
||||
open_path:
|
||||
name: Open Path
|
||||
description: Open a file on the server using the default application.
|
||||
fields:
|
||||
bridge:
|
||||
name: Bridge
|
||||
|
@ -36,8 +10,26 @@ open:
|
|||
device:
|
||||
integration: system_bridge
|
||||
path:
|
||||
name: Path/URL
|
||||
description: Path/URL to open.
|
||||
name: Path
|
||||
description: Path to open.
|
||||
required: true
|
||||
example: "C:\\test\\image.png"
|
||||
selector:
|
||||
text:
|
||||
open_url:
|
||||
name: Open URL
|
||||
description: Open a URL on the server using the default application.
|
||||
fields:
|
||||
bridge:
|
||||
name: Bridge
|
||||
description: The server to talk to.
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: system_bridge
|
||||
url:
|
||||
name: URL
|
||||
description: URL to open.
|
||||
required: true
|
||||
example: "https://www.home-assistant.io"
|
||||
selector:
|
||||
|
@ -60,16 +52,6 @@ send_keypress:
|
|||
example: "audio_play"
|
||||
selector:
|
||||
text:
|
||||
modifiers:
|
||||
name: Modifiers
|
||||
description: "List of modifier(s). Accepts alt, command/win, control, and shift."
|
||||
required: false
|
||||
default: ""
|
||||
example:
|
||||
- "control"
|
||||
- "shift"
|
||||
selector:
|
||||
text:
|
||||
send_text:
|
||||
name: Send Keyboard Text
|
||||
description: Sends text for the server to type.
|
||||
|
|
|
@ -370,7 +370,7 @@ ZEROCONF = {
|
|||
"name": "smappee50*"
|
||||
}
|
||||
],
|
||||
"_system-bridge._udp.local.": [
|
||||
"_system-bridge._tcp.local.": [
|
||||
{
|
||||
"domain": "system_bridge"
|
||||
}
|
||||
|
|
|
@ -2271,7 +2271,7 @@ swisshydrodata==0.1.0
|
|||
synology-srm==0.2.0
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridge==2.3.1
|
||||
systembridgeconnector==3.1.3
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.2.0
|
||||
|
|
|
@ -1498,7 +1498,7 @@ sunwatcher==0.2.1
|
|||
surepy==0.7.2
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridge==2.3.1
|
||||
systembridgeconnector==3.1.3
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.2.0
|
||||
|
|
|
@ -1,13 +1,28 @@
|
|||
"""Test the System Bridge config flow."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError
|
||||
from systembridge.exceptions import BridgeAuthenticationException
|
||||
from systembridgeconnector.const import (
|
||||
EVENT_DATA,
|
||||
EVENT_MESSAGE,
|
||||
EVENT_MODULE,
|
||||
EVENT_SUBTYPE,
|
||||
EVENT_TYPE,
|
||||
SUBTYPE_BAD_API_KEY,
|
||||
TYPE_DATA_UPDATE,
|
||||
TYPE_ERROR,
|
||||
)
|
||||
from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.system_bridge.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -29,7 +44,7 @@ FIXTURE_ZEROCONF_INPUT = {
|
|||
}
|
||||
|
||||
FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo(
|
||||
host="1.1.1.1",
|
||||
host="test-bridge",
|
||||
addresses=["1.1.1.1"],
|
||||
port=9170,
|
||||
hostname="test-bridge.local.",
|
||||
|
@ -58,37 +73,40 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo(
|
|||
},
|
||||
)
|
||||
|
||||
|
||||
FIXTURE_INFORMATION = {
|
||||
"address": "http://test-bridge:9170",
|
||||
"apiPort": 9170,
|
||||
"fqdn": "test-bridge",
|
||||
"host": "test-bridge",
|
||||
"ip": "1.1.1.1",
|
||||
"mac": FIXTURE_MAC_ADDRESS,
|
||||
"updates": {
|
||||
"available": False,
|
||||
"newer": False,
|
||||
"url": "https://github.com/timmo001/system-bridge/releases/tag/v2.3.2",
|
||||
"version": {"current": "2.3.2", "new": "2.3.2"},
|
||||
FIXTURE_DATA_SYSTEM = {
|
||||
EVENT_TYPE: TYPE_DATA_UPDATE,
|
||||
EVENT_MESSAGE: "Data changed",
|
||||
EVENT_MODULE: "system",
|
||||
EVENT_DATA: {
|
||||
"uuid": FIXTURE_UUID,
|
||||
},
|
||||
"uuid": FIXTURE_UUID,
|
||||
"version": "2.3.2",
|
||||
"websocketAddress": "ws://test-bridge:9172",
|
||||
"websocketPort": 9172,
|
||||
}
|
||||
|
||||
FIXTURE_DATA_SYSTEM_BAD = {
|
||||
EVENT_TYPE: TYPE_DATA_UPDATE,
|
||||
EVENT_MESSAGE: "Data changed",
|
||||
EVENT_MODULE: "system",
|
||||
EVENT_DATA: {},
|
||||
}
|
||||
|
||||
FIXTURE_DATA_AUTH_ERROR = {
|
||||
EVENT_TYPE: TYPE_ERROR,
|
||||
EVENT_SUBTYPE: SUBTYPE_BAD_API_KEY,
|
||||
EVENT_MESSAGE: "Invalid api-key",
|
||||
}
|
||||
|
||||
|
||||
FIXTURE_BASE_URL = (
|
||||
f"http://{FIXTURE_USER_INPUT[CONF_HOST]}:{FIXTURE_USER_INPUT[CONF_PORT]}"
|
||||
)
|
||||
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the setup form is served."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
FIXTURE_ZEROCONF_BASE_URL = f"http://{FIXTURE_ZEROCONF.host}:{FIXTURE_ZEROCONF.port}"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_user_flow(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_user_flow(hass: HomeAssistant) -> None:
|
||||
"""Test full user flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
|
@ -97,20 +115,19 @@ async def test_user_flow(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{FIXTURE_BASE_URL}/information",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=FIXTURE_INFORMATION,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.system_bridge.config_flow.WebSocketClient.connect"
|
||||
), patch("systembridgeconnector.websocket_client.WebSocketClient.get_data"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
return_value=FIXTURE_DATA_SYSTEM,
|
||||
), patch(
|
||||
"homeassistant.components.system_bridge.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["title"] == "test-bridge"
|
||||
|
@ -118,34 +135,7 @@ async def test_user_flow(
|
|||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
|
@ -154,11 +144,13 @@ async def test_form_cannot_connect(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
with patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.connect",
|
||||
side_effect=ConnectionErrorException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
@ -166,10 +158,8 @@ async def test_form_cannot_connect(
|
|||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unknown_error(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
"""Test we handle unknown error."""
|
||||
async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle connection closed cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
@ -177,23 +167,123 @@ async def test_form_unknown_error(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.system_bridge.config_flow.Bridge.async_get_information",
|
||||
side_effect=Exception("Boom"),
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
side_effect=ConnectionClosedException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle timeout cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
side_effect=asyncio.TimeoutError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
side_effect=AuthenticationException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_uuid_error(hass: HomeAssistant) -> None:
|
||||
"""Test we handle error from bad uuid."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
return_value=FIXTURE_DATA_SYSTEM_BAD,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test we handle unknown errors."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_reauth_authorization_error(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
|
||||
"""Test we show user form on authorization error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||
|
@ -202,13 +292,15 @@ async def test_reauth_authorization_error(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "authenticate"
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
side_effect=AuthenticationException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
@ -216,9 +308,7 @@ async def test_reauth_authorization_error(
|
|||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_reauth_connection_error(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_reauth_connection_error(hass: HomeAssistant) -> None:
|
||||
"""Test we show user form on connection error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||
|
@ -227,11 +317,13 @@ async def test_reauth_connection_error(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "authenticate"
|
||||
|
||||
aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
with patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.connect",
|
||||
side_effect=ConnectionErrorException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
@ -239,9 +331,32 @@ async def test_reauth_connection_error(
|
|||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_reauth_flow(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None:
|
||||
"""Test we show user form on connection error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "authenticate"
|
||||
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
side_effect=ConnectionClosedException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "authenticate"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_reauth_flow(hass: HomeAssistant) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_config = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id=FIXTURE_UUID, data=FIXTURE_USER_INPUT
|
||||
|
@ -255,20 +370,19 @@ async def test_reauth_flow(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "authenticate"
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{FIXTURE_BASE_URL}/information",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=FIXTURE_INFORMATION,
|
||||
)
|
||||
|
||||
with patch(
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
return_value=FIXTURE_DATA_SYSTEM,
|
||||
), patch(
|
||||
"homeassistant.components.system_bridge.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
@ -276,9 +390,7 @@ async def test_reauth_flow(
|
|||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_zeroconf_flow(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_zeroconf_flow(hass: HomeAssistant) -> None:
|
||||
"""Test zeroconf flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -290,30 +402,27 @@ async def test_zeroconf_flow(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert not result["errors"]
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{FIXTURE_ZEROCONF_BASE_URL}/information",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=FIXTURE_INFORMATION,
|
||||
)
|
||||
|
||||
with patch(
|
||||
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||
), patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||
return_value=FIXTURE_DATA_SYSTEM,
|
||||
), patch(
|
||||
"homeassistant.components.system_bridge.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["title"] == "test-bridge"
|
||||
assert result2["title"] == "1.1.1.1"
|
||||
assert result2["data"] == FIXTURE_ZEROCONF_INPUT
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_zeroconf_cannot_connect(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test zeroconf cannot connect flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -325,13 +434,13 @@ async def test_zeroconf_cannot_connect(
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert not result["errors"]
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{FIXTURE_ZEROCONF_BASE_URL}/information", exc=ClientConnectionError
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
with patch(
|
||||
"systembridgeconnector.websocket_client.WebSocketClient.connect",
|
||||
side_effect=ConnectionErrorException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
@ -339,9 +448,7 @@ async def test_zeroconf_cannot_connect(
|
|||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_zeroconf_bad_zeroconf_info(
|
||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
||||
) -> None:
|
||||
async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None:
|
||||
"""Test zeroconf cannot connect flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
|
Loading…
Add table
Reference in a new issue