Skip to content

Commit e4022c5

Browse files
committed
Fixes #6
1 parent 452d338 commit e4022c5

File tree

4 files changed

+118
-26
lines changed

4 files changed

+118
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [1.1.0] - 2025-10-14
99

1010
### Added
11-
- Cached values feature: Integration now remembers last known sensor values when printer is offline
11+
- Cached values feature: Integration now remembers last known sensor values when printer is offline (Issue #3)
1212
- Offline status indication in sensor attributes with timestamp of last successful data fetch
1313
- Status sensor now shows "offline" state when using cached data
1414

15+
### Fixed
16+
- Reduced excessive SNMP error logging when printer is offline (Issue #6)
17+
- SNMP errors now log once as ERROR, then at WARNING level every 5 minutes to prevent log spam
18+
- Connection recovery is properly logged when printer comes back online
19+
1520
## [1.0.0] - 2025-10-01
1621

1722
### Added

custom_components/snmp_printer/__init__.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
8484

8585
# Create storage for cached data
8686
store = Store(hass, STORAGE_VERSION, f"{STORAGE_KEY}_{entry.entry_id}")
87-
87+
8888
# Load cached data
8989
cached_data = await store.async_load() or {}
9090

@@ -109,38 +109,52 @@ async def async_update_data():
109109
entry.data[CONF_HOST], hass
110110
),
111111
}
112-
112+
113113
# Save successful data to cache with timestamp
114114
cache_data = {
115115
"data": data,
116116
"timestamp": datetime.now().isoformat(),
117-
"host": entry.data[CONF_HOST]
117+
"host": entry.data[CONF_HOST],
118118
}
119119
await store.async_save(cache_data)
120-
120+
121121
# Mark as online
122122
data["is_online"] = True
123-
123+
124124
return data
125125
except Exception as err:
126126
# Check if this is a connection-related error
127127
error_msg = str(err).lower()
128-
is_connection_error = any(keyword in error_msg for keyword in [
129-
"timeout", "unreachable", "no route", "connection", "network",
130-
"host", "refused", "failed", "no response"
131-
])
132-
128+
is_connection_error = any(
129+
keyword in error_msg
130+
for keyword in [
131+
"timeout",
132+
"unreachable",
133+
"no route",
134+
"connection",
135+
"network",
136+
"host",
137+
"refused",
138+
"failed",
139+
"no response",
140+
]
141+
)
142+
133143
# If we have cached data and this is a connection issue, return cached data
134144
if cached_data.get("data") and is_connection_error:
135-
_LOGGER.warning("Printer %s is offline (%s), using cached data from %s",
136-
entry.data[CONF_HOST], err, cached_data.get("timestamp", "unknown"))
137-
145+
_LOGGER.warning(
146+
"Printer %s is offline (%s), using cached data from %s",
147+
entry.data[CONF_HOST],
148+
err,
149+
cached_data.get("timestamp", "unknown"),
150+
)
151+
138152
cached_printer_data = cached_data["data"].copy()
139153
cached_printer_data["is_online"] = False
140154
cached_printer_data["offline_since"] = cached_data.get("timestamp")
141-
155+
142156
return cached_printer_data
143-
157+
144158
# For other errors or when we don't have cached data, re-raise the error
145159
raise UpdateFailed(f"Error fetching printer data: {err}") from err
146160

custom_components/snmp_printer/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
DEFAULT_UPDATE_INTERVAL: Final = 60
2121
DEFAULT_SNMP_VERSION: Final = "2c"
2222

23+
# Error logging configuration
24+
DEFAULT_ERROR_LOG_INTERVAL: Final = (
25+
300 # Log offline errors at most once every 5 minutes
26+
)
27+
2328
# SNMP OIDs based on RFC 3805 (Printer MIB) and RFC 1213 (MIB-II)
2429
# System information
2530
OID_SYSTEM_DESCRIPTION: Final = "1.3.6.1.2.1.1.1.0"

custom_components/snmp_printer/snmp_client.py

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import logging
66
import re
7+
import time
78
from typing import Any
89

910
from pysnmp.hlapi.v3arch.asyncio import (
@@ -25,8 +26,11 @@
2526
from pysnmp.proto.rfc1902 import OctetString
2627

2728
from .const import (
29+
DEFAULT_ERROR_LOG_INTERVAL,
2830
DEVICE_STATUS,
31+
OID_COVER_DESCRIPTION,
2932
OID_COVER_STATUS,
33+
OID_DEVICE_DESCRIPTION,
3034
OID_DEVICE_ERRORS,
3135
OID_DEVICE_STATE,
3236
OID_DISPLAY_BUFFER,
@@ -41,6 +45,8 @@
4145
OID_MARKER_SUPPLIES_TYPE,
4246
OID_MEMORY_SIZE,
4347
OID_PAGE_COUNT,
48+
OID_PRINTER_ERRORS,
49+
OID_PRINTER_STATUS,
4450
OID_SERIAL_NUMBER,
4551
OID_SYSTEM_CONTACT,
4652
OID_SYSTEM_DESCRIPTION,
@@ -93,12 +99,73 @@ def __init__(
9399
self._transport = None # Will be created async
94100
self._auth_data = self._get_auth_data()
95101

102+
# Connection state tracking for better error logging
103+
self._connection_state = "unknown" # unknown, online, offline
104+
self._last_error_log_time = 0
105+
self._consecutive_failures = 0
106+
96107
def _create_engine(self):
97108
"""Create SNMP engine (blocking operation)."""
98109
if self._engine is None:
99110
self._engine = SnmpEngine()
100111
return self._engine
101112

113+
def _handle_snmp_error(self, error_message: str) -> None:
114+
"""Handle SNMP errors with intelligent logging to reduce spam."""
115+
current_time = time.time()
116+
self._consecutive_failures += 1
117+
118+
# Determine if we should log this error
119+
should_log_error = False
120+
log_level = logging.ERROR
121+
122+
if self._connection_state == "unknown" or self._connection_state == "online":
123+
# First error or was previously online - log as error
124+
should_log_error = True
125+
self._connection_state = "offline"
126+
self._last_error_log_time = current_time
127+
elif self._connection_state == "offline":
128+
# Already offline - check if enough time has passed for another log
129+
time_since_last_log = current_time - self._last_error_log_time
130+
if time_since_last_log >= DEFAULT_ERROR_LOG_INTERVAL:
131+
should_log_error = True
132+
log_level = logging.WARNING # Reduce severity for ongoing issues
133+
self._last_error_log_time = current_time
134+
135+
if should_log_error:
136+
if self._consecutive_failures == 1:
137+
_LOGGER.error(
138+
"Printer %s: %s (switching to cached data if available)",
139+
self.host,
140+
error_message,
141+
)
142+
else:
143+
_LOGGER.warning(
144+
"Printer %s still offline: %s (attempt %d, using cached data)",
145+
self.host,
146+
error_message,
147+
self._consecutive_failures,
148+
)
149+
else:
150+
# Still log at debug level for troubleshooting
151+
_LOGGER.debug(
152+
"Printer %s offline: %s (suppressed, attempt %d)",
153+
self.host,
154+
error_message,
155+
self._consecutive_failures,
156+
)
157+
158+
def _mark_connection_success(self) -> None:
159+
"""Mark a successful connection to reset error tracking."""
160+
if self._connection_state == "offline":
161+
_LOGGER.info(
162+
"Printer %s is back online after %d failed attempts",
163+
self.host,
164+
self._consecutive_failures,
165+
)
166+
self._connection_state = "online"
167+
self._consecutive_failures = 0
168+
102169
async def _ensure_transport(self):
103170
"""Ensure transport and engine are created (async operation)."""
104171
if self._engine is None:
@@ -160,17 +227,16 @@ async def _get_oid(self, oid: str) -> Any:
160227
)
161228

162229
if errorIndication:
163-
_LOGGER.debug("SNMP error: %s", errorIndication)
230+
self._handle_snmp_error(f"SNMP error: {errorIndication}")
164231
return None
165232
elif errorStatus:
166-
_LOGGER.debug(
167-
"SNMP error: %s at %s",
168-
errorStatus.prettyPrint(),
169-
errorIndex and varBinds[int(errorIndex) - 1][0] or "?",
233+
self._handle_snmp_error(
234+
f"SNMP error: {errorStatus.prettyPrint()} at {errorIndex and varBinds[int(errorIndex) - 1][0] or '?'}"
170235
)
171236
return None
172237

173238
for varBind in varBinds:
239+
self._mark_connection_success()
174240
return varBind[1].prettyPrint()
175241

176242
return None
@@ -191,16 +257,18 @@ async def _walk_oid(self, oid: str) -> dict[str, str]:
191257
lexicographicMode=False,
192258
):
193259
if errorIndication:
194-
_LOGGER.error("SNMP walk error: %s", errorIndication)
260+
self._handle_snmp_error(f"SNMP walk error: {errorIndication}")
195261
break
196262
elif errorStatus:
197-
_LOGGER.error(
198-
"SNMP walk error: %s at %s",
199-
errorStatus.prettyPrint(),
200-
errorIndex and varBinds[int(errorIndex) - 1][0] or "?",
263+
self._handle_snmp_error(
264+
f"SNMP walk error: {errorStatus.prettyPrint()} at {errorIndex and varBinds[int(errorIndex) - 1][0] or '?'}"
201265
)
202266
break
203267
else:
268+
# Mark success if we get any data
269+
if not results and varBinds:
270+
self._mark_connection_success()
271+
204272
for varBind in varBinds:
205273
# Extract the index from the OID (last part after the base OID)
206274
full_oid = str(varBind[0])

0 commit comments

Comments
 (0)