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