mirror of https://github.com/lumapu/ahoy.git
10 changed files with 1492 additions and 442 deletions
@ -0,0 +1,151 @@ |
|||||
|
|
||||
|
package main; |
||||
|
|
||||
|
use strict; |
||||
|
use warnings; |
||||
|
use DevIo; # load DevIo.pm if not already loaded |
||||
|
|
||||
|
# called upon loading the module MY_MODULE |
||||
|
sub AHOYUL_Initialize($) |
||||
|
{ |
||||
|
my ($hash) = @_; |
||||
|
|
||||
|
$hash->{DefFn} = "AHOYUL_Define"; |
||||
|
$hash->{UndefFn} = "AHOYUL_Undef"; |
||||
|
$hash->{SetFn} = "AHOYUL_Set"; |
||||
|
$hash->{ReadFn} = "AHOYUL_Read"; |
||||
|
$hash->{ReadyFn} = "AHOYUL_Ready"; |
||||
|
|
||||
|
$hash->{ParseFn} = "AHOYUL_Parse"; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
# called when a new definition is created (by hand or from configuration read on FHEM startup) |
||||
|
sub AHOYUL_Define($$) |
||||
|
{ |
||||
|
my ($hash, $def) = @_; |
||||
|
my @a = split("[ \t]+", $def); |
||||
|
|
||||
|
my $name = $a[0]; |
||||
|
|
||||
|
# $a[1] is always equals the module name "MY_MODULE" |
||||
|
|
||||
|
# first argument is a serial device (e.g. "/dev/ttyUSB0@57600,8,N,1") |
||||
|
my $dev = $a[2]; |
||||
|
|
||||
|
return "no device given" unless($dev); |
||||
|
|
||||
|
# close connection if maybe open (on definition modify) |
||||
|
DevIo_CloseDev($hash) if(DevIo_IsOpen($hash)); |
||||
|
|
||||
|
# add a default baud rate (9600), if not given by user |
||||
|
$dev .= '@57600,8,N,1' if(not $dev =~ m/\@\d+$/); |
||||
|
|
||||
|
# set the device to open |
||||
|
$hash->{DeviceName} = $dev; |
||||
|
|
||||
|
# open connection with custom init function |
||||
|
my $ret = DevIo_OpenDev($hash, 0, "AHOYUL_Init"); |
||||
|
|
||||
|
return undef; |
||||
|
} |
||||
|
|
||||
|
# called when definition is undefined |
||||
|
# (config reload, shutdown or delete of definition) |
||||
|
sub AHOYUL_Undef($$) |
||||
|
{ |
||||
|
my ($hash, $name) = @_; |
||||
|
|
||||
|
# close the connection |
||||
|
DevIo_CloseDev($hash); |
||||
|
|
||||
|
return undef; |
||||
|
} |
||||
|
|
||||
|
# called repeatedly if device disappeared |
||||
|
sub AHOYUL_Ready($) |
||||
|
{ |
||||
|
my ($hash) = @_; |
||||
|
|
||||
|
# try to reopen the connection in case the connection is lost |
||||
|
return DevIo_OpenDev($hash, 1, "AHOYUL_Init"); |
||||
|
} |
||||
|
|
||||
|
# called when data was received |
||||
|
sub AHOYUL_Read($) |
||||
|
{ |
||||
|
my ($hash) = @_; |
||||
|
my $name = $hash->{NAME}; |
||||
|
|
||||
|
# read the available data |
||||
|
my $buf = DevIo_SimpleRead($hash); |
||||
|
|
||||
|
# stop processing if no data is available (device disconnected) |
||||
|
return if(!defined($buf)); |
||||
|
|
||||
|
Log3 $name, 5, "AHOYUL ($name) - received: $buf"; |
||||
|
|
||||
|
# |
||||
|
# do something with $buf, e.g. generate readings, send answers via DevIo_SimpleWrite(), ... |
||||
|
# |
||||
|
|
||||
|
} |
||||
|
|
||||
|
# called if set command is executed |
||||
|
sub AHOYUL_Set($$@) |
||||
|
{ |
||||
|
my ($hash, $name, $params) = @_; |
||||
|
my @a = split("[ \t]+", $params); |
||||
|
$cmd = $params[0] |
||||
|
|
||||
|
my $usage = "unknown argument $cmd, choose one of statusRequest:noArg on:noArg off:noArg"; |
||||
|
|
||||
|
# get command overview from ahoy-nano device |
||||
|
if($cmd eq "?") |
||||
|
{ |
||||
|
#todo |
||||
|
DevIo_SimpleWrite($hash, "?\r\n", 2); |
||||
|
} |
||||
|
elsif($cmd eq "a") |
||||
|
{ |
||||
|
#todo handle automode and send command to ahoy-nano via cmd a[[:<period_sec>]:<12 digit inverter id>:] |
||||
|
DevIo_SimpleWrite($hash, "a:{$params[1]}:{$params[2]}:\r\n", 2); |
||||
|
} |
||||
|
elsif($cmd eq "c") |
||||
|
{ |
||||
|
#todo |
||||
|
#DevIo_SimpleWrite($hash, "off\r\n", 2); |
||||
|
} |
||||
|
elsif($cmd eq "d") |
||||
|
{ |
||||
|
#todo |
||||
|
#DevIo_SimpleWrite($hash, "off\r\n", 2); |
||||
|
} |
||||
|
elsif($cmd eq "i") |
||||
|
{ |
||||
|
#todo |
||||
|
#DevIo_SimpleWrite($hash, "off\r\n", 2); |
||||
|
} |
||||
|
elsif($cmd eq "s") |
||||
|
{ |
||||
|
#todo |
||||
|
#DevIo_SimpleWrite($hash, "off\r\n", 2); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return $usage; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
# will be executed upon successful connection establishment (see DevIo_OpenDev()) |
||||
|
sub AHOYUL_Init($) |
||||
|
{ |
||||
|
my ($hash) = @_; |
||||
|
|
||||
|
# send init to device, here e.g. enable automode to send DevInfoReq (0x15 ... 0x0B ....) every 120sec and enable simple decoding in ahoy-nano |
||||
|
DevIo_SimpleWrite($hash, "a120:::::d1:\r\n", 2); |
||||
|
|
||||
|
return undef; |
||||
|
} |
||||
|
|
||||
|
1; |
@ -0,0 +1,168 @@ |
|||||
|
|
||||
|
#handles at-commands send and response evaluation, a thread can catch the URCs of the module |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
import time |
||||
|
import datetime |
||||
|
import threading |
||||
|
import binascii |
||||
|
import serial.tools.list_ports |
||||
|
import _libs2.file_handling as fh |
||||
|
|
||||
|
|
||||
|
#some global variables |
||||
|
__version_info__ = ('2022', '11', '13') |
||||
|
__version_string__ = '%40s' % ('hm inverter handling version: ' + '-'.join(__version_info__)) |
||||
|
lck2 = threading.Lock() # used for AT-commands to access serial port |
||||
|
com_stop = True |
||||
|
promt = '/> ' |
||||
|
respo = '<< ' |
||||
|
sendi = '>> ' |
||||
|
|
||||
|
|
||||
|
def get_version(): |
||||
|
return __hm_handling_version__ |
||||
|
|
||||
|
|
||||
|
# async reading thread of comport data (e.g. for URCs) |
||||
|
class AsyncComRead(threading.Thread): |
||||
|
def __init__(self, _com, _addTimestamp, _debug): |
||||
|
# calling superclass init |
||||
|
threading.Thread.__init__(self) |
||||
|
self.com = _com |
||||
|
self.addTS = _addTimestamp |
||||
|
self.debug = _debug |
||||
|
self.data = '' |
||||
|
|
||||
|
def run(self): |
||||
|
global com_stop |
||||
|
if self.debug: print('\n +++ Thread: com reader started', end='', flush=True) |
||||
|
urc_millis = millis_since(0) |
||||
|
urc_ln = True |
||||
|
while not com_stop: |
||||
|
with lck2: |
||||
|
urc = [] |
||||
|
while self.com.in_waiting: |
||||
|
line = self.com.readline().decode('ascii','ignore') |
||||
|
line = line.replace('\n','').replace('\r','') |
||||
|
if len(line) > 2: # filter out empty lines |
||||
|
if self.addTS: |
||||
|
urc.append('\n%s%s%s' % (timestamp(0), respo, line)) |
||||
|
else: |
||||
|
urc.append('\n%s%s' % (respo, line)) |
||||
|
|
||||
|
#todo: return values into callback function |
||||
|
|
||||
|
if not self.com.is_open or com_stop: break |
||||
|
if urc: |
||||
|
fh.my_print('\n%s' % ( (' '.join(urc)).replace('\n ', '\n') )) |
||||
|
urc_millis = millis_since(0) |
||||
|
urc_ln = False |
||||
|
|
||||
|
if (not urc_ln) and (millis_since(urc_millis) >= 3000): |
||||
|
urc_ln = True |
||||
|
print('\n%s' % (promt,), end = '') |
||||
|
|
||||
|
time.sleep(0.1) |
||||
|
if self.debug: print('\n +++ Thread: com reader stopped', end='', flush=True) |
||||
|
|
||||
|
|
||||
|
#def parse_payload(_self, _data): |
||||
|
|
||||
|
|
||||
|
# opens the com-port and starts the Async com reading thread |
||||
|
def openCom_startComReadThread(_tp, _com, _debug): |
||||
|
global com_stop |
||||
|
#start urc com logging thread |
||||
|
if not _com.is_open: _com.open() |
||||
|
_tp = AsyncComRead(_com, True, _debug) |
||||
|
_tp.setDaemon = True |
||||
|
com_stop = False |
||||
|
_tp.start() |
||||
|
return _tp |
||||
|
|
||||
|
|
||||
|
# stops the com-reading thread and closes com-port |
||||
|
def stopComReadThread_closeCom(_tp, _com): |
||||
|
global com_stop |
||||
|
com_stop = True |
||||
|
_tp.join(1.1) |
||||
|
if _com.is_open: _com.close() |
||||
|
return com_stop |
||||
|
|
||||
|
|
||||
|
# opens com-port if the given port-string exists and starts the Async com-port reading thread |
||||
|
def check_port_and_openCom(_tp, _com, _portstr, MATCH_sec, TIMEOUT_sec, _DEBUG): |
||||
|
if (check_wait_port(_portstr, MATCH_sec, TIMEOUT_sec, _DEBUG) == 0): |
||||
|
fh.my_print('\n +++ Port enumerated and detected: ' + _portstr.lower()) |
||||
|
# reopen port |
||||
|
_tp = openCom_startComReadThread(_tp, _com, _DEBUG) |
||||
|
else: |
||||
|
fh.my_print('\n +++ No USB com-port detected again --> end') |
||||
|
_tp = None |
||||
|
|
||||
|
return _tp |
||||
|
|
||||
|
|
||||
|
|
||||
|
# simply list the available com-ports of the system |
||||
|
def list_ports(): |
||||
|
portlist = serial.tools.list_ports.comports() |
||||
|
fh.my_print('\n +++ detected ports:') |
||||
|
for element in portlist: |
||||
|
fh.my_print(' %s' % (str(element.device))) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## simple Terminal to enter and display cmds, resonses are read by urc thread |
||||
|
def simple_terminal(_com, _debug): |
||||
|
next = True |
||||
|
while(next): |
||||
|
time.sleep(1.0) |
||||
|
_cmd_in = input('\n%s'%(promt,)) |
||||
|
fh.my_print('\n%s%s' % (promt, _cmd_in, )) |
||||
|
|
||||
|
if "start" in _cmd_in: |
||||
|
#end terminal and return true |
||||
|
return True |
||||
|
elif "kill" in _cmd_in: |
||||
|
#ends terminal and returns false |
||||
|
return False |
||||
|
elif '_time' in _cmd_in or '_tnow' in _cmd_in: |
||||
|
_uartstr = 't{:d}'.format(int(time.time())) |
||||
|
_com.write( _uartstr.encode('ascii') ) |
||||
|
fh.my_print('\n%s%s%s' % (timestamp(0), sendi, _uartstr, )) |
||||
|
continue |
||||
|
|
||||
|
_uartstr = _cmd_in.encode('ascii') |
||||
|
_com.write( ('%s%s' % (sendi, _uartstr, )).encode('ascii') ) |
||||
|
fh.my_print('\n%s%s%s' % (timestamp(0), sendi, _uartstr, )) |
||||
|
|
||||
|
return True |
||||
|
|
||||
|
|
||||
|
|
||||
|
# calculates the time in milli-seconds from a given start time |
||||
|
def millis_since(thisstart): |
||||
|
return int(round(time.time() * 1000) - thisstart) |
||||
|
|
||||
|
|
||||
|
# different timestamp formats |
||||
|
def timestamp(format): |
||||
|
if(format==0): |
||||
|
return ('%s: ' % (datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S.%f")[:-3])) |
||||
|
elif format==1: |
||||
|
return str(time.time()) + ": " |
||||
|
elif format==2: |
||||
|
#for file name extention |
||||
|
return datetime.datetime.now().strftime("%Y-%m-%d_%H%M_") |
||||
|
elif format==3: |
||||
|
#for file name extention with seconds |
||||
|
return datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S_") |
||||
|
elif format==4: |
||||
|
return datetime.datetime.now().strftime("%H%M%S.%f") |
||||
|
else: |
||||
|
return datetime.datetime.now().strftime("%y/%m/%d,%H:%M:%S+01") |
||||
|
|
@ -0,0 +1,129 @@ |
|||||
|
|
||||
|
# has a thread for periodic log file writing and some generic file handling |
||||
|
# my_print function write into global variable to be stored in log file |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
import time |
||||
|
import datetime |
||||
|
import threading |
||||
|
import binascii |
||||
|
|
||||
|
|
||||
|
#some global variables |
||||
|
__version_info__ = ('2022', '11', '13') |
||||
|
__version_string__ = 'file handling version: ' + '-'.join(__version_info__) |
||||
|
lck = threading.Lock() # used for log_sum writing |
||||
|
log_sum = 'logstart... ' |
||||
|
log_stop = True |
||||
|
|
||||
|
|
||||
|
def get_version(): |
||||
|
return __version_string__ |
||||
|
|
||||
|
def set_log_stop(_state): |
||||
|
global log_stop |
||||
|
log_stop = _state |
||||
|
|
||||
|
def get_log_stop(): |
||||
|
return log_stop |
||||
|
|
||||
|
|
||||
|
def my_print(out): |
||||
|
global lck |
||||
|
global log_sum |
||||
|
print(out, end='', flush=True) |
||||
|
with lck: |
||||
|
log_sum += out |
||||
|
|
||||
|
|
||||
|
# async writing thread of log file data |
||||
|
class AsyncWrite(threading.Thread): |
||||
|
def __init__(self, _logfile, _debug): |
||||
|
# calling superclass init |
||||
|
threading.Thread.__init__(self) |
||||
|
self.file = _logfile |
||||
|
self.debug = _debug |
||||
|
|
||||
|
def run(self): |
||||
|
global lck |
||||
|
global log_sum |
||||
|
global log_stop |
||||
|
|
||||
|
log_stop = False |
||||
|
if self.debug: print('\n +++ Thread: file writer started', end='', flush=True) |
||||
|
while not log_stop: |
||||
|
with lck: |
||||
|
if len(log_sum) > 2000: |
||||
|
if self.debug: print('\n +++ writing to logfile, ' + str(len(log_sum)) + ' bytes', end='', flush=True) |
||||
|
self.file.write(log_sum.replace('\r','').replace('\n\n','\n')) |
||||
|
log_sum = '' |
||||
|
|
||||
|
time.sleep(2.0) |
||||
|
|
||||
|
#write remaining data to log |
||||
|
with lck: |
||||
|
if self.debug: print('\n +++ writing to logfile remaining, ' + str(len(log_sum)) + ' bytes', end='', flush=True) |
||||
|
self.file.write(log_sum.replace('\r','').replace('\n\n','\n')) |
||||
|
log_sum = '' |
||||
|
self.file.flush() #needed to finally write all data into file after stop |
||||
|
self.file.close() |
||||
|
|
||||
|
if self.debug: print('\n +++ Thread: file writer stopped and file closed', end='', flush=True) |
||||
|
|
||||
|
|
||||
|
def start_logging_thread_open_file(_dirname, _filename, _mode, _DEBUG): |
||||
|
global log_stop |
||||
|
|
||||
|
#open file |
||||
|
_logfile = open_file_makedir(_dirname, _filename, _mode, _DEBUG) |
||||
|
if _DEBUG: my_print('\n +++ generate log file: ' + str(_dirname + '/' + _filename)) |
||||
|
|
||||
|
#start logging thread |
||||
|
_tf = AsyncWrite(_logfile, _DEBUG) |
||||
|
_tf.setDaemon = True |
||||
|
log_stop = False |
||||
|
_tf.start() |
||||
|
return _tf |
||||
|
|
||||
|
def stop_logging_thread_close_file(_tf, _DEBUG): |
||||
|
global log_stop |
||||
|
log_stop = True |
||||
|
if _DEBUG: my_print('\n +++ log_stop is: ' + str(log_stop)) |
||||
|
if _tf: _tf.join(2.1) |
||||
|
|
||||
|
|
||||
|
|
||||
|
## extracts the pathname and filename from fiven input string and returns two strings |
||||
|
def get_dir_file_name(_pathfile, _DEBUG): |
||||
|
_dir_name = './' |
||||
|
_file_name = '' |
||||
|
|
||||
|
if len(_pathfile) > 0: |
||||
|
#if '/' not in _pathfile or '\\' not in _pathfile: |
||||
|
#_pathfile = './' + _pathfile |
||||
|
_dir_name, _file_name = os.path.split(_pathfile) |
||||
|
if len(_dir_name) <= 0: |
||||
|
_dir_name = './' |
||||
|
else: |
||||
|
my_print("\n +++ no file name, exit ") |
||||
|
sys.exit() |
||||
|
|
||||
|
if _DEBUG: |
||||
|
my_print("\n +++ get_dir_file_name: " + str(_dir_name + ' / ' + _file_name)) |
||||
|
|
||||
|
return _dir_name,_file_name |
||||
|
|
||||
|
|
||||
|
## handling of file opening for read and write in a given directory, directory will be created if not existing |
||||
|
## it returns the file connection pointer |
||||
|
def open_file_makedir(_dir_name, _file_name, _mode, _DEBUG): |
||||
|
if _DEBUG: |
||||
|
my_print("\n +++ open_file_makedir: " + str(_dir_name + ' / ' + _file_name)) |
||||
|
if len(_dir_name) > 0: |
||||
|
os.makedirs(_dir_name, exist_ok=True) |
||||
|
if _DEBUG: |
||||
|
my_print("\n +++ open file: " + str(_dir_name + ' / ' + _file_name) + " mode: " + _mode) |
||||
|
_file = open(_dir_name + '/' + _file_name, _mode) |
||||
|
return _file |
||||
|
|
@ -0,0 +1,118 @@ |
|||||
|
#works for python 3.7.3 |
||||
|
|
||||
|
import serial |
||||
|
import os |
||||
|
import argparse |
||||
|
import time |
||||
|
import datetime |
||||
|
import binascii |
||||
|
import threading |
||||
|
import sys |
||||
|
from binascii import hexlify |
||||
|
# my own libs |
||||
|
import _libs2.com_handling as cph |
||||
|
import _libs2.file_handling as fh |
||||
|
|
||||
|
|
||||
|
# 2022-02-11: init mb |
||||
|
|
||||
|
|
||||
|
#some global variables |
||||
|
__version_info__ = ('2022', '11', '13') |
||||
|
__version__ = 'app version: ' + '-'.join(__version_info__) |
||||
|
DEBUG = False |
||||
|
|
||||
|
|
||||
|
|
||||
|
############################################################################################################################ |
||||
|
# here the program starts |
||||
|
def main(): |
||||
|
global DEBUG |
||||
|
global com_stop |
||||
|
ret = 0 |
||||
|
err = 0 |
||||
|
|
||||
|
starttime = cph.millis_since(0) |
||||
|
parser = argparse.ArgumentParser(description='simple Hoymiles Terminal') |
||||
|
parser.add_argument('-p', type=str, default='/dev/ttyUSB0', help='Serial port') |
||||
|
parser.add_argument('-b', type=int, default=57600, help='baudrate of the rf-module') |
||||
|
parser.add_argument('-d', default=False, help='use parameter to print additional debug info', action='store_true') |
||||
|
parser.add_argument('-v', '--version', action='version', version="%(prog)s (" + __version__ + ")") |
||||
|
parser.add_argument('-l', type=str, default='', help='log-file path/name') |
||||
|
parser.add_argument('-i', default=True, help='have a simple AT-command line', action='store_true') |
||||
|
|
||||
|
args = parser.parse_args() |
||||
|
DEBUG = args.d |
||||
|
#used later for Threads |
||||
|
tp = None |
||||
|
tf = None |
||||
|
com = None |
||||
|
logfile = None |
||||
|
|
||||
|
|
||||
|
#shows the used lib versions |
||||
|
fh.my_print('\n +++ starting AhoyUL Terminal ...') |
||||
|
fh.my_print('\n +++ %40s' % (__version__)) |
||||
|
fh.my_print('\n +++ %40s' % (cph.__version_string__)) |
||||
|
fh.my_print('\n +++ %40s' % (fh.__version_string__)) |
||||
|
|
||||
|
if DEBUG: fh.my_print('\n' + str(args)) |
||||
|
|
||||
|
#output file logging |
||||
|
com_port_name = (args.p).replace('/dev/','_').replace('/','_') #for Linux, no matter on Windows |
||||
|
if len(args.l) > 0: |
||||
|
tmp1, tmp2 = fh.get_dir_file_name(args.l, DEBUG) |
||||
|
logfile_name = cph.timestamp(3) + tmp2 |
||||
|
tf = fh.start_logging_thread_open_file(tmp1, logfile_name, 'w+', DEBUG) |
||||
|
else: |
||||
|
# default path of logfile, always logging |
||||
|
tmp1, tmp2 = fh.get_dir_file_name('log/default_{0:s}.log'.format(com_port_name), DEBUG) |
||||
|
logfile_name = cph.timestamp(3) + tmp2 |
||||
|
tf = fh.start_logging_thread_open_file(tmp1, logfile_name, 'w+', DEBUG) |
||||
|
|
||||
|
com = serial.Serial(args.p, args.b, timeout=0.2, rtscts=False, dsrdtr=False) |
||||
|
com.rts = True |
||||
|
com.dtr = True |
||||
|
fh.my_print("\n +++ serial port is open: %s, baud: %d, rtscts: %s, rts: %s" % (com.portstr,com.baudrate,com.rtscts,com.rts)) |
||||
|
|
||||
|
|
||||
|
#start urc com logging thread |
||||
|
tp = cph.openCom_startComReadThread(tp, com, DEBUG) |
||||
|
|
||||
|
try: |
||||
|
# do init AT here |
||||
|
|
||||
|
|
||||
|
#start small AT-command terminal |
||||
|
if args.i == True: |
||||
|
result = cph.simple_terminal(com, DEBUG) |
||||
|
if result == False: |
||||
|
starttime_sec = cph.millis_since(0) / 1000 |
||||
|
return -1 |
||||
|
|
||||
|
|
||||
|
# todo: do some automated scripting and data sending to the AhoyUL board |
||||
|
|
||||
|
|
||||
|
#wait remeining data |
||||
|
TO_sec = 5 |
||||
|
fh.my_print('\n +++ wait remaining data for %d sec' % (TO_sec,)) |
||||
|
time.sleep(TO_sec) |
||||
|
|
||||
|
except KeyboardInterrupt: |
||||
|
fh.my_print('\n +++ keyboard interrupt, end ...') |
||||
|
|
||||
|
finally: |
||||
|
fh.my_print('\n +++ end, wait until finshed\n') |
||||
|
|
||||
|
#cleanup |
||||
|
#stop comport reader thread |
||||
|
if tp: cph.stopComReadThread_closeCom(tp, com) |
||||
|
|
||||
|
# if logging to file was enabled |
||||
|
#if len(args.l) > 0: |
||||
|
if tf: fh.stop_logging_thread_close_file(tf, DEBUG) |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
Loading…
Reference in new issue