diff --git a/.coveragerc b/.coveragerc index 1f28a9a2aee..5ebef801c6d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1007,8 +1007,9 @@ omit = homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py homeassistant/components/system_bridge/__init__.py - homeassistant/components/system_bridge/const.py homeassistant/components/system_bridge/binary_sensor.py + homeassistant/components/system_bridge/const.py + homeassistant/components/system_bridge/coordinator.py homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 10ee4165295..f016cca798d 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from datetime import timedelta import logging import shlex @@ -22,20 +21,17 @@ from homeassistant.const import ( CONF_PORT, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN +from .coordinator import SystemBridgeDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -61,51 +57,55 @@ SERVICE_OPEN_SCHEMA = vol.Schema( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up System Bridge from a config entry.""" - - client = Bridge( + bridge = Bridge( BridgeClient(aiohttp_client.async_get_clientsession(hass)), f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}", entry.data[CONF_API_KEY], ) - async def async_update_data() -> Bridge: - """Fetch data from Bridge.""" - try: - async with async_timeout.timeout(60): - await asyncio.gather( - *[ - client.async_get_battery(), - client.async_get_cpu(), - client.async_get_filesystem(), - client.async_get_memory(), - client.async_get_network(), - client.async_get_os(), - client.async_get_processes(), - client.async_get_system(), - ] - ) - return client - except BridgeAuthenticationException as exception: - raise ConfigEntryAuthFailed from exception - except BRIDGE_CONNECTION_ERRORS as exception: - raise UpdateFailed("Could not connect to System Bridge.") from exception + try: + async with async_timeout.timeout(30): + await bridge.async_get_information() + except BridgeAuthenticationException as exception: + raise ConfigEntryAuthFailed( + f"Authentication failed for {entry.title} ({entry.data[CONF_HOST]})" + ) from exception + except BRIDGE_CONNECTION_ERRORS as exception: + raise ConfigEntryNotReady( + f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})." + ) from exception - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name=f"{DOMAIN}_coordinator", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=60), - ) + coordinator = SystemBridgeDataUpdateCoordinator(hass, bridge, _LOGGER, entry=entry) + await coordinator.async_config_entry_first_refresh() + + # Wait for initial data + try: + async with async_timeout.timeout(60): + while ( + coordinator.bridge.battery is None + or coordinator.bridge.cpu is None + or coordinator.bridge.filesystem 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( + "Waiting for initial data from %s (%s)", + entry.title, + entry.data[CONF_HOST], + ) + await asyncio.sleep(1) + except asyncio.TimeoutError as exception: + raise ConfigEntryNotReady( + f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." + ) from exception hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - # Fetch initial data so we have data when entities subscribe - await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND): @@ -128,8 +128,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entry in hass.config_entries.async_entries(DOMAIN) if entry.entry_id in device_entry.config_entries ) - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry_id] - bridge: Bridge = coordinator.data + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry_id] + bridge: Bridge = coordinator.bridge _LOGGER.debug( "Command payload: %s", @@ -166,8 +166,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entry in hass.config_entries.async_entries(DOMAIN) if entry.entry_id in device_entry.config_entries ) - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry_id] - bridge: Bridge = coordinator.data + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry_id] + bridge: Bridge = coordinator.bridge _LOGGER.debug("Open payload: %s", {CONF_PATH: path}) try: @@ -190,14 +190,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: schema=SERVICE_OPEN_SCHEMA, ) + # Reload entry when its updated. + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + + # Ensure disconnected and cleanup stop sub + await coordinator.bridge.async_close_websocket() + if coordinator.unsub: + coordinator.unsub() + + del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: hass.services.async_remove(DOMAIN, SERVICE_SEND_COMMAND) @@ -206,13 +218,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class BridgeEntity(CoordinatorEntity): +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload the config entry when it changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class SystemBridgeEntity(CoordinatorEntity): """Defines a base System Bridge entity.""" def __init__( self, - coordinator: DataUpdateCoordinator, - bridge: Bridge, + coordinator: SystemBridgeDataUpdateCoordinator, key: str, name: str, icon: str | None, @@ -220,14 +236,13 @@ class BridgeEntity(CoordinatorEntity): ) -> None: """Initialize the System Bridge entity.""" super().__init__(coordinator) - self._key = f"{bridge.os.hostname}_{key}" - self._name = f"{bridge.os.hostname} {name}" + bridge: Bridge = coordinator.data + self._key = f"{bridge.information.host}_{key}" + self._name = f"{bridge.information.host} {name}" self._icon = icon self._enabled_default = enabled_by_default - self._hostname = bridge.os.hostname - self._default_interface = bridge.network.interfaces[ - bridge.network.interfaceDefault - ] + self._hostname = bridge.information.host + self._mac = bridge.information.mac self._manufacturer = bridge.system.system.manufacturer self._model = bridge.system.system.model self._version = bridge.system.system.version @@ -253,16 +268,14 @@ class BridgeEntity(CoordinatorEntity): return self._enabled_default -class BridgeDeviceEntity(BridgeEntity): +class SystemBridgeDeviceEntity(SystemBridgeEntity): """Defines a System Bridge device entity.""" @property def device_info(self) -> DeviceInfo: """Return device information about this System Bridge instance.""" return { - "connections": { - (dr.CONNECTION_NETWORK_MAC, self._default_interface["mac"]) - }, + "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, "manufacturer": self._manufacturer, "model": self._model, "name": self._hostname, diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index 488aca90bde..f6b765f8079 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -9,30 +9,29 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import BridgeDeviceEntity +from . import SystemBridgeDeviceEntity from .const import DOMAIN +from .coordinator import SystemBridgeDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up System Bridge binary sensor based on a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] bridge: Bridge = coordinator.data - if bridge.battery.hasBattery: - async_add_entities([BridgeBatteryIsChargingBinarySensor(coordinator, bridge)]) + if bridge.battery and bridge.battery.hasBattery: + async_add_entities([SystemBridgeBatteryIsChargingBinarySensor(coordinator)]) -class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): +class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity): """Defines a System Bridge binary sensor.""" def __init__( self, - coordinator: DataUpdateCoordinator, - bridge: Bridge, + coordinator: SystemBridgeDataUpdateCoordinator, key: str, name: str, icon: str | None, @@ -42,7 +41,7 @@ class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): """Initialize System Bridge binary sensor.""" self._device_class = device_class - super().__init__(coordinator, bridge, key, name, icon, enabled_by_default) + super().__init__(coordinator, key, name, icon, enabled_by_default) @property def device_class(self) -> str | None: @@ -50,14 +49,13 @@ class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): return self._device_class -class BridgeBatteryIsChargingBinarySensor(BridgeBinarySensor): +class SystemBridgeBatteryIsChargingBinarySensor(SystemBridgeBinarySensor): """Defines a Battery is charging binary sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge binary sensor.""" super().__init__( coordinator, - bridge, "battery_is_charging", "Battery Is Charging", None, diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 8402a3c1d3e..4ff887c6389 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -8,8 +8,6 @@ import async_timeout from systembridge import Bridge from systembridge.client import BridgeClient from systembridge.exceptions import BridgeAuthenticationException -from systembridge.objects.os import Os -from systembridge.objects.system import System import voluptuous as vol from homeassistant import config_entries, exceptions @@ -47,10 +45,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, hostname = data[CONF_HOST] try: async with async_timeout.timeout(30): - bridge_os: Os = await bridge.async_get_os() - if bridge_os.hostname is not None: - hostname = bridge_os.hostname - bridge_system: System = await bridge.async_get_system() + await bridge.async_get_information() + if ( + bridge.information is not None + and bridge.information.host is not None + and bridge.information.uuid is not None + ): + hostname = bridge.information.host + uuid = bridge.information.uuid except BridgeAuthenticationException as exception: _LOGGER.info(exception) raise InvalidAuth from exception @@ -58,7 +60,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, _LOGGER.info(exception) raise CannotConnect from exception - return {"hostname": hostname, "uuid": bridge_system.uuid.os} + return {"hostname": hostname, "uuid": uuid} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py new file mode 100644 index 00000000000..d34e1019a0b --- /dev/null +++ b/homeassistant/components/system_bridge/coordinator.py @@ -0,0 +1,139 @@ +"""DataUpdateCoordinator for System Bridge.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging +from typing import Callable + +from systembridge import Bridge +from systembridge.exceptions import ( + BridgeAuthenticationException, + BridgeConnectionClosedException, + BridgeException, +) +from systembridge.objects.events import Event + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN + + +class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]): + """Class to manage fetching System Bridge data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + bridge: Bridge, + LOGGER: logging.Logger, + *, + entry: ConfigEntry, + ) -> None: + """Initialize global System Bridge data updater.""" + self.bridge = bridge + self.title = entry.title + self.host = entry.data[CONF_HOST] + self.unsub: Callable | None = None + + super().__init__( + hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) + ) + + def update_listeners(self) -> None: + """Call update on all listeners.""" + for update_callback in self._listeners: + update_callback() + + async def async_handle_event(self, event: Event): + """Handle System Bridge events from the WebSocket.""" + # No need to update anything, as everything is updated in the caller + self.logger.debug( + "New event from %s (%s): %s", self.title, self.host, event.name + ) + self.async_set_updated_data(self.bridge) + + async def _listen_for_events(self) -> None: + """Listen for events from the WebSocket.""" + try: + await self.bridge.async_send_event( + "get-data", + [ + "battery", + "cpu", + "filesystem", + "memory", + "network", + "os", + "processes", + "system", + ], + ) + await self.bridge.listen_for_events(callback=self.async_handle_event) + except BridgeConnectionClosedException as exception: + self.last_update_success = False + self.logger.info( + "Websocket Connection Closed for %s (%s). Will retry: %s", + self.title, + self.host, + exception, + ) + except BridgeException as exception: + self.last_update_success = False + self.update_listeners() + self.logger.warning( + "Exception occurred for %s (%s). Will retry: %s", + self.title, + self.host, + exception, + ) + + async def _setup_websocket(self) -> None: + """Use WebSocket for updates.""" + + try: + self.logger.debug( + "Connecting to ws://%s:%s", + self.host, + self.bridge.information.websocketPort, + ) + await self.bridge.async_connect_websocket( + self.host, self.bridge.information.websocketPort + ) + except BridgeAuthenticationException as exception: + if self.unsub: + self.unsub() + self.unsub = None + raise ConfigEntryAuthFailed() from exception + except (*BRIDGE_CONNECTION_ERRORS, ConnectionRefusedError) as exception: + if self.unsub: + self.unsub() + self.unsub = None + raise UpdateFailed( + f"Could not connect to {self.title} ({self.host})." + ) from exception + asyncio.create_task(self._listen_for_events()) + + async def close_websocket(_) -> None: + """Close WebSocket connection.""" + await self.bridge.async_close_websocket() + + # Clean disconnect WebSocket on Home Assistant shutdown + self.unsub = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket + ) + + async def _async_update_data(self) -> Bridge: + """Update System Bridge data from WebSocket.""" + self.logger.debug( + "_async_update_data - WebSocket Connected: %s", + self.bridge.websocket_connected, + ) + if not self.bridge.websocket_connected: + await self._setup_websocket() + + return self.bridge diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 0a800657009..2f1ec0111cf 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,10 +3,10 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridge==1.1.5"], + "requirements": ["systembridge==2.0.6"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._udp.local."], "after_dependencies": ["zeroconf"], "quality_scale": "silver", - "iot_class": "local_polling" + "iot_class": "local_push" } diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index ea7fc628e76..ed3c569f10f 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -20,10 +20,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import BridgeDeviceEntity +from . import SystemBridgeDeviceEntity from .const import DOMAIN +from .coordinator import SystemBridgeDataUpdateCoordinator ATTR_AVAILABLE = "available" ATTR_FILESYSTEM = "filesystem" @@ -41,40 +41,38 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up System Bridge sensor based on a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - bridge: Bridge = coordinator.data + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities = [ - BridgeCpuSpeedSensor(coordinator, bridge), - BridgeCpuTemperatureSensor(coordinator, bridge), - BridgeCpuVoltageSensor(coordinator, bridge), + SystemBridgeCpuSpeedSensor(coordinator), + SystemBridgeCpuTemperatureSensor(coordinator), + SystemBridgeCpuVoltageSensor(coordinator), *( - BridgeFilesystemSensor(coordinator, bridge, key) - for key, _ in bridge.filesystem.fsSize.items() + SystemBridgeFilesystemSensor(coordinator, key) + for key, _ in coordinator.data.filesystem.fsSize.items() ), - BridgeMemoryFreeSensor(coordinator, bridge), - BridgeMemoryUsedSensor(coordinator, bridge), - BridgeMemoryUsedPercentageSensor(coordinator, bridge), - BridgeKernelSensor(coordinator, bridge), - BridgeOsSensor(coordinator, bridge), - BridgeProcessesLoadSensor(coordinator, bridge), - BridgeBiosVersionSensor(coordinator, bridge), + SystemBridgeMemoryFreeSensor(coordinator), + SystemBridgeMemoryUsedSensor(coordinator), + SystemBridgeMemoryUsedPercentageSensor(coordinator), + SystemBridgeKernelSensor(coordinator), + SystemBridgeOsSensor(coordinator), + SystemBridgeProcessesLoadSensor(coordinator), + SystemBridgeBiosVersionSensor(coordinator), ] - if bridge.battery.hasBattery: - entities.append(BridgeBatterySensor(coordinator, bridge)) - entities.append(BridgeBatteryTimeRemainingSensor(coordinator, bridge)) + if coordinator.data.battery.hasBattery: + entities.append(SystemBridgeBatterySensor(coordinator)) + entities.append(SystemBridgeBatteryTimeRemainingSensor(coordinator)) async_add_entities(entities) -class BridgeSensor(BridgeDeviceEntity, SensorEntity): +class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): """Defines a System Bridge sensor.""" def __init__( self, - coordinator: DataUpdateCoordinator, - bridge: Bridge, + coordinator: SystemBridgeDataUpdateCoordinator, key: str, name: str, icon: str | None, @@ -86,7 +84,7 @@ class BridgeSensor(BridgeDeviceEntity, SensorEntity): self._device_class = device_class self._unit_of_measurement = unit_of_measurement - super().__init__(coordinator, bridge, key, name, icon, enabled_by_default) + super().__init__(coordinator, key, name, icon, enabled_by_default) @property def device_class(self) -> str | None: @@ -99,14 +97,13 @@ class BridgeSensor(BridgeDeviceEntity, SensorEntity): return self._unit_of_measurement -class BridgeBatterySensor(BridgeSensor): +class SystemBridgeBatterySensor(SystemBridgeSensor): """Defines a Battery sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "battery", "Battery", None, @@ -122,14 +119,13 @@ class BridgeBatterySensor(BridgeSensor): return bridge.battery.percent -class BridgeBatteryTimeRemainingSensor(BridgeSensor): +class SystemBridgeBatteryTimeRemainingSensor(SystemBridgeSensor): """Defines the Battery Time Remaining sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "battery_time_remaining", "Battery Time Remaining", None, @@ -147,14 +143,13 @@ class BridgeBatteryTimeRemainingSensor(BridgeSensor): return str(datetime.now() + timedelta(minutes=bridge.battery.timeRemaining)) -class BridgeCpuSpeedSensor(BridgeSensor): +class SystemBridgeCpuSpeedSensor(SystemBridgeSensor): """Defines a CPU speed sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "cpu_speed", "CPU Speed", "mdi:speedometer", @@ -170,14 +165,13 @@ class BridgeCpuSpeedSensor(BridgeSensor): return bridge.cpu.currentSpeed.avg -class BridgeCpuTemperatureSensor(BridgeSensor): +class SystemBridgeCpuTemperatureSensor(SystemBridgeSensor): """Defines a CPU temperature sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "cpu_temperature", "CPU Temperature", None, @@ -193,14 +187,13 @@ class BridgeCpuTemperatureSensor(BridgeSensor): return bridge.cpu.temperature.main -class BridgeCpuVoltageSensor(BridgeSensor): +class SystemBridgeCpuVoltageSensor(SystemBridgeSensor): """Defines a CPU voltage sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "cpu_voltage", "CPU Voltage", None, @@ -216,17 +209,16 @@ class BridgeCpuVoltageSensor(BridgeSensor): return bridge.cpu.cpu.voltage -class BridgeFilesystemSensor(BridgeSensor): +class SystemBridgeFilesystemSensor(SystemBridgeSensor): """Defines a filesystem sensor.""" def __init__( - self, coordinator: DataUpdateCoordinator, bridge: Bridge, key: str + self, coordinator: SystemBridgeDataUpdateCoordinator, key: str ) -> None: """Initialize System Bridge sensor.""" uid_key = key.replace(":", "") super().__init__( coordinator, - bridge, f"filesystem_{uid_key}", f"{key} Space Used", "mdi:harddisk", @@ -260,14 +252,13 @@ class BridgeFilesystemSensor(BridgeSensor): } -class BridgeMemoryFreeSensor(BridgeSensor): +class SystemBridgeMemoryFreeSensor(SystemBridgeSensor): """Defines a memory free sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "memory_free", "Memory Free", "mdi:memory", @@ -287,14 +278,13 @@ class BridgeMemoryFreeSensor(BridgeSensor): ) -class BridgeMemoryUsedSensor(BridgeSensor): +class SystemBridgeMemoryUsedSensor(SystemBridgeSensor): """Defines a memory used sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "memory_used", "Memory Used", "mdi:memory", @@ -314,14 +304,13 @@ class BridgeMemoryUsedSensor(BridgeSensor): ) -class BridgeMemoryUsedPercentageSensor(BridgeSensor): +class SystemBridgeMemoryUsedPercentageSensor(SystemBridgeSensor): """Defines a memory used percentage sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "memory_used_percentage", "Memory Used %", "mdi:memory", @@ -341,14 +330,13 @@ class BridgeMemoryUsedPercentageSensor(BridgeSensor): ) -class BridgeKernelSensor(BridgeSensor): +class SystemBridgeKernelSensor(SystemBridgeSensor): """Defines a kernel sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "kernel", "Kernel", "mdi:devices", @@ -364,14 +352,13 @@ class BridgeKernelSensor(BridgeSensor): return bridge.os.kernel -class BridgeOsSensor(BridgeSensor): +class SystemBridgeOsSensor(SystemBridgeSensor): """Defines an OS sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "os", "Operating System", "mdi:devices", @@ -387,14 +374,13 @@ class BridgeOsSensor(BridgeSensor): return f"{bridge.os.distro} {bridge.os.release}" -class BridgeProcessesLoadSensor(BridgeSensor): +class SystemBridgeProcessesLoadSensor(SystemBridgeSensor): """Defines a Processes Load sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "processes_load", "Load", "mdi:percent", @@ -429,14 +415,13 @@ class BridgeProcessesLoadSensor(BridgeSensor): return attrs -class BridgeBiosVersionSensor(BridgeSensor): +class SystemBridgeBiosVersionSensor(SystemBridgeSensor): """Defines a bios version sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + def __init__(self, coordinator: SystemBridgeDataUpdateCoordinator) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, - bridge, "bios_version", "BIOS Version", "mdi:chip", diff --git a/requirements_all.txt b/requirements_all.txt index df6f90b8854..010c42b6e74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2234,7 +2234,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridge==1.1.5 +systembridge==2.0.6 # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5ef5035db0..90bafb5efe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1232,7 +1232,7 @@ sunwatcher==0.2.1 surepy==0.7.0 # homeassistant.components.system_bridge -systembridge==1.1.5 +systembridge==2.0.6 # homeassistant.components.tellduslive tellduslive==0.10.11 diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 5cd7a77d911..96603c39bcd 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -55,79 +55,24 @@ FIXTURE_ZEROCONF_BAD = { }, } -FIXTURE_OS = { - "platform": "linux", - "distro": "Ubuntu", - "release": "20.10", - "codename": "Groovy Gorilla", - "kernel": "5.8.0-44-generic", - "arch": "x64", - "hostname": "test-bridge", - "fqdn": "test-bridge.local", - "codepage": "UTF-8", - "logofile": "ubuntu", - "serial": "abcdefghijklmnopqrstuvwxyz", - "build": "", - "servicepack": "", - "uefi": True, - "users": [], -} - -FIXTURE_NETWORK = { - "connections": [], - "gatewayDefault": "192.168.1.1", - "interfaceDefault": "wlp2s0", - "interfaces": { - "wlp2s0": { - "iface": "wlp2s0", - "ifaceName": "wlp2s0", - "ip4": "1.1.1.1", - "mac": FIXTURE_MAC_ADDRESS, - }, - }, - "stats": {}, -} - -FIXTURE_SYSTEM = { - "baseboard": { - "manufacturer": "System manufacturer", - "model": "Model", - "version": "Rev X.0x", - "serial": "1234567", - "assetTag": "", - "memMax": 134217728, - "memSlots": 4, - }, - "bios": { - "vendor": "System vendor", - "version": "12345", - "releaseDate": "2019-11-13", - "revision": "", - }, - "chassis": { - "manufacturer": "Default string", - "model": "", - "type": "Desktop", - "version": "Default string", - "serial": "Default string", - "assetTag": "", - "sku": "", - }, - "system": { - "manufacturer": "System manufacturer", - "model": "System Product Name", - "version": "System Version", - "serial": "System Serial Number", - "uuid": "abc123-def456", - "sku": "SKU", - "virtual": False, - }, - "uuid": { - "os": FIXTURE_UUID, - "hardware": "abc123-def456", - "macs": [FIXTURE_MAC_ADDRESS], +FIXTURE_INFORMATION = { + "address": "http://test-bridge:9170", + "apiPort": 9170, + "fqdn": "test-bridge", + "host": "test-bridge", + "ip": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + "updates": { + "available": False, + "newer": False, + "url": "https://github.com/timmo001/system-bridge/releases/tag/v2.3.2", + "version": {"current": "2.3.2", "new": "2.3.2"}, }, + "uuid": FIXTURE_UUID, + "version": "2.3.2", + "websocketAddress": "ws://test-bridge:9172", + "websocketPort": 9172, } @@ -151,9 +96,11 @@ async def test_user_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", json=FIXTURE_OS) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", json=FIXTURE_NETWORK) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", json=FIXTURE_SYSTEM) + aioclient_mock.get( + f"{FIXTURE_BASE_URL}/information", + headers={"Content-Type": "application/json"}, + json=FIXTURE_INFORMATION, + ) with patch( "homeassistant.components.system_bridge.async_setup_entry", @@ -181,9 +128,9 @@ async def test_form_invalid_auth( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=BridgeAuthenticationException) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=BridgeAuthenticationException) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=BridgeAuthenticationException) + aioclient_mock.get( + f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -206,9 +153,7 @@ async def test_form_cannot_connect( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=ClientConnectionError) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=ClientConnectionError) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -220,7 +165,7 @@ async def test_form_cannot_connect( assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_unknow_error( +async def test_form_unknown_error( hass, aiohttp_client, aioclient_mock, current_request_with_host ) -> None: """Test we handle unknown error.""" @@ -232,10 +177,9 @@ async def test_form_unknow_error( assert result["errors"] is None with patch( - "homeassistant.components.system_bridge.config_flow.Bridge.async_get_os", + "homeassistant.components.system_bridge.config_flow.Bridge.async_get_information", side_effect=Exception("Boom"), ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT ) @@ -257,9 +201,9 @@ async def test_reauth_authorization_error( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=BridgeAuthenticationException) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=BridgeAuthenticationException) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=BridgeAuthenticationException) + aioclient_mock.get( + f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -282,9 +226,7 @@ async def test_reauth_connection_error( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=ClientConnectionError) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=ClientConnectionError) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -312,9 +254,11 @@ async def test_reauth_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", json=FIXTURE_OS) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", json=FIXTURE_NETWORK) - aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", json=FIXTURE_SYSTEM) + aioclient_mock.get( + f"{FIXTURE_BASE_URL}/information", + headers={"Content-Type": "application/json"}, + json=FIXTURE_INFORMATION, + ) with patch( "homeassistant.components.system_bridge.async_setup_entry", @@ -345,9 +289,11 @@ async def test_zeroconf_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert not result["errors"] - aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/os", json=FIXTURE_OS) - aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/network", json=FIXTURE_NETWORK) - aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/system", json=FIXTURE_SYSTEM) + aioclient_mock.get( + f"{FIXTURE_ZEROCONF_BASE_URL}/information", + headers={"Content-Type": "application/json"}, + json=FIXTURE_INFORMATION, + ) with patch( "homeassistant.components.system_bridge.async_setup_entry", @@ -378,11 +324,9 @@ async def test_zeroconf_cannot_connect( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert not result["errors"] - aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/os", exc=ClientConnectionError) aioclient_mock.get( - f"{FIXTURE_ZEROCONF_BASE_URL}/network", exc=ClientConnectionError + f"{FIXTURE_ZEROCONF_BASE_URL}/information", exc=ClientConnectionError ) - aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/system", exc=ClientConnectionError) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT