Skip to content

Commit 6bc53c2

Browse files
authored
Merge pull request smarthomeNG#1028 from Morg42/smartmeter
smartmeter: add docs, improve webif
2 parents c4c047e + d38b23f commit 6bc53c2

8 files changed

Lines changed: 559 additions & 45 deletions

File tree

smartmeter/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ def __init__(self, sh):
115115
# load parameters from config
116116
self._load_parameters()
117117

118+
self.discovery_logs = {}
119+
self.item_file = None
120+
118121
# quit if errors on parameter read
119122
if not self._init_complete:
120123
return
@@ -133,6 +136,8 @@ def discover(self, protocol=None) -> bool:
133136
else:
134137
disc_protos = [protocol]
135138

139+
self.discovery_logs = {proto: None for proto in disc_protos}
140+
136141
for proto in disc_protos:
137142
if self._get_module(proto).discover(self._config):
138143
self.logger.info(f'discovery of {protocol} was successful')
@@ -141,6 +146,7 @@ def discover(self, protocol=None) -> bool:
141146
self.proto_detected = True
142147
return True
143148
else:
149+
self.discovery_logs[proto] = self._config.pop('discover_log', '')
144150
self.logger.info(f'discovery of {protocol} was unsuccessful')
145151

146152
return False
@@ -166,7 +172,7 @@ def query(self, assign_values: bool = True, protocol=None) -> dict:
166172
try:
167173
result = ref.query(self._config)
168174
if not result:
169-
self.logger.warning('no results from smartmeter query received')
175+
self.logger.info('no results from smartmeter query received')
170176
else:
171177
self.logger.debug(f'got result: {result}')
172178
if assign_values:
@@ -191,6 +197,7 @@ def create_items(self, data: dict = {}, file: str = '') -> bool:
191197
if not data:
192198
return False
193199

200+
# try to get smartmeter serial, fallback to time string
194201
try:
195202
id = data['1-0:96.1.0*255'][0]['value']
196203
except (KeyError, IndexError, AttributeError):
@@ -239,6 +246,7 @@ def create_items(self, data: dict = {}, file: str = '') -> bool:
239246

240247
try:
241248
yaml_save(file, {id: result})
249+
self.item_file = file
242250
except Exception as e:
243251
self.logger.warning(f'saving item file {file} failed with error: {e}')
244252
return False

smartmeter/dlms.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import logging
3232
import time
3333
import serial
34+
import io
35+
3436
try:
3537
import serial_asyncio
3638
ASYNC_IMPORTED = True
@@ -240,6 +242,52 @@ def format_time(timedelta: float) -> str:
240242
return f"{timedelta * 10 ** 9:.2f} ns"
241243

242244

245+
#
246+
# string logger
247+
#
248+
249+
250+
class StringLogger():
251+
252+
def __init__(self):
253+
### Create the logger
254+
self.logger = logging.getLogger('sml_string_logger')
255+
self.logger.setLevel(logging.DEBUG)
256+
257+
### Setup the console handler with a StringIO object
258+
self.log_capture_string = io.StringIO()
259+
self.ch = logging.StreamHandler(self.log_capture_string)
260+
self.ch.setLevel(logging.DEBUG)
261+
262+
### Optionally add a formatter
263+
formatter = logging.Formatter('%(levelname)s: %(message)s')
264+
self.ch.setFormatter(formatter)
265+
266+
### Add the console handler to the logger
267+
self.logger.addHandler(self.ch)
268+
269+
def __call__(self):
270+
return self.log_capture_string.getvalue()
271+
272+
def close(self):
273+
self.log_capture_string.close()
274+
275+
def debug(self, *args, **kwargs):
276+
self.logger.debug(*args, **kwargs)
277+
278+
def info(self, *args, **kwargs):
279+
self.logger.info(*args, **kwargs)
280+
281+
def warning(self, *args, **kwargs):
282+
self.logger.warning(*args, **kwargs)
283+
284+
def error(self, *args, **kwargs):
285+
self.logger.error(*args, **kwargs)
286+
287+
def critical(self, *args, **kwargs):
288+
self.logger.critical(*args, **kwargs)
289+
290+
243291
# TODO: asyncio for DLMS disabled until real testing has succeeded
244292
# #
245293
# # asyncio reader
@@ -519,7 +567,7 @@ def get_sock(self):
519567
host = self.config.get('host')
520568
port = self.config.get('port')
521569
timeout = self.config.get('timeout', 2)
522-
baudrate = self.config.get('DLMS', {'baudate_min': 300}).get('baudrate_min', 300)
570+
baudrate = self.config.get('DLMS', {}).get('baudrate_min', 300)
523571

524572
if TESTING:
525573
self.target = '(test input)'
@@ -539,7 +587,7 @@ def get_sock(self):
539587
timeout=timeout
540588
)
541589
if not serial_port == self.sock.name:
542-
logger.debug(f"Asked for {serial_port} as serial port, but really using now {sock.name}")
590+
logger.debug(f"Asked for {serial_port} as serial port, but really using now {self.sock.name}")
543591
self.target = f'serial://{self.sock.name}'
544592

545593
except FileNotFoundError:
@@ -969,7 +1017,7 @@ def parse(self, data: str) -> dict:
9691017
return result
9701018

9711019

972-
def query(config, discover: bool = False) -> Union[dict, None]:
1020+
def query(config, discover: bool = False, logger=logger) -> Union[dict, None]:
9731021
"""
9741022
This function will
9751023
1. open a serial communication line to the smartmeter
@@ -1022,14 +1070,16 @@ def discover(config: dict) -> bool:
10221070
# reduced baud rates or changed parameters, but there would need to be
10231071
# the need for this.
10241072
# For now, let's see how well this works...
1025-
result = query(config, discover=True)
1073+
str_log = StringLogger()
1074+
result = query(config, discover=True, logger=str_log)
10261075

10271076
# result should have one key 'readout' with the full answer and a separate
10281077
# key for every read OBIS code. If no OBIS codes are read/converted, we can
10291078
# not be sure this is really DLMS, so we check for at least one OBIS code.
10301079
if result:
10311080
return len(result) > 1
10321081
else:
1082+
config['discover_log'] = str_log()
10331083
return False
10341084

10351085

@@ -1107,7 +1157,7 @@ def discover(config: dict) -> bool:
11071157
logger.info("This is Smartmeter Plugin, DLMS module, running in standalone mode")
11081158
logger.info("==================================================================")
11091159

1110-
result = query(config)
1160+
result = discover(config)
11111161

11121162
if not result:
11131163
logger.info(f"No results from query, maybe a problem with the serial port '{config['serial_port']}' given.")

smartmeter/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pyserial>=3.2.1
2-
SmlLib>=1.3
2+
smllib>=1.3
3+
pyserial-asyncio

smartmeter/sml.py

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import errno
3232
import logging
3333
import serial
34+
import io
35+
3436
try:
3537
import serial_asyncio
3638
ASYNC_IMPORTED = True
@@ -90,6 +92,7 @@ class SmartPluginWebIf():
9092
if __name__ == '__main__':
9193
logger = logging.getLogger(__name__)
9294
logger.debug(f"init standalone {__name__}")
95+
9396
else:
9497
logger = logging.getLogger(__name__)
9598
logger.debug(f"init plugin component {__name__}")
@@ -152,6 +155,53 @@ def format_time(timedelta):
152155
elif timedelta > 0.000000001:
153156
return f"{timedelta * 1000000000.0:.2f} ns"
154157

158+
159+
#
160+
# string logger
161+
#
162+
163+
164+
class StringLogger():
165+
166+
def __init__(self):
167+
### Create the logger
168+
self.logger = logging.getLogger('sml_string_logger')
169+
self.logger.setLevel(logging.DEBUG)
170+
171+
### Setup the console handler with a StringIO object
172+
self.log_capture_string = io.StringIO()
173+
self.ch = logging.StreamHandler(self.log_capture_string)
174+
self.ch.setLevel(logging.DEBUG)
175+
176+
### Optionally add a formatter
177+
formatter = logging.Formatter('%(levelname)s: %(message)s')
178+
self.ch.setFormatter(formatter)
179+
180+
### Add the console handler to the logger
181+
self.logger.addHandler(self.ch)
182+
183+
def __call__(self):
184+
return self.log_capture_string.getvalue()
185+
186+
def close(self):
187+
self.log_capture_string.close()
188+
189+
def debug(self, *args, **kwargs):
190+
self.logger.debug(*args, **kwargs)
191+
192+
def info(self, *args, **kwargs):
193+
self.logger.info(*args, **kwargs)
194+
195+
def warning(self, *args, **kwargs):
196+
self.logger.warning(*args, **kwargs)
197+
198+
def error(self, *args, **kwargs):
199+
self.logger.error(*args, **kwargs)
200+
201+
def critical(self, *args, **kwargs):
202+
self.logger.critical(*args, **kwargs)
203+
204+
155205
#
156206
# asyncio reader
157207
#
@@ -307,7 +357,7 @@ def __init__(self, logger, config: dict):
307357
self.target = '(not set)'
308358
self.buffersize = config.get('sml', {'buffersize': 1024}).get('buffersize', 1024)
309359

310-
logger.debug(f"config='{config}'")
360+
self.logger.debug(f"config='{config}'")
311361

312362
def __call__(self) -> bytes:
313363

@@ -316,7 +366,7 @@ def __call__(self) -> bytes:
316366
#
317367
locked = self.lock.acquire(blocking=False)
318368
if not locked:
319-
logger.error('could not get lock for serial/network access. Is another scheduled/manual action still active?')
369+
self.logger.error('could not get lock for serial/network access. Is another scheduled/manual action still active?')
320370
return b''
321371

322372
try: # lock release
@@ -325,7 +375,7 @@ def __call__(self) -> bytes:
325375
if not self.sock:
326376
# error already logged, just go
327377
return b''
328-
logger.debug(f"time to open {self.target}: {format_time(time.time() - runtime)}")
378+
self.logger.debug(f"time to open {self.target}: {format_time(time.time() - runtime)}")
329379

330380
#
331381
# read data from device
@@ -334,13 +384,13 @@ def __call__(self) -> bytes:
334384
try:
335385
response = self.read()
336386
if len(response) == 0:
337-
logger.error('reading data from device returned 0 bytes!')
387+
self.logger.info('reading data from device returned 0 bytes!')
338388
return b''
339389
else:
340-
logger.debug(f'read {len(response)} bytes')
390+
self.logger.debug(f'read {len(response)} bytes')
341391

342392
except Exception as e:
343-
logger.error(f'reading data from {self.target} failed with error: {e}')
393+
self.logger.error(f'reading data from {self.target} failed with error: {e}')
344394

345395
except Exception:
346396
# passthrough, this is only for releasing the lock
@@ -428,41 +478,41 @@ def get_sock(self):
428478
timeout=self.timeout
429479
)
430480
if not self.serial_port == self.sock.name:
431-
logger.debug(f"Asked for {self.serial_port} as serial port, but really using now {self.sock.name}")
481+
self.logger.debug(f"Asked for {self.serial_port} as serial port, but really using now {self.sock.name}")
432482
self.target = f'serial://{self.sock.name}'
433483

434484
except FileNotFoundError:
435-
logger.error(f"Serial port '{self.serial_port}' does not exist, please check your port")
485+
self.logger.error(f"Serial port '{self.serial_port}' does not exist, please check your port")
436486
return None, ''
437487
except serial.SerialException:
438488
if self.sock is None:
439489
if count < 3:
440490
# count += 1
441-
logger.error(f"Serial port '{self.serial_port}' could not be opened, retrying {count}/3...")
491+
self.logger.error(f"Serial port '{self.serial_port}' could not be opened, retrying {count}/3...")
442492
time.sleep(3)
443493
continue
444494
else:
445-
logger.error(f"Serial port '{self.serial_port}' could not be opened")
495+
self.logger.error(f"Serial port '{self.serial_port}' could not be opened")
446496
else:
447-
logger.error(f"Serial port '{self.serial_port}' could be opened but somehow not accessed")
497+
self.logger.error(f"Serial port '{self.serial_port}' could be opened but somehow not accessed")
448498
return None, ''
449499
except OSError:
450-
logger.error(f"Serial port '{self.serial_port}' does not exist, please check the spelling")
500+
self.logger.error(f"Serial port '{self.serial_port}' does not exist, please check the spelling")
451501
return None, ''
452502
except Exception as e:
453-
logger.error(f"unforeseen error occurred: '{e}'")
503+
self.logger.error(f"unforeseen error occurred: '{e}'")
454504
return None, ''
455505

456506
if self.sock is None:
457507
if count == 3:
458-
logger.error("retries unsuccessful, serial port could not be opened, giving up.")
508+
self.logger.error("retries unsuccessful, serial port could not be opened, giving up.")
459509
else:
460510
# this should not happen...
461-
logger.error("retries unsuccessful or unforeseen error occurred, serial object was not initialized.")
511+
self.logger.error("retries unsuccessful or unforeseen error occurred, serial object was not initialized.")
462512
return None, ''
463513

464514
if not self.sock.is_open:
465-
logger.error(f"serial port '{self.serial_port}' could not be opened with given parameters, maybe wrong baudrate?")
515+
self.logger.error(f"serial port '{self.serial_port}' could not be opened with given parameters, maybe wrong baudrate?")
466516
return None, ''
467517

468518
elif self.host:
@@ -476,7 +526,7 @@ def get_sock(self):
476526
self.target = f'tcp://{self.host}:{self.port}'
477527

478528
else:
479-
logger.error('neither serialport nor host/port was given, no action possible.')
529+
self.logger.error('neither serialport nor host/port was given, no action possible.')
480530
return None, ''
481531

482532

@@ -592,7 +642,7 @@ def parse(self, data: bytes) -> dict:
592642
return self.fp()
593643

594644

595-
def query(config) -> dict:
645+
def query(config, logger=logger) -> dict:
596646
"""
597647
This function will
598648
1. open a serial communication line to the smartmeter
@@ -653,7 +703,11 @@ def discover(config: dict) -> bool:
653703
# reduced baud rates or changed parameters, but there would need to be
654704
# the need for this.
655705
# For now, let's see how well this works...
656-
return bool(query(config))
706+
str_log = StringLogger()
707+
result = bool(query(config, str_log))
708+
if not result:
709+
config['discover_log'] = str_log()
710+
return result
657711

658712

659713
if __name__ == '__main__':
@@ -718,8 +772,8 @@ def discover(config: dict) -> bool:
718772
logger.info("This is Smartmeter Plugin, SML module, running in standalone mode")
719773
logger.info("==================================================================")
720774

721-
result = query(config)
722-
775+
result = discover(config)
776+
723777
if not result:
724778
logger.info(f"No results from query, maybe a problem with the serial port '{config['serial_port']}' given.")
725779
elif len(result) > 1:

0 commit comments

Comments
 (0)