mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00
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
@ -3,102 +3,114 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
from systembridge import Bridge
|
from systembridgeconnector.exceptions import (
|
||||||
from systembridge.client import BridgeClient
|
AuthenticationException,
|
||||||
from systembridge.exceptions import BridgeAuthenticationException
|
ConnectionClosedException,
|
||||||
from systembridge.objects.command.response import CommandResponse
|
ConnectionErrorException,
|
||||||
from systembridge.objects.keyboard.payload import KeyboardPayload
|
)
|
||||||
|
from systembridgeconnector.version import SUPPORTED_VERSION, Version
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_COMMAND,
|
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PATH,
|
CONF_PATH,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_URL,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
ConfigEntryAuthFailed,
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
ConfigEntryNotReady,
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
HomeAssistantError,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers import (
|
|
||||||
aiohttp_client,
|
|
||||||
config_validation as cv,
|
|
||||||
device_registry as dr,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN
|
from .const import DOMAIN, MODULES
|
||||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS = [
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.SENSOR,
|
||||||
|
]
|
||||||
|
|
||||||
CONF_ARGUMENTS = "arguments"
|
|
||||||
CONF_BRIDGE = "bridge"
|
CONF_BRIDGE = "bridge"
|
||||||
CONF_KEY = "key"
|
CONF_KEY = "key"
|
||||||
CONF_MODIFIERS = "modifiers"
|
|
||||||
CONF_TEXT = "text"
|
CONF_TEXT = "text"
|
||||||
CONF_WAIT = "wait"
|
|
||||||
|
|
||||||
SERVICE_SEND_COMMAND = "send_command"
|
SERVICE_OPEN_PATH = "open_path"
|
||||||
SERVICE_OPEN = "open"
|
SERVICE_OPEN_URL = "open_url"
|
||||||
SERVICE_SEND_KEYPRESS = "send_keypress"
|
SERVICE_SEND_KEYPRESS = "send_keypress"
|
||||||
SERVICE_SEND_TEXT = "send_text"
|
SERVICE_SEND_TEXT = "send_text"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up System Bridge from a config entry."""
|
"""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:
|
try:
|
||||||
async with async_timeout.timeout(30):
|
if not await version.check_supported():
|
||||||
await bridge.async_get_information()
|
raise ConfigEntryNotReady(
|
||||||
except BridgeAuthenticationException as exception:
|
f"You are not running a supported version of System Bridge. Please update to {SUPPORTED_VERSION} or higher."
|
||||||
raise ConfigEntryAuthFailed(
|
)
|
||||||
f"Authentication failed for {entry.title} ({entry.data[CONF_HOST]})"
|
except AuthenticationException as exception:
|
||||||
) from exception
|
_LOGGER.error("Authentication failed for %s: %s", entry.title, exception)
|
||||||
except BRIDGE_CONNECTION_ERRORS as exception:
|
raise ConfigEntryAuthFailed from exception
|
||||||
|
except (ConnectionClosedException, ConnectionErrorException) as exception:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})."
|
f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})."
|
||||||
) from exception
|
) 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()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Wait for initial data
|
_LOGGER.debug("Data: %s", coordinator.data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(60):
|
# Wait for initial data
|
||||||
while (
|
async with async_timeout.timeout(30):
|
||||||
coordinator.bridge.battery is None
|
while coordinator.data is None or all(
|
||||||
or coordinator.bridge.cpu is None
|
getattr(coordinator.data, module) is None for module in MODULES
|
||||||
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
|
|
||||||
):
|
):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Waiting for initial data from %s (%s)",
|
"Waiting for initial data from %s (%s): %s",
|
||||||
entry.title,
|
entry.title,
|
||||||
entry.data[CONF_HOST],
|
entry.data[CONF_HOST],
|
||||||
|
coordinator.data,
|
||||||
)
|
)
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
except asyncio.TimeoutError as exception:
|
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)
|
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
|
return True
|
||||||
|
|
||||||
def valid_device(device: str):
|
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 from exception
|
||||||
raise vol.Invalid(f"Device {device} does not exist")
|
raise vol.Invalid(f"Device {device} does not exist")
|
||||||
|
|
||||||
async def handle_send_command(call: ServiceCall) -> None:
|
async def handle_open_path(call: ServiceCall) -> None:
|
||||||
"""Handle the send_command service call."""
|
"""Handle the open path service call."""
|
||||||
|
_LOGGER.info("Open: %s", call.data)
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
call.data[CONF_BRIDGE]
|
call.data[CONF_BRIDGE]
|
||||||
]
|
]
|
||||||
bridge: Bridge = coordinator.bridge
|
await coordinator.websocket_client.open_path(call.data[CONF_PATH])
|
||||||
|
|
||||||
command = call.data[CONF_COMMAND]
|
async def handle_open_url(call: ServiceCall) -> None:
|
||||||
arguments = shlex.split(call.data[CONF_ARGUMENTS])
|
"""Handle the open url service call."""
|
||||||
|
_LOGGER.info("Open: %s", call.data)
|
||||||
_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."""
|
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
call.data[CONF_BRIDGE]
|
call.data[CONF_BRIDGE]
|
||||||
]
|
]
|
||||||
bridge: Bridge = coordinator.bridge
|
await coordinator.websocket_client.open_url(call.data[CONF_URL])
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
async def handle_send_keypress(call: ServiceCall) -> None:
|
async def handle_send_keypress(call: ServiceCall) -> None:
|
||||||
"""Handle the send_keypress service call."""
|
"""Handle the send_keypress service call."""
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
call.data[CONF_BRIDGE]
|
call.data[CONF_BRIDGE]
|
||||||
]
|
]
|
||||||
bridge: Bridge = coordinator.data
|
await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY])
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
async def handle_send_text(call: ServiceCall) -> None:
|
async def handle_send_text(call: ServiceCall) -> None:
|
||||||
"""Handle the send_keypress service call."""
|
"""Handle the send_keypress service call."""
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
call.data[CONF_BRIDGE]
|
call.data[CONF_BRIDGE]
|
||||||
]
|
]
|
||||||
bridge: Bridge = coordinator.data
|
await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT])
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SEND_COMMAND,
|
SERVICE_OPEN_PATH,
|
||||||
handle_send_command,
|
handle_open_path,
|
||||||
schema=vol.Schema(
|
schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_BRIDGE): valid_device,
|
vol.Required(CONF_BRIDGE): valid_device,
|
||||||
vol.Required(CONF_COMMAND): cv.string,
|
vol.Required(CONF_PATH): cv.string,
|
||||||
vol.Optional(CONF_ARGUMENTS, ""): cv.string,
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_OPEN,
|
SERVICE_OPEN_URL,
|
||||||
handle_open,
|
handle_open_url,
|
||||||
schema=vol.Schema(
|
schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_BRIDGE): valid_device,
|
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_BRIDGE): valid_device,
|
||||||
vol.Required(CONF_KEY): cv.string,
|
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
|
# Ensure disconnected and cleanup stop sub
|
||||||
await coordinator.bridge.async_close_websocket()
|
await coordinator.websocket_client.close()
|
||||||
if coordinator.unsub:
|
if coordinator.unsub:
|
||||||
coordinator.unsub()
|
coordinator.unsub()
|
||||||
|
|
||||||
del hass.data[DOMAIN][entry.entry_id]
|
del hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
if not hass.data[DOMAIN]:
|
if not hass.data[DOMAIN]:
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_COMMAND)
|
hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_OPEN)
|
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
|
return unload_ok
|
||||||
|
|
||||||
@ -295,20 +260,21 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator,
|
coordinator: SystemBridgeDataUpdateCoordinator,
|
||||||
|
api_port: int,
|
||||||
key: str,
|
key: str,
|
||||||
name: str | None,
|
name: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the System Bridge entity."""
|
"""Initialize the System Bridge entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
bridge: Bridge = coordinator.data
|
|
||||||
self._key = f"{bridge.information.host}_{key}"
|
self._hostname = coordinator.data.system.hostname
|
||||||
self._name = f"{bridge.information.host} {name}"
|
self._key = f"{self._hostname}_{key}"
|
||||||
self._configuration_url = bridge.get_configuration_url()
|
self._name = f"{self._hostname} {name}"
|
||||||
self._hostname = bridge.information.host
|
self._configuration_url = (
|
||||||
self._mac = bridge.information.mac
|
f"http://{self._hostname}:{api_port}/app/settings.html"
|
||||||
self._manufacturer = bridge.system.system.manufacturer
|
)
|
||||||
self._model = bridge.system.system.model
|
self._mac_address = coordinator.data.system.mac_address
|
||||||
self._version = bridge.system.system.version
|
self._version = coordinator.data.system.version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
@ -329,9 +295,7 @@ class SystemBridgeDeviceEntity(SystemBridgeEntity):
|
|||||||
"""Return device information about this System Bridge instance."""
|
"""Return device information about this System Bridge instance."""
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
configuration_url=self._configuration_url,
|
configuration_url=self._configuration_url,
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, self._mac)},
|
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_address)},
|
||||||
manufacturer=self._manufacturer,
|
|
||||||
model=self._model,
|
|
||||||
name=self._hostname,
|
name=self._hostname,
|
||||||
sw_version=self._version,
|
sw_version=self._version,
|
||||||
)
|
)
|
||||||
|
@ -4,14 +4,13 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from systembridge import Bridge
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...]
|
|||||||
key="version_available",
|
key="version_available",
|
||||||
name="New Version Available",
|
name="New Version Available",
|
||||||
device_class=BinarySensorDeviceClass.UPDATE,
|
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",
|
key="battery_is_charging",
|
||||||
name="Battery Is Charging",
|
name="Battery Is Charging",
|
||||||
device_class=BinarySensorDeviceClass.BATTERY_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:
|
) -> None:
|
||||||
"""Set up System Bridge binary sensor based on a config entry."""
|
"""Set up System Bridge binary sensor based on a config entry."""
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
bridge: Bridge = coordinator.data
|
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
for description in BASE_BINARY_SENSOR_TYPES:
|
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:
|
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)
|
async_add_entities(entities)
|
||||||
|
|
||||||
@ -73,10 +81,12 @@ class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity):
|
|||||||
self,
|
self,
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator,
|
coordinator: SystemBridgeDataUpdateCoordinator,
|
||||||
description: SystemBridgeBinarySensorEntityDescription,
|
description: SystemBridgeBinarySensorEntityDescription,
|
||||||
|
api_port: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
coordinator,
|
coordinator,
|
||||||
|
api_port,
|
||||||
description.key,
|
description.key,
|
||||||
description.name,
|
description.name,
|
||||||
)
|
)
|
||||||
@ -85,5 +95,4 @@ class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return the boolean state of the binary sensor."""
|
"""Return the boolean state of the binary sensor."""
|
||||||
bridge: Bridge = self.coordinator.data
|
return self.entity_description.value(self.coordinator.data)
|
||||||
return self.entity_description.value(bridge)
|
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
"""Config flow for System Bridge integration."""
|
"""Config flow for System Bridge integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
from systembridge import Bridge
|
from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE
|
||||||
from systembridge.client import BridgeClient
|
from systembridgeconnector.exceptions import (
|
||||||
from systembridge.exceptions import BridgeAuthenticationException
|
AuthenticationException,
|
||||||
|
ConnectionClosedException,
|
||||||
|
ConnectionErrorException,
|
||||||
|
)
|
||||||
|
from systembridgeconnector.websocket_client import WebSocketClient
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, exceptions
|
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.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -31,56 +37,67 @@ 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.
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||||
"""
|
"""
|
||||||
bridge = Bridge(
|
host = data[CONF_HOST]
|
||||||
BridgeClient(aiohttp_client.async_get_clientsession(hass)),
|
|
||||||
f"http://{data[CONF_HOST]}:{data[CONF_PORT]}",
|
websocket_client = WebSocketClient(
|
||||||
|
host,
|
||||||
|
data[CONF_PORT],
|
||||||
data[CONF_API_KEY],
|
data[CONF_API_KEY],
|
||||||
)
|
)
|
||||||
|
|
||||||
hostname = data[CONF_HOST]
|
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(30):
|
async with async_timeout.timeout(30):
|
||||||
await bridge.async_get_information()
|
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 (
|
if (
|
||||||
bridge.information is not None
|
message[EVENT_TYPE] == TYPE_DATA_UPDATE
|
||||||
and bridge.information.host is not None
|
and message[EVENT_MODULE] == "system"
|
||||||
and bridge.information.uuid is not None
|
|
||||||
):
|
):
|
||||||
hostname = bridge.information.host
|
break
|
||||||
uuid = bridge.information.uuid
|
except AuthenticationException as exception:
|
||||||
except BridgeAuthenticationException as exception:
|
_LOGGER.warning(
|
||||||
_LOGGER.info(exception)
|
"Authentication error when connecting to %s: %s", data[CONF_HOST], exception
|
||||||
|
)
|
||||||
raise InvalidAuth from exception
|
raise InvalidAuth from exception
|
||||||
except BRIDGE_CONNECTION_ERRORS as exception:
|
except (
|
||||||
_LOGGER.info(exception)
|
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
|
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(
|
||||||
"""Handle a config flow for System Bridge."""
|
hass: HomeAssistant,
|
||||||
|
user_input: dict[str, Any],
|
||||||
VERSION = 1
|
) -> tuple[dict[str, str], dict[str, str] | None]:
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize flow."""
|
|
||||||
self._name: str | None = None
|
|
||||||
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 = {}
|
errors = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = await validate_input(self.hass, user_input)
|
info = await validate_input(hass, user_input)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
@ -93,6 +110,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return errors, None
|
return errors, None
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(
|
||||||
|
config_entries.ConfigFlow,
|
||||||
|
domain=DOMAIN,
|
||||||
|
):
|
||||||
|
"""Handle a config flow for System Bridge."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize flow."""
|
||||||
|
self._name: str | None = None
|
||||||
|
self._input: dict[str, Any] = {}
|
||||||
|
self._reauth = False
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
@ -102,7 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
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:
|
if not errors and info is not None:
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
await self.async_set_unique_id(info["uuid"], raise_on_progress=False)
|
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:
|
if user_input is not None:
|
||||||
user_input = {**self._input, **user_input}
|
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:
|
if not errors and info is not None:
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
existing_entry = await self.async_set_unique_id(info["uuid"])
|
existing_entry = await self.async_set_unique_id(info["uuid"])
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
"""Constants for the System Bridge integration."""
|
"""Constants for the System Bridge integration."""
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from aiohttp.client_exceptions import (
|
|
||||||
ClientConnectionError,
|
|
||||||
ClientConnectorError,
|
|
||||||
ClientResponseError,
|
|
||||||
)
|
|
||||||
from systembridge.exceptions import BridgeException
|
|
||||||
|
|
||||||
DOMAIN = "system_bridge"
|
DOMAIN = "system_bridge"
|
||||||
|
|
||||||
BRIDGE_CONNECTION_ERRORS = (
|
MODULES = [
|
||||||
asyncio.TimeoutError,
|
"battery",
|
||||||
BridgeException,
|
"cpu",
|
||||||
ClientConnectionError,
|
"disk",
|
||||||
ClientConnectorError,
|
"display",
|
||||||
ClientResponseError,
|
"gpu",
|
||||||
OSError,
|
"memory",
|
||||||
)
|
"system",
|
||||||
|
]
|
||||||
|
@ -6,40 +6,71 @@ from collections.abc import Callable
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from systembridge import Bridge
|
import async_timeout
|
||||||
from systembridge.exceptions import (
|
from pydantic import BaseModel # pylint: disable=no-name-in-module
|
||||||
BridgeAuthenticationException,
|
from systembridgeconnector.exceptions import (
|
||||||
BridgeConnectionClosedException,
|
AuthenticationException,
|
||||||
BridgeException,
|
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.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.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
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."""
|
"""Class to manage fetching System Bridge data from single endpoint."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
bridge: Bridge,
|
|
||||||
LOGGER: logging.Logger,
|
LOGGER: logging.Logger,
|
||||||
*,
|
*,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize global System Bridge data updater."""
|
"""Initialize global System Bridge data updater."""
|
||||||
self.bridge = bridge
|
|
||||||
self.title = entry.title
|
self.title = entry.title
|
||||||
self.host = entry.data[CONF_HOST]
|
|
||||||
self.unsub: Callable | None = None
|
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__(
|
super().__init__(
|
||||||
hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30)
|
hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30)
|
||||||
)
|
)
|
||||||
@ -49,97 +80,117 @@ class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]):
|
|||||||
for update_callback in self._listeners:
|
for update_callback in self._listeners:
|
||||||
update_callback()
|
update_callback()
|
||||||
|
|
||||||
async def async_handle_event(self, event: Event):
|
async def async_get_data(
|
||||||
"""Handle System Bridge events from the WebSocket."""
|
self,
|
||||||
# No need to update anything, as everything is updated in the caller
|
modules: list[str],
|
||||||
self.logger.debug(
|
) -> None:
|
||||||
"New event from %s (%s): %s", self.title, self.host, event.name
|
"""Get data from WebSocket."""
|
||||||
)
|
if not self.websocket_client.connected:
|
||||||
self.async_set_updated_data(self.bridge)
|
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."""
|
"""Listen for events from the WebSocket."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.bridge.async_send_event(
|
await self.websocket_client.register_data_listener(MODULES)
|
||||||
"get-data",
|
await self.websocket_client.listen(callback=self.async_handle_module)
|
||||||
[
|
except AuthenticationException as exception:
|
||||||
{"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:
|
|
||||||
self.last_update_success = False
|
self.last_update_success = False
|
||||||
self.logger.info(
|
self.logger.error("Authentication failed for %s: %s", self.title, exception)
|
||||||
"Websocket Connection Closed for %s (%s). Will retry: %s",
|
if self.unsub:
|
||||||
self.title,
|
self.unsub()
|
||||||
self.host,
|
self.unsub = None
|
||||||
exception,
|
|
||||||
)
|
|
||||||
except BridgeException as exception:
|
|
||||||
self.last_update_success = False
|
self.last_update_success = False
|
||||||
self.update_listeners()
|
self.update_listeners()
|
||||||
self.logger.warning(
|
except (ConnectionClosedException, ConnectionResetError) as exception:
|
||||||
"Exception occurred for %s (%s). Will retry: %s",
|
self.logger.info(
|
||||||
|
"Websocket connection closed for %s. Will retry: %s",
|
||||||
self.title,
|
self.title,
|
||||||
self.host,
|
|
||||||
exception,
|
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:
|
async def _setup_websocket(self) -> None:
|
||||||
"""Use WebSocket for updates."""
|
"""Use WebSocket for updates."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.debug(
|
async with async_timeout.timeout(20):
|
||||||
"Connecting to ws://%s:%s",
|
await self.websocket_client.connect(
|
||||||
self.host,
|
session=async_get_clientsession(self.hass),
|
||||||
self.bridge.information.websocketPort,
|
|
||||||
)
|
)
|
||||||
await self.bridge.async_connect_websocket(
|
except AuthenticationException as exception:
|
||||||
self.host, self.bridge.information.websocketPort
|
self.last_update_success = False
|
||||||
)
|
self.logger.error("Authentication failed for %s: %s", self.title, exception)
|
||||||
except BridgeAuthenticationException as exception:
|
|
||||||
if self.unsub:
|
if self.unsub:
|
||||||
self.unsub()
|
self.unsub()
|
||||||
self.unsub = None
|
self.unsub = None
|
||||||
raise ConfigEntryAuthFailed() from exception
|
self.last_update_success = False
|
||||||
except (*BRIDGE_CONNECTION_ERRORS, ConnectionRefusedError) as exception:
|
self.update_listeners()
|
||||||
if self.unsub:
|
except ConnectionErrorException as exception:
|
||||||
self.unsub()
|
self.logger.warning(
|
||||||
self.unsub = None
|
"Connection error occurred for %s. Will retry: %s",
|
||||||
raise UpdateFailed(
|
self.title,
|
||||||
f"Could not connect to {self.title} ({self.host})."
|
exception,
|
||||||
) from exception
|
)
|
||||||
asyncio.create_task(self._listen_for_events())
|
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:
|
async def close_websocket(_) -> None:
|
||||||
"""Close WebSocket connection."""
|
"""Close WebSocket connection."""
|
||||||
await self.bridge.async_close_websocket()
|
await self.websocket_client.close()
|
||||||
|
|
||||||
# Clean disconnect WebSocket on Home Assistant shutdown
|
# Clean disconnect WebSocket on Home Assistant shutdown
|
||||||
self.unsub = self.hass.bus.async_listen_once(
|
self.unsub = self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_STOP, close_websocket
|
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."""
|
"""Update System Bridge data from WebSocket."""
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"_async_update_data - WebSocket Connected: %s",
|
"_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()
|
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",
|
"name": "System Bridge",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/system_bridge",
|
"documentation": "https://www.home-assistant.io/integrations/system_bridge",
|
||||||
"requirements": ["systembridge==2.3.1"],
|
"requirements": ["systembridgeconnector==3.1.3"],
|
||||||
"codeowners": ["@timmo001"],
|
"codeowners": ["@timmo001"],
|
||||||
"zeroconf": ["_system-bridge._udp.local."],
|
"zeroconf": ["_system-bridge._tcp.local."],
|
||||||
"after_dependencies": ["zeroconf"],
|
"after_dependencies": ["zeroconf"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["systembridge"]
|
"loggers": ["systembridgeconnector"]
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Final, cast
|
from typing import Final, cast
|
||||||
|
|
||||||
from systembridge import Bridge
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
@ -16,6 +14,7 @@ from homeassistant.components.sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONF_PORT,
|
||||||
DATA_GIGABYTES,
|
DATA_GIGABYTES,
|
||||||
ELECTRIC_POTENTIAL_VOLT,
|
ELECTRIC_POTENTIAL_VOLT,
|
||||||
FREQUENCY_GIGAHERTZ,
|
FREQUENCY_GIGAHERTZ,
|
||||||
@ -28,10 +27,11 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import SystemBridgeDeviceEntity
|
from . import SystemBridgeDeviceEntity
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator
|
||||||
|
|
||||||
ATTR_AVAILABLE: Final = "available"
|
ATTR_AVAILABLE: Final = "available"
|
||||||
ATTR_FILESYSTEM: Final = "filesystem"
|
ATTR_FILESYSTEM: Final = "filesystem"
|
||||||
@ -41,6 +41,7 @@ ATTR_TYPE: Final = "type"
|
|||||||
ATTR_USED: Final = "used"
|
ATTR_USED: Final = "used"
|
||||||
|
|
||||||
PIXELS: Final = "px"
|
PIXELS: Final = "px"
|
||||||
|
RPM: Final = "RPM"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -50,21 +51,87 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription):
|
|||||||
value: Callable = round
|
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, ...] = (
|
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(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="cpu_speed",
|
key="cpu_speed",
|
||||||
name="CPU Speed",
|
name="CPU Speed",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=FREQUENCY_GIGAHERTZ,
|
native_unit_of_measurement=FREQUENCY_GIGAHERTZ,
|
||||||
icon="mdi:speedometer",
|
icon="mdi:speedometer",
|
||||||
value=lambda bridge: bridge.cpu.currentSpeed.avg,
|
value=cpu_speed,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="cpu_temperature",
|
key="cpu_temperature",
|
||||||
@ -73,7 +140,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=TEMP_CELSIUS,
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
value=lambda bridge: bridge.cpu.temperature.main,
|
value=lambda data: data.cpu.temperature,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="cpu_voltage",
|
key="cpu_voltage",
|
||||||
@ -82,21 +149,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.VOLTAGE,
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
value=lambda bridge: bridge.cpu.cpu.voltage,
|
value=lambda data: data.cpu.voltage,
|
||||||
),
|
|
||||||
SystemBridgeSensorEntityDescription(
|
|
||||||
key="displays_connected",
|
|
||||||
name="Displays Connected",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
icon="mdi:monitor",
|
|
||||||
value=lambda bridge: len(bridge.display.displays),
|
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="kernel",
|
key="kernel",
|
||||||
name="Kernel",
|
name="Kernel",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
icon="mdi:devices",
|
icon="mdi:devices",
|
||||||
value=lambda bridge: bridge.os.kernel,
|
value=lambda data: data.system.platform,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="memory_free",
|
key="memory_free",
|
||||||
@ -104,7 +164,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=DATA_GIGABYTES,
|
native_unit_of_measurement=DATA_GIGABYTES,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
value=lambda bridge: round(bridge.memory.free / 1000**3, 2),
|
value=memory_free,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="memory_used_percentage",
|
key="memory_used_percentage",
|
||||||
@ -112,7 +172,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
value=lambda bridge: round((bridge.memory.used / bridge.memory.total) * 100, 2),
|
value=lambda data: data.memory.virtual_percent,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="memory_used",
|
key="memory_used",
|
||||||
@ -121,14 +181,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=DATA_GIGABYTES,
|
native_unit_of_measurement=DATA_GIGABYTES,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
value=lambda bridge: round(bridge.memory.used / 1000**3, 2),
|
value=memory_used,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="os",
|
key="os",
|
||||||
name="Operating System",
|
name="Operating System",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
icon="mdi:devices",
|
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(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="processes_load",
|
key="processes_load",
|
||||||
@ -136,46 +196,19 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:percent",
|
icon="mdi:percent",
|
||||||
value=lambda bridge: round(bridge.processes.load.currentLoad, 2),
|
value=lambda data: data.cpu.usage,
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="version",
|
key="version",
|
||||||
name="Version",
|
name="Version",
|
||||||
icon="mdi:counter",
|
icon="mdi:counter",
|
||||||
value=lambda bridge: bridge.information.version,
|
value=lambda data: data.system.version,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="version_latest",
|
key="version_latest",
|
||||||
name="Latest Version",
|
name="Latest Version",
|
||||||
icon="mdi:counter",
|
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,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
value=lambda bridge: bridge.battery.percent,
|
value=lambda data: data.battery.percentage,
|
||||||
),
|
),
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key="battery_time_remaining",
|
key="battery_time_remaining",
|
||||||
name="Battery Time Remaining",
|
name="Battery Time Remaining",
|
||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value=lambda bridge: str(
|
value=battery_time_remaining,
|
||||||
datetime.now() + timedelta(minutes=bridge.battery.timeRemaining)
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up System Bridge sensor based on a config entry."""
|
"""Set up System Bridge sensor based on a config entry."""
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
for description in BASE_SENSOR_TYPES:
|
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():
|
for partition in coordinator.data.disk.partitions:
|
||||||
uid = key.replace(":", "")
|
|
||||||
entities.append(
|
entities.append(
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"filesystem_{uid}",
|
key=f"filesystem_{partition.replace(':', '')}",
|
||||||
name=f"{key} Space Used",
|
name=f"{partition} Space Used",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:harddisk",
|
icon="mdi:harddisk",
|
||||||
value=lambda bridge, i=key: round(
|
value=lambda data, p=partition: getattr(
|
||||||
bridge.filesystem.fsSize[i]["use"], 2
|
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:
|
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):
|
displays = []
|
||||||
name = index + 1
|
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 = [
|
||||||
*entities,
|
*entities,
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"display_{name}_resolution_x",
|
key=f"display_{display['name']}_resolution_x",
|
||||||
name=f"Display {name} Resolution X",
|
name=f"Display {display['name']} Resolution X",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PIXELS,
|
native_unit_of_measurement=PIXELS,
|
||||||
icon="mdi:monitor",
|
icon="mdi:monitor",
|
||||||
value=lambda bridge, i=index: bridge.display.displays[
|
value=lambda data, k=display["key"]: getattr(
|
||||||
i
|
data.display, f"{k}_resolution_horizontal"
|
||||||
].resolutionX,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"display_{name}_resolution_y",
|
key=f"display_{display['name']}_resolution_y",
|
||||||
name=f"Display {name} Resolution Y",
|
name=f"Display {display['name']} Resolution Y",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PIXELS,
|
native_unit_of_measurement=PIXELS,
|
||||||
icon="mdi:monitor",
|
icon="mdi:monitor",
|
||||||
value=lambda bridge, i=index: bridge.display.displays[
|
value=lambda data, k=display["key"]: getattr(
|
||||||
i
|
data.display, f"{k}_resolution_vertical"
|
||||||
].resolutionY,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"display_{name}_refresh_rate",
|
key=f"display_{display['name']}_refresh_rate",
|
||||||
name=f"Display {name} Refresh Rate",
|
name=f"Display {display['name']} Refresh Rate",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=FREQUENCY_HERTZ,
|
native_unit_of_measurement=FREQUENCY_HERTZ,
|
||||||
icon="mdi:monitor",
|
icon="mdi:monitor",
|
||||||
value=lambda bridge, i=index: bridge.display.displays[
|
value=lambda data, k=display["key"]: getattr(
|
||||||
i
|
data.display, f"{k}_refresh_rate"
|
||||||
].currentRefreshRate,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
for index, _ in enumerate(coordinator.data.graphics.controllers):
|
gpus = []
|
||||||
if coordinator.data.graphics.controllers[index].name is not None:
|
for gpu in coordinator.data.gpu.gpus:
|
||||||
# Remove vendor from name
|
gpus.append(
|
||||||
name = (
|
{
|
||||||
coordinator.data.graphics.controllers[index]
|
"key": gpu,
|
||||||
.name.replace(coordinator.data.graphics.controllers[index].vendor, "")
|
"name": getattr(coordinator.data.gpu, f"{gpu}_name"),
|
||||||
.strip()
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for index, gpu in enumerate(gpus):
|
||||||
entities = [
|
entities = [
|
||||||
*entities,
|
*entities,
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_core_clock_speed",
|
key=f"gpu_{index}_core_clock_speed",
|
||||||
name=f"{name} Clock Speed",
|
name=f"{gpu['name']} Clock Speed",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
||||||
icon="mdi:speedometer",
|
icon="mdi:speedometer",
|
||||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
value=lambda data, k=gpu["key"]: gpu_core_clock_speed(data, k),
|
||||||
i
|
|
||||||
].clockCore,
|
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_memory_clock_speed",
|
key=f"gpu_{index}_memory_clock_speed",
|
||||||
name=f"{name} Memory Clock Speed",
|
name=f"{gpu['name']} Memory Clock Speed",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
native_unit_of_measurement=FREQUENCY_MEGAHERTZ,
|
||||||
icon="mdi:speedometer",
|
icon="mdi:speedometer",
|
||||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
value=lambda data, k=gpu["key"]: gpu_memory_clock_speed(data, k),
|
||||||
i
|
|
||||||
].clockMemory,
|
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_memory_free",
|
key=f"gpu_{index}_memory_free",
|
||||||
name=f"{name} Memory Free",
|
name=f"{gpu['name']} Memory Free",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=DATA_GIGABYTES,
|
native_unit_of_measurement=DATA_GIGABYTES,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
value=lambda bridge, i=index: round(
|
value=lambda data, k=gpu["key"]: gpu_memory_free(data, k),
|
||||||
bridge.graphics.controllers[i].memoryFree / 10**3, 2
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_memory_used_percentage",
|
key=f"gpu_{index}_memory_used_percentage",
|
||||||
name=f"{name} Memory Used %",
|
name=f"{gpu['name']} Memory Used %",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
value=lambda bridge, i=index: round(
|
value=lambda data, k=gpu["key"]: gpu_memory_used_percentage(
|
||||||
(
|
data, k
|
||||||
bridge.graphics.controllers[i].memoryUsed
|
|
||||||
/ bridge.graphics.controllers[i].memoryTotal
|
|
||||||
)
|
|
||||||
* 100,
|
|
||||||
2,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_memory_used",
|
key=f"gpu_{index}_memory_used",
|
||||||
name=f"{name} Memory Used",
|
name=f"{gpu['name']} Memory Used",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=DATA_GIGABYTES,
|
native_unit_of_measurement=DATA_GIGABYTES,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
value=lambda bridge, i=index: round(
|
value=lambda data, k=gpu["key"]: gpu_memory_used(data, k),
|
||||||
bridge.graphics.controllers[i].memoryUsed / 10**3, 2
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_fan_speed",
|
key=f"gpu_{index}_fan_speed",
|
||||||
name=f"{name} Fan Speed",
|
name=f"{gpu['name']} Fan Speed",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=RPM,
|
||||||
icon="mdi:fan",
|
icon="mdi:fan",
|
||||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
value=lambda data, k=gpu["key"]: getattr(
|
||||||
i
|
data.gpu, f"{k}_fan_speed"
|
||||||
].fanSpeed,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_power_usage",
|
key=f"gpu_{index}_power_usage",
|
||||||
name=f"{name} Power Usage",
|
name=f"{gpu['name']} Power Usage",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
device_class=SensorDeviceClass.POWER,
|
device_class=SensorDeviceClass.POWER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=POWER_WATT,
|
native_unit_of_measurement=POWER_WATT,
|
||||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
value=lambda data, k=gpu["key"]: getattr(data.gpu, f"{k}_power"),
|
||||||
i
|
|
||||||
].powerDraw,
|
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_temperature",
|
key=f"gpu_{index}_temperature",
|
||||||
name=f"{name} Temperature",
|
name=f"{gpu['name']} Temperature",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=TEMP_CELSIUS,
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
value=lambda data, k=gpu["key"]: getattr(
|
||||||
i
|
data.gpu, f"{k}_temperature"
|
||||||
].temperatureGpu,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
),
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
SystemBridgeSensorEntityDescription(
|
SystemBridgeSensorEntityDescription(
|
||||||
key=f"gpu_{index}_usage_percentage",
|
key=f"gpu_{index}_usage_percentage",
|
||||||
name=f"{name} Usage %",
|
name=f"{gpu['name']} Usage %",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:percent",
|
icon="mdi:percent",
|
||||||
value=lambda bridge, i=index: bridge.graphics.controllers[
|
value=lambda data, k=gpu["key"]: getattr(
|
||||||
i
|
data.gpu, f"{k}_core_load"
|
||||||
].utilizationGpu,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
for index, _ in enumerate(coordinator.data.processes.load.cpus):
|
for index in range(coordinator.data.cpu.count):
|
||||||
entities = [
|
entities = [
|
||||||
*entities,
|
*entities,
|
||||||
SystemBridgeSensor(
|
SystemBridgeSensor(
|
||||||
@ -429,52 +494,9 @@ async def async_setup_entry(
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:percent",
|
icon="mdi:percent",
|
||||||
value=lambda bridge, index=index: round(
|
value=lambda data, k=index: getattr(data.cpu, f"usage_{k}"),
|
||||||
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
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
entry.data[CONF_PORT],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -490,10 +512,12 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity):
|
|||||||
self,
|
self,
|
||||||
coordinator: SystemBridgeDataUpdateCoordinator,
|
coordinator: SystemBridgeDataUpdateCoordinator,
|
||||||
description: SystemBridgeSensorEntityDescription,
|
description: SystemBridgeSensorEntityDescription,
|
||||||
|
api_port: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
coordinator,
|
coordinator,
|
||||||
|
api_port,
|
||||||
description.key,
|
description.key,
|
||||||
description.name,
|
description.name,
|
||||||
)
|
)
|
||||||
@ -502,8 +526,7 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state."""
|
"""Return the state."""
|
||||||
bridge: Bridge = self.coordinator.data
|
|
||||||
try:
|
try:
|
||||||
return cast(StateType, self.entity_description.value(bridge))
|
return cast(StateType, self.entity_description.value(self.coordinator.data))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return None
|
return None
|
||||||
|
@ -1,32 +1,6 @@
|
|||||||
send_command:
|
open_path:
|
||||||
name: Send Command
|
name: Open Path
|
||||||
description: Sends a command to the server to run.
|
description: Open a file on the server using the default application.
|
||||||
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.
|
|
||||||
fields:
|
fields:
|
||||||
bridge:
|
bridge:
|
||||||
name: Bridge
|
name: Bridge
|
||||||
@ -36,8 +10,26 @@ open:
|
|||||||
device:
|
device:
|
||||||
integration: system_bridge
|
integration: system_bridge
|
||||||
path:
|
path:
|
||||||
name: Path/URL
|
name: Path
|
||||||
description: Path/URL to open.
|
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
|
required: true
|
||||||
example: "https://www.home-assistant.io"
|
example: "https://www.home-assistant.io"
|
||||||
selector:
|
selector:
|
||||||
@ -60,16 +52,6 @@ send_keypress:
|
|||||||
example: "audio_play"
|
example: "audio_play"
|
||||||
selector:
|
selector:
|
||||||
text:
|
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:
|
send_text:
|
||||||
name: Send Keyboard Text
|
name: Send Keyboard Text
|
||||||
description: Sends text for the server to type.
|
description: Sends text for the server to type.
|
||||||
|
@ -370,7 +370,7 @@ ZEROCONF = {
|
|||||||
"name": "smappee50*"
|
"name": "smappee50*"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_system-bridge._udp.local.": [
|
"_system-bridge._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "system_bridge"
|
"domain": "system_bridge"
|
||||||
}
|
}
|
||||||
|
@ -2271,7 +2271,7 @@ swisshydrodata==0.1.0
|
|||||||
synology-srm==0.2.0
|
synology-srm==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridge==2.3.1
|
systembridgeconnector==3.1.3
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.2.0
|
tailscale==0.2.0
|
||||||
|
@ -1498,7 +1498,7 @@ sunwatcher==0.2.1
|
|||||||
surepy==0.7.2
|
surepy==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridge==2.3.1
|
systembridgeconnector==3.1.3
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.2.0
|
tailscale==0.2.0
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
"""Test the System Bridge config flow."""
|
"""Test the System Bridge config flow."""
|
||||||
|
import asyncio
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectionError
|
from systembridgeconnector.const import (
|
||||||
from systembridge.exceptions import BridgeAuthenticationException
|
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 import config_entries, data_entry_flow
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.components.system_bridge.const import DOMAIN
|
from homeassistant.components.system_bridge.const import DOMAIN
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -29,7 +44,7 @@ FIXTURE_ZEROCONF_INPUT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo(
|
FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo(
|
||||||
host="1.1.1.1",
|
host="test-bridge",
|
||||||
addresses=["1.1.1.1"],
|
addresses=["1.1.1.1"],
|
||||||
port=9170,
|
port=9170,
|
||||||
hostname="test-bridge.local.",
|
hostname="test-bridge.local.",
|
||||||
@ -58,37 +73,40 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FIXTURE_DATA_SYSTEM = {
|
||||||
FIXTURE_INFORMATION = {
|
EVENT_TYPE: TYPE_DATA_UPDATE,
|
||||||
"address": "http://test-bridge:9170",
|
EVENT_MESSAGE: "Data changed",
|
||||||
"apiPort": 9170,
|
EVENT_MODULE: "system",
|
||||||
"fqdn": "test-bridge",
|
EVENT_DATA: {
|
||||||
"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"},
|
|
||||||
},
|
|
||||||
"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 = (
|
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||||
f"http://{FIXTURE_USER_INPUT[CONF_HOST]}:{FIXTURE_USER_INPUT[CONF_PORT]}"
|
"""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(
|
async def test_user_flow(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
|
||||||
) -> None:
|
|
||||||
"""Test full user flow."""
|
"""Test full user flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
@ -97,13 +115,12 @@ async def test_user_flow(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] is None
|
assert result["errors"] is None
|
||||||
|
|
||||||
aioclient_mock.get(
|
|
||||||
f"{FIXTURE_BASE_URL}/information",
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
json=FIXTURE_INFORMATION,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
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",
|
"homeassistant.components.system_bridge.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
@ -118,34 +135,7 @@ async def test_user_flow(
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_form_invalid_auth(
|
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||||
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:
|
|
||||||
"""Test we handle cannot connect error."""
|
"""Test we handle cannot connect error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
@ -154,8 +144,10 @@ async def test_form_cannot_connect(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] is None
|
assert result["errors"] is None
|
||||||
|
|
||||||
aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError)
|
with patch(
|
||||||
|
"systembridgeconnector.websocket_client.WebSocketClient.connect",
|
||||||
|
side_effect=ConnectionErrorException,
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], FIXTURE_USER_INPUT
|
result["flow_id"], FIXTURE_USER_INPUT
|
||||||
)
|
)
|
||||||
@ -166,10 +158,8 @@ async def test_form_cannot_connect(
|
|||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_form_unknown_error(
|
async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
"""Test we handle connection closed cannot connect error."""
|
||||||
) -> None:
|
|
||||||
"""Test we handle unknown error."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
@ -177,9 +167,111 @@ async def test_form_unknown_error(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] is None
|
assert result["errors"] is None
|
||||||
|
|
||||||
with patch(
|
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||||
"homeassistant.components.system_bridge.config_flow.Bridge.async_get_information",
|
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||||
side_effect=Exception("Boom"),
|
), 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()
|
||||||
|
|
||||||
|
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(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], FIXTURE_USER_INPUT
|
result["flow_id"], FIXTURE_USER_INPUT
|
||||||
@ -191,9 +283,7 @@ async def test_form_unknown_error(
|
|||||||
assert result2["errors"] == {"base": "unknown"}
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
async def test_reauth_authorization_error(
|
async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
|
||||||
) -> None:
|
|
||||||
"""Test we show user form on authorization error."""
|
"""Test we show user form on authorization error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
@ -202,10 +292,12 @@ async def test_reauth_authorization_error(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
|
||||||
aioclient_mock.get(
|
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||||
f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException
|
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||||
)
|
), patch(
|
||||||
|
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||||
|
side_effect=AuthenticationException,
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||||
)
|
)
|
||||||
@ -216,9 +308,7 @@ async def test_reauth_authorization_error(
|
|||||||
assert result2["errors"] == {"base": "invalid_auth"}
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
async def test_reauth_connection_error(
|
async def test_reauth_connection_error(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
|
||||||
) -> None:
|
|
||||||
"""Test we show user form on connection error."""
|
"""Test we show user form on connection error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
@ -227,8 +317,10 @@ async def test_reauth_connection_error(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
|
||||||
aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError)
|
with patch(
|
||||||
|
"systembridgeconnector.websocket_client.WebSocketClient.connect",
|
||||||
|
side_effect=ConnectionErrorException,
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||||
)
|
)
|
||||||
@ -239,9 +331,32 @@ async def test_reauth_connection_error(
|
|||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_reauth_flow(
|
async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
"""Test we show user form on connection error."""
|
||||||
) -> None:
|
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."""
|
"""Test reauth flow."""
|
||||||
mock_config = MockConfigEntry(
|
mock_config = MockConfigEntry(
|
||||||
domain=DOMAIN, unique_id=FIXTURE_UUID, data=FIXTURE_USER_INPUT
|
domain=DOMAIN, unique_id=FIXTURE_UUID, data=FIXTURE_USER_INPUT
|
||||||
@ -255,13 +370,12 @@ async def test_reauth_flow(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "authenticate"
|
assert result["step_id"] == "authenticate"
|
||||||
|
|
||||||
aioclient_mock.get(
|
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||||
f"{FIXTURE_BASE_URL}/information",
|
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||||
headers={"Content-Type": "application/json"},
|
), patch(
|
||||||
json=FIXTURE_INFORMATION,
|
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||||
)
|
return_value=FIXTURE_DATA_SYSTEM,
|
||||||
|
), patch(
|
||||||
with patch(
|
|
||||||
"homeassistant.components.system_bridge.async_setup_entry",
|
"homeassistant.components.system_bridge.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
@ -276,9 +390,7 @@ async def test_reauth_flow(
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_flow(
|
async def test_zeroconf_flow(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
|
||||||
) -> None:
|
|
||||||
"""Test zeroconf flow."""
|
"""Test zeroconf flow."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -290,13 +402,12 @@ async def test_zeroconf_flow(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert not result["errors"]
|
assert not result["errors"]
|
||||||
|
|
||||||
aioclient_mock.get(
|
with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch(
|
||||||
f"{FIXTURE_ZEROCONF_BASE_URL}/information",
|
"systembridgeconnector.websocket_client.WebSocketClient.get_data"
|
||||||
headers={"Content-Type": "application/json"},
|
), patch(
|
||||||
json=FIXTURE_INFORMATION,
|
"systembridgeconnector.websocket_client.WebSocketClient.receive_message",
|
||||||
)
|
return_value=FIXTURE_DATA_SYSTEM,
|
||||||
|
), patch(
|
||||||
with patch(
|
|
||||||
"homeassistant.components.system_bridge.async_setup_entry",
|
"homeassistant.components.system_bridge.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
@ -306,14 +417,12 @@ async def test_zeroconf_flow(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
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 result2["data"] == FIXTURE_ZEROCONF_INPUT
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_cannot_connect(
|
async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
|
||||||
) -> None:
|
|
||||||
"""Test zeroconf cannot connect flow."""
|
"""Test zeroconf cannot connect flow."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -325,10 +434,10 @@ async def test_zeroconf_cannot_connect(
|
|||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert not result["errors"]
|
assert not result["errors"]
|
||||||
|
|
||||||
aioclient_mock.get(
|
with patch(
|
||||||
f"{FIXTURE_ZEROCONF_BASE_URL}/information", exc=ClientConnectionError
|
"systembridgeconnector.websocket_client.WebSocketClient.connect",
|
||||||
)
|
side_effect=ConnectionErrorException,
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], FIXTURE_AUTH_INPUT
|
result["flow_id"], FIXTURE_AUTH_INPUT
|
||||||
)
|
)
|
||||||
@ -339,9 +448,7 @@ async def test_zeroconf_cannot_connect(
|
|||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_bad_zeroconf_info(
|
async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None:
|
||||||
hass, aiohttp_client, aioclient_mock, current_request_with_host
|
|
||||||
) -> None:
|
|
||||||
"""Test zeroconf cannot connect flow."""
|
"""Test zeroconf cannot connect flow."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user