Automatically enable/disable zwave_js server logging in lib (#100837)

* Bump zwave-js-server-python to 0.52.0

* Add WS command to enabled zwave_js server logging in lib

* enable and disable server logging automatically

* fix conditionals

* fix tests

* Add logging

* small tweaks

* Add logger as a dependency

* fix test

* Prepare for movement of event constant

* Add constant so tests pass
This commit is contained in:
Raman Gupta 2023-09-27 01:17:52 -04:00 committed by GitHub
parent 067b94899f
commit 70e3da207a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 12 deletions

View file

@ -5,6 +5,7 @@ import asyncio
from collections import defaultdict from collections import defaultdict
from collections.abc import Coroutine from collections.abc import Coroutine
from contextlib import suppress from contextlib import suppress
import logging
from typing import Any from typing import Any
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
@ -29,6 +30,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
CONF_URL, CONF_URL,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
EVENT_LOGGING_CHANGED,
Platform, Platform,
) )
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
@ -93,6 +95,7 @@ from .const import (
DATA_CLIENT, DATA_CLIENT,
DOMAIN, DOMAIN,
EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_DEVICE_ADDED_TO_REGISTRY,
LIB_LOGGER,
LOGGER, LOGGER,
USER_AGENT, USER_AGENT,
ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_NOTIFICATION_EVENT,
@ -105,6 +108,8 @@ from .discovery import (
async_discover_single_value, async_discover_single_value,
) )
from .helpers import ( from .helpers import (
async_disable_server_logging_if_needed,
async_enable_server_logging_if_needed,
async_enable_statistics, async_enable_statistics,
get_device_id, get_device_id,
get_device_id_ext, get_device_id_ext,
@ -249,6 +254,24 @@ class DriverEvents:
elif opted_in is False: elif opted_in is False:
await driver.async_disable_statistics() await driver.async_disable_statistics()
async def handle_logging_changed(_: Event | None = None) -> None:
"""Handle logging changed event."""
if LIB_LOGGER.isEnabledFor(logging.DEBUG):
await async_enable_server_logging_if_needed(
self.hass, self.config_entry, driver
)
else:
await async_disable_server_logging_if_needed(
self.hass, self.config_entry, driver
)
# Set up server logging on setup if needed
await handle_logging_changed()
self.config_entry.async_on_unload(
self.hass.bus.async_listen(EVENT_LOGGING_CHANGED, handle_logging_changed)
)
# Check for nodes that no longer exist and remove them # Check for nodes that no longer exist and remove them
stored_devices = dr.async_entries_for_config_entry( stored_devices = dr.async_entries_for_config_entry(
self.dev_reg, self.config_entry.entry_id self.dev_reg, self.config_entry.entry_id
@ -902,6 +925,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(await asyncio.gather(*tasks)) if tasks else True unload_ok = all(await asyncio.gather(*tasks)) if tasks else True
if hasattr(driver_events, "driver"):
await async_disable_server_logging_if_needed(hass, entry, driver_events.driver)
if DATA_CLIENT_LISTEN_TASK in info: if DATA_CLIENT_LISTEN_TASK in info:
await disconnect_client(hass, entry) await disconnect_client(hass, entry)

View file

@ -82,7 +82,6 @@ from .const import (
from .helpers import ( from .helpers import (
async_enable_statistics, async_enable_statistics,
async_get_node_from_device_id, async_get_node_from_device_id,
async_update_data_collection_preference,
get_device_id, get_device_id,
) )
@ -490,6 +489,7 @@ async def websocket_network_status(
"state": "connected" if client.connected else "disconnected", "state": "connected" if client.connected else "disconnected",
"driver_version": client_version_info.driver_version, "driver_version": client_version_info.driver_version,
"server_version": client_version_info.server_version, "server_version": client_version_info.server_version,
"server_logging_enabled": client.server_logging_enabled,
}, },
"controller": { "controller": {
"home_id": controller.home_id, "home_id": controller.home_id,
@ -1875,7 +1875,10 @@ async def websocket_update_data_collection_preference(
) -> None: ) -> None:
"""Update preference for data collection and enable/disable collection.""" """Update preference for data collection and enable/disable collection."""
opted_in = msg[OPTED_IN] opted_in = msg[OPTED_IN]
async_update_data_collection_preference(hass, entry, opted_in) if entry.data.get(CONF_DATA_COLLECTION_OPTED_IN) != opted_in:
new_data = entry.data.copy()
new_data[CONF_DATA_COLLECTION_OPTED_IN] = opted_in
hass.config_entries.async_update_entry(entry, data=new_data)
if opted_in: if opted_in:
await async_enable_statistics(driver) await async_enable_statistics(driver)

View file

@ -31,10 +31,12 @@ CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in"
DOMAIN = "zwave_js" DOMAIN = "zwave_js"
DATA_CLIENT = "client" DATA_CLIENT = "client"
DATA_OLD_SERVER_LOG_LEVEL = "old_server_log_level"
EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
LIB_LOGGER = logging.getLogger("zwave_js_server")
# constants extra state attributes # constants extra state attributes
ATTR_RESERVED_VALUES = "reserved_values" # ConfigurationValue number entities ATTR_RESERVED_VALUES = "reserved_values" # ConfigurationValue number entities

View file

@ -8,8 +8,14 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandClass, ConfigurationValueType from zwave_js_server.const import (
LOG_LEVEL_MAP,
CommandClass,
ConfigurationValueType,
LogLevel,
)
from zwave_js_server.model.driver import Driver from zwave_js_server.model.driver import Driver
from zwave_js_server.model.log_config import LogConfig
from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import ( from zwave_js_server.model.value import (
ConfigurationValue, ConfigurationValue,
@ -39,9 +45,10 @@ from .const import (
ATTR_ENDPOINT, ATTR_ENDPOINT,
ATTR_PROPERTY, ATTR_PROPERTY,
ATTR_PROPERTY_KEY, ATTR_PROPERTY_KEY,
CONF_DATA_COLLECTION_OPTED_IN,
DATA_CLIENT, DATA_CLIENT,
DATA_OLD_SERVER_LOG_LEVEL,
DOMAIN, DOMAIN,
LIB_LOGGER,
LOGGER, LOGGER,
) )
@ -127,14 +134,68 @@ async def async_enable_statistics(driver: Driver) -> None:
await driver.async_enable_statistics("Home Assistant", HA_VERSION) await driver.async_enable_statistics("Home Assistant", HA_VERSION)
@callback async def async_enable_server_logging_if_needed(
def async_update_data_collection_preference( hass: HomeAssistant, entry: ConfigEntry, driver: Driver
hass: HomeAssistant, entry: ConfigEntry, preference: bool
) -> None: ) -> None:
"""Update data collection preference on config entry.""" """Enable logging of zwave-js-server in the lib."""
new_data = entry.data.copy() # If lib log level is set to debug, we want to enable server logging. First we
new_data[CONF_DATA_COLLECTION_OPTED_IN] = preference # check if server log level is less verbose than library logging, and if so, set it
hass.config_entries.async_update_entry(entry, data=new_data) # to debug to match library logging. We will store the old server log level in
# hass.data so we can reset it later
if (
not driver
or not driver.client.connected
or driver.client.server_logging_enabled
):
return
LOGGER.info("Enabling zwave-js-server logging")
if (curr_server_log_level := driver.log_config.level) and (
LOG_LEVEL_MAP[curr_server_log_level]
) > (lib_log_level := LIB_LOGGER.getEffectiveLevel()):
entry_data = hass.data[DOMAIN][entry.entry_id]
LOGGER.warning(
(
"Server logging is set to %s and is currently less verbose "
"than library logging, setting server log level to %s to match"
),
curr_server_log_level,
logging.getLevelName(lib_log_level),
)
entry_data[DATA_OLD_SERVER_LOG_LEVEL] = curr_server_log_level
await driver.async_update_log_config(LogConfig(level=LogLevel.DEBUG))
await driver.client.enable_server_logging()
LOGGER.info("Zwave-js-server logging is enabled")
async def async_disable_server_logging_if_needed(
hass: HomeAssistant, entry: ConfigEntry, driver: Driver
) -> None:
"""Disable logging of zwave-js-server in the lib if still connected to server."""
entry_data = hass.data[DOMAIN][entry.entry_id]
if (
not driver
or not driver.client.connected
or not driver.client.server_logging_enabled
):
return
LOGGER.info("Disabling zwave_js server logging")
if (
DATA_OLD_SERVER_LOG_LEVEL in entry_data
and (old_server_log_level := entry_data.pop(DATA_OLD_SERVER_LOG_LEVEL))
!= driver.log_config.level
):
LOGGER.info(
(
"Server logging is currently set to %s as a result of server logging "
"being enabled. It is now being reset to %s"
),
driver.log_config.level,
old_server_log_level,
)
await driver.async_update_log_config(LogConfig(level=old_server_log_level))
await driver.client.disable_server_logging()
LOGGER.info("Zwave-js-server logging is enabled")
def get_valueless_base_unique_id(driver: Driver, node: ZwaveNode) -> str: def get_valueless_base_unique_id(driver: Driver, node: ZwaveNode) -> str:

View file

@ -3,7 +3,7 @@
"name": "Z-Wave", "name": "Z-Wave",
"codeowners": ["@home-assistant/z-wave"], "codeowners": ["@home-assistant/z-wave"],
"config_flow": true, "config_flow": true,
"dependencies": ["usb", "http", "repairs", "websocket_api"], "dependencies": ["http", "repairs", "usb", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/zwave_js", "documentation": "https://www.home-assistant.io/integrations/zwave_js",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",

View file

@ -126,12 +126,14 @@ async def test_network_status(
hass: HomeAssistant, hass: HomeAssistant,
multisensor_6, multisensor_6,
controller_state, controller_state,
client,
integration, integration,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
) -> None: ) -> None:
"""Test the network status websocket command.""" """Test the network status websocket command."""
entry = integration entry = integration
ws_client = await hass_ws_client(hass) ws_client = await hass_ws_client(hass)
client.server_logging_enabled = False
# Try API call with entry ID # Try API call with entry ID
with patch( with patch(
@ -150,6 +152,7 @@ async def test_network_status(
assert result["client"]["ws_server_url"] == "ws://test:3000/zjs" assert result["client"]["ws_server_url"] == "ws://test:3000/zjs"
assert result["client"]["server_version"] == "1.0.0" assert result["client"]["server_version"] == "1.0.0"
assert not result["client"]["server_logging_enabled"]
assert result["controller"]["inclusion_state"] == InclusionState.IDLE assert result["controller"]["inclusion_state"] == InclusionState.IDLE
# Try API call with device ID # Try API call with device ID

View file

@ -1,6 +1,7 @@
"""Test the Z-Wave JS init module.""" """Test the Z-Wave JS init module."""
import asyncio import asyncio
from copy import deepcopy from copy import deepcopy
import logging
from unittest.mock import AsyncMock, call, patch from unittest.mock import AsyncMock, call, patch
import pytest import pytest
@ -11,6 +12,7 @@ from zwave_js_server.model.node import Node
from zwave_js_server.model.version import VersionInfo from zwave_js_server.model.version import VersionInfo
from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.logger import DOMAIN as LOGGER_DOMAIN, SERVICE_SET_LEVEL
from homeassistant.components.persistent_notification import async_dismiss from homeassistant.components.persistent_notification import async_dismiss
from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js import DOMAIN
from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.components.zwave_js.helpers import get_device_id
@ -23,6 +25,7 @@ from homeassistant.helpers import (
entity_registry as er, entity_registry as er,
issue_registry as ir, issue_registry as ir,
) )
from homeassistant.setup import async_setup_component
from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY
@ -1550,3 +1553,94 @@ async def test_identify_event(
assert len(notifications) == 1 assert len(notifications) == 1
assert list(notifications)[0] == msg_id assert list(notifications)[0] == msg_id
assert "network with the home ID `3245146787`" in notifications[msg_id]["message"] assert "network with the home ID `3245146787`" in notifications[msg_id]["message"]
async def test_server_logging(hass: HomeAssistant, client) -> None:
"""Test automatic server logging functionality."""
def _reset_mocks():
client.async_send_command.reset_mock()
client.enable_server_logging.reset_mock()
client.disable_server_logging.reset_mock()
# Set server logging to disabled
client.server_logging_enabled = False
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Setup logger and set log level to debug to trigger event listener
assert await async_setup_component(hass, "logger", {"logger": {}})
assert logging.getLogger("zwave_js_server").getEffectiveLevel() == logging.INFO
client.async_send_command.reset_mock()
await hass.services.async_call(
LOGGER_DOMAIN, SERVICE_SET_LEVEL, {"zwave_js_server": "debug"}, blocking=True
)
await hass.async_block_till_done()
assert logging.getLogger("zwave_js_server").getEffectiveLevel() == logging.DEBUG
# Validate that the server logging was enabled
assert len(client.async_send_command.call_args_list) == 1
assert client.async_send_command.call_args[0][0] == {
"command": "driver.update_log_config",
"config": {"level": "debug"},
}
assert client.enable_server_logging.called
assert not client.disable_server_logging.called
_reset_mocks()
# Emulate server by setting log level to debug
event = Event(
type="log config updated",
data={
"source": "driver",
"event": "log config updated",
"config": {
"enabled": False,
"level": "debug",
"logToFile": True,
"filename": "test",
"forceConsole": True,
},
},
)
client.driver.receive_event(event)
# "Enable" server logging and unload the entry
client.server_logging_enabled = True
await hass.config_entries.async_unload(entry.entry_id)
# Validate that the server logging was disabled
assert len(client.async_send_command.call_args_list) == 1
assert client.async_send_command.call_args[0][0] == {
"command": "driver.update_log_config",
"config": {"level": "info"},
}
assert not client.enable_server_logging.called
assert client.disable_server_logging.called
_reset_mocks()
# Validate that the server logging doesn't get enabled because HA thinks it already
# is enabled
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(client.async_send_command.call_args_list) == 0
assert not client.enable_server_logging.called
assert not client.disable_server_logging.called
_reset_mocks()
# "Disable" server logging and unload the entry
client.server_logging_enabled = False
await hass.config_entries.async_unload(entry.entry_id)
# Validate that the server logging was not disabled because HA thinks it is already
# is disabled
assert len(client.async_send_command.call_args_list) == 0
assert not client.enable_server_logging.called
assert not client.disable_server_logging.called