From 4fb3b5a66a94f567c24267ff7f1d6f8cf2d32bbf Mon Sep 17 00:00:00 2001 From: bentele <> Date: Sun, 8 Apr 2012 13:27:48 +0000 Subject: [PATCH] new modul 70_SML.pm git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@1423 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- contrib/contrib/00_TAHR.pm | 153 ++ contrib/contrib/1-Wire/00_OWX.pm | 1590 +++++++++++++++++ contrib/contrib/1-Wire/21_OWAD.pm | 1033 +++++++++++ contrib/contrib/1-Wire/21_OWCOUNT.pm | 706 ++++++++ contrib/contrib/1-Wire/21_OWID.pm | 212 +++ contrib/contrib/1-Wire/21_OWLCD.pm | 970 ++++++++++ contrib/contrib/1-Wire/21_OWTEMP.pm | 821 +++++++++ .../Schaltplan_Aktives_1-Wire_Interface.png | Bin 0 -> 19686 bytes .../Schaltplan_Passives_1-Wire_Interface.png | Bin 0 -> 29208 bytes .../Schaltplan_USB_1-Wire_Interface.png | Bin 0 -> 41324 bytes contrib/contrib/11_FHT8V.pm | 247 +++ contrib/contrib/21_OWTEMP.pm.fork | 531 ++++++ contrib/contrib/64_ESA.pm | 160 ++ contrib/contrib/70_NT5000.pm | 1098 ++++++++++++ contrib/contrib/70_SolarView.pm | 354 ++++ contrib/contrib/86_FS10.pm | 166 ++ contrib/contrib/90_EIBUPDOWN.pm | 211 +++ contrib/contrib/91_DbLog.pm | 97 + contrib/contrib/95_FLOORPLAN/95_FLOORPLAN.pm | 611 +++++++ .../95_FLOORPLAN/99_myFloorplanList.pm | 75 + .../95_FLOORPLAN/commandref - floorplan.html | 131 ++ .../95_FLOORPLAN/darkfloorplanstyle.css | 48 + .../fhem-floorplan-installation-guide.pdf | Bin 0 -> 824264 bytes .../fhem-floorplan-installation-guide_de.pdf | Bin 0 -> 911729 bytes .../contrib/95_FLOORPLAN/floorplanstyle.css | 48 + contrib/contrib/95_VIEW.pm | 299 ++++ contrib/contrib/97_GROUP.pm | 365 ++++ contrib/contrib/98_FHTCONF.pm | 519 ++++++ contrib/contrib/99_ALARM.pm | 149 ++ contrib/contrib/99_PID.pm | 112 ++ contrib/contrib/99_PRIV.pm | 50 + contrib/contrib/99_SUNRISE.pm | 89 + contrib/contrib/99_Taupunkt.pm | 113 ++ contrib/contrib/99_dumpdef.pm | 95 + contrib/contrib/99_priv_cgi.pm | 485 +++++ contrib/contrib/99_twitter.pm | 222 +++ contrib/contrib/DEBIAN/conffiles | 1 + contrib/contrib/DEBIAN/control | 12 + contrib/contrib/DEBIAN/postinst | 24 + contrib/contrib/DEBIAN/postrm | 20 + contrib/contrib/DEBIAN/prerm | 11 + contrib/contrib/FB7270/fhemcmd.sh | 14 + contrib/contrib/FB7270/makeimage | 44 + contrib/contrib/FB7270/startfhem | 26 + contrib/contrib/FB7390/fhemcmd.sh | 14 + contrib/contrib/FB7390/install | 63 + contrib/contrib/FB7390/makeimage | 55 + contrib/contrib/FB7390/startfhem | 37 + contrib/contrib/HM-USB-CFG/00_HMLAN.pm | 405 +++++ contrib/contrib/HM-USB-CFG/DevIo.pm | 443 +++++ contrib/contrib/HM-USB-CFG/README.txt | 90 + contrib/contrib/HMRPC/00_HMRPC.pm | 280 +++ contrib/contrib/HMRPC/01_HMDEV.pm | 155 ++ contrib/contrib/HMRPC/HMRPC.txt | 150 ++ contrib/contrib/HMRPC/import_from_webui.bsh | 100 ++ contrib/contrib/JeeStuff/00_JeeLink.pm | 465 +++++ contrib/contrib/JeeStuff/18_JME.pm | 261 +++ contrib/contrib/JeeStuff/18_JSN.pm | 238 +++ contrib/contrib/JeeStuff/FHEM_JSN_BMP85.pde | 179 ++ contrib/contrib/JeeStuff/FHEM_JSN_LUX.pde | 167 ++ .../contrib/JeeStuff/FHEM_JSN_RoomNode.pde | 334 ++++ contrib/contrib/README | 67 + contrib/contrib/README.FS10 | 37 + contrib/contrib/RSSImonitor.pl | 90 + contrib/contrib/WBS/18_WBS.pm | 138 ++ contrib/contrib/WBS/99_CGI_RAWMSG.pm | 123 ++ contrib/contrib/aut.holiday | 22 + contrib/contrib/by.holiday | 19 + contrib/contrib/checkmsg.pl | 27 + contrib/contrib/crc.pl | 35 + contrib/contrib/dblog/93_DbLog.pm | 341 ++++ contrib/contrib/dblog/README | 13 + contrib/contrib/dblog/db.conf | 11 + contrib/contrib/dblog/fhemdb_create.sql | 7 + contrib/contrib/dblog/fhemdb_get.pl | 93 + contrib/contrib/em1010.pl | 474 +++++ contrib/contrib/executables | 28 + contrib/contrib/fhem2speech/ChangeLog | 45 + .../contrib/fhem2speech/JSON-XS-2.231.tar.gz | Bin 0 -> 66373 bytes .../contrib/fhem2speech/README.fhem-speech | 345 ++++ contrib/contrib/fhem2speech/extensions.conf | 163 ++ contrib/contrib/fhem2speech/fhem-speech | 1169 ++++++++++++ contrib/contrib/fhem2speech/fhem-speech.agi | 55 + contrib/contrib/fhemupdate.pl | 183 ++ contrib/contrib/fht.gnuplot | 34 + contrib/contrib/four2hex/Makefile | 6 + contrib/contrib/four2hex/README | 23 + contrib/contrib/four2hex/four2hex.c | 94 + contrib/contrib/fs20_holidays.sh | 94 + contrib/contrib/garden.pl | 212 +++ contrib/contrib/getstate/99_getstate.pm | 139 ++ contrib/contrib/getstate/README.getstate | 50 + contrib/contrib/getstate/fhem-getstate | 91 + contrib/contrib/gplotmapping.txt | 16 + contrib/contrib/he.holiday | 14 + contrib/contrib/init-scripts/fhem.1 | 46 + contrib/contrib/init-scripts/fhem.2 | 25 + contrib/contrib/init-scripts/fhem.3 | 44 + contrib/contrib/init-scripts/fhem.upstart | 14 + contrib/contrib/inputevent/89_inputevent.pm | 234 +++ contrib/contrib/inputevent/inputevent.txt | 21 + contrib/contrib/inputevent/sample.conf | 42 + .../contrib/inputevent/vdr-plugin-remote.diff | 18 + contrib/contrib/km271.pl | 192 ++ contrib/contrib/ks300avg.pl | 77 + .../contrib/remove_serialport_for_cul.patch | 105 ++ contrib/contrib/rolwzo_not_off.sh | 15 + .../rotateShiftWork/README.rotateShiftWork | 24 + .../rotateShiftWork/rotateShiftWork.sh | 102 ++ contrib/contrib/rrd/95_RRD_Log.pm | 601 +++++++ contrib/contrib/rrd/98_RRD.pm | 76 + contrib/contrib/rrd/fhz1000-rrd-howto.txt | 52 + contrib/contrib/serial.pl | 89 + contrib/contrib/survey.pl | 143 ++ contrib/contrib/tcptee.pl | 160 ++ contrib/contrib/voip2fhem/CHANGED | 3 + contrib/contrib/voip2fhem/HISTORY | 3 + contrib/contrib/voip2fhem/README | 56 + contrib/contrib/voip2fhem/README.html | 55 + contrib/contrib/voip2fhem/extensions.conf | 47 + contrib/contrib/voip2fhem/sip.conf | 13 + contrib/contrib/voip2fhem/voip2fhem | 72 + .../voip2fhem/voip2fhem_create_telefonlist | 30 + .../voip2fhem/voip2fhem_create_txt2gsm | 79 + contrib/contrib/ws2000_reader.pl | 353 ++++ 125 files changed, 22397 insertions(+) create mode 100644 contrib/contrib/00_TAHR.pm create mode 100644 contrib/contrib/1-Wire/00_OWX.pm create mode 100644 contrib/contrib/1-Wire/21_OWAD.pm create mode 100644 contrib/contrib/1-Wire/21_OWCOUNT.pm create mode 100644 contrib/contrib/1-Wire/21_OWID.pm create mode 100644 contrib/contrib/1-Wire/21_OWLCD.pm create mode 100644 contrib/contrib/1-Wire/21_OWTEMP.pm create mode 100644 contrib/contrib/1-Wire/Schaltplan_Aktives_1-Wire_Interface.png create mode 100644 contrib/contrib/1-Wire/Schaltplan_Passives_1-Wire_Interface.png create mode 100644 contrib/contrib/1-Wire/Schaltplan_USB_1-Wire_Interface.png create mode 100755 contrib/contrib/11_FHT8V.pm create mode 100644 contrib/contrib/21_OWTEMP.pm.fork create mode 100644 contrib/contrib/64_ESA.pm create mode 100755 contrib/contrib/70_NT5000.pm create mode 100644 contrib/contrib/70_SolarView.pm create mode 100644 contrib/contrib/86_FS10.pm create mode 100644 contrib/contrib/90_EIBUPDOWN.pm create mode 100755 contrib/contrib/91_DbLog.pm create mode 100644 contrib/contrib/95_FLOORPLAN/95_FLOORPLAN.pm create mode 100644 contrib/contrib/95_FLOORPLAN/99_myFloorplanList.pm create mode 100644 contrib/contrib/95_FLOORPLAN/commandref - floorplan.html create mode 100644 contrib/contrib/95_FLOORPLAN/darkfloorplanstyle.css create mode 100644 contrib/contrib/95_FLOORPLAN/fhem-floorplan-installation-guide.pdf create mode 100644 contrib/contrib/95_FLOORPLAN/fhem-floorplan-installation-guide_de.pdf create mode 100644 contrib/contrib/95_FLOORPLAN/floorplanstyle.css create mode 100755 contrib/contrib/95_VIEW.pm create mode 100644 contrib/contrib/97_GROUP.pm create mode 100755 contrib/contrib/98_FHTCONF.pm create mode 100755 contrib/contrib/99_ALARM.pm create mode 100644 contrib/contrib/99_PID.pm create mode 100755 contrib/contrib/99_PRIV.pm create mode 100755 contrib/contrib/99_SUNRISE.pm create mode 100644 contrib/contrib/99_Taupunkt.pm create mode 100644 contrib/contrib/99_dumpdef.pm create mode 100644 contrib/contrib/99_priv_cgi.pm create mode 100755 contrib/contrib/99_twitter.pm create mode 100644 contrib/contrib/DEBIAN/conffiles create mode 100755 contrib/contrib/DEBIAN/control create mode 100644 contrib/contrib/DEBIAN/postinst create mode 100755 contrib/contrib/DEBIAN/postrm create mode 100755 contrib/contrib/DEBIAN/prerm create mode 100644 contrib/contrib/FB7270/fhemcmd.sh create mode 100755 contrib/contrib/FB7270/makeimage create mode 100755 contrib/contrib/FB7270/startfhem create mode 100644 contrib/contrib/FB7390/fhemcmd.sh create mode 100755 contrib/contrib/FB7390/install create mode 100755 contrib/contrib/FB7390/makeimage create mode 100755 contrib/contrib/FB7390/startfhem create mode 100644 contrib/contrib/HM-USB-CFG/00_HMLAN.pm create mode 100644 contrib/contrib/HM-USB-CFG/DevIo.pm create mode 100644 contrib/contrib/HM-USB-CFG/README.txt create mode 100755 contrib/contrib/HMRPC/00_HMRPC.pm create mode 100644 contrib/contrib/HMRPC/01_HMDEV.pm create mode 100644 contrib/contrib/HMRPC/HMRPC.txt create mode 100644 contrib/contrib/HMRPC/import_from_webui.bsh create mode 100644 contrib/contrib/JeeStuff/00_JeeLink.pm create mode 100644 contrib/contrib/JeeStuff/18_JME.pm create mode 100644 contrib/contrib/JeeStuff/18_JSN.pm create mode 100644 contrib/contrib/JeeStuff/FHEM_JSN_BMP85.pde create mode 100644 contrib/contrib/JeeStuff/FHEM_JSN_LUX.pde create mode 100644 contrib/contrib/JeeStuff/FHEM_JSN_RoomNode.pde create mode 100755 contrib/contrib/README create mode 100644 contrib/contrib/README.FS10 create mode 100755 contrib/contrib/RSSImonitor.pl create mode 100644 contrib/contrib/WBS/18_WBS.pm create mode 100644 contrib/contrib/WBS/99_CGI_RAWMSG.pm create mode 100644 contrib/contrib/aut.holiday create mode 100644 contrib/contrib/by.holiday create mode 100755 contrib/contrib/checkmsg.pl create mode 100755 contrib/contrib/crc.pl create mode 100755 contrib/contrib/dblog/93_DbLog.pm create mode 100644 contrib/contrib/dblog/README create mode 100644 contrib/contrib/dblog/db.conf create mode 100644 contrib/contrib/dblog/fhemdb_create.sql create mode 100755 contrib/contrib/dblog/fhemdb_get.pl create mode 100755 contrib/contrib/em1010.pl create mode 100644 contrib/contrib/executables create mode 100644 contrib/contrib/fhem2speech/ChangeLog create mode 100644 contrib/contrib/fhem2speech/JSON-XS-2.231.tar.gz create mode 100644 contrib/contrib/fhem2speech/README.fhem-speech create mode 100644 contrib/contrib/fhem2speech/extensions.conf create mode 100755 contrib/contrib/fhem2speech/fhem-speech create mode 100755 contrib/contrib/fhem2speech/fhem-speech.agi create mode 100755 contrib/contrib/fhemupdate.pl create mode 100644 contrib/contrib/fht.gnuplot create mode 100644 contrib/contrib/four2hex/Makefile create mode 100644 contrib/contrib/four2hex/README create mode 100644 contrib/contrib/four2hex/four2hex.c create mode 100755 contrib/contrib/fs20_holidays.sh create mode 100755 contrib/contrib/garden.pl create mode 100644 contrib/contrib/getstate/99_getstate.pm create mode 100644 contrib/contrib/getstate/README.getstate create mode 100755 contrib/contrib/getstate/fhem-getstate create mode 100644 contrib/contrib/gplotmapping.txt create mode 100644 contrib/contrib/he.holiday create mode 100755 contrib/contrib/init-scripts/fhem.1 create mode 100755 contrib/contrib/init-scripts/fhem.2 create mode 100644 contrib/contrib/init-scripts/fhem.3 create mode 100644 contrib/contrib/init-scripts/fhem.upstart create mode 100644 contrib/contrib/inputevent/89_inputevent.pm create mode 100644 contrib/contrib/inputevent/inputevent.txt create mode 100644 contrib/contrib/inputevent/sample.conf create mode 100644 contrib/contrib/inputevent/vdr-plugin-remote.diff create mode 100644 contrib/contrib/km271.pl create mode 100755 contrib/contrib/ks300avg.pl create mode 100644 contrib/contrib/remove_serialport_for_cul.patch create mode 100755 contrib/contrib/rolwzo_not_off.sh create mode 100644 contrib/contrib/rotateShiftWork/README.rotateShiftWork create mode 100755 contrib/contrib/rotateShiftWork/rotateShiftWork.sh create mode 100644 contrib/contrib/rrd/95_RRD_Log.pm create mode 100644 contrib/contrib/rrd/98_RRD.pm create mode 100644 contrib/contrib/rrd/fhz1000-rrd-howto.txt create mode 100755 contrib/contrib/serial.pl create mode 100755 contrib/contrib/survey.pl create mode 100644 contrib/contrib/tcptee.pl create mode 100644 contrib/contrib/voip2fhem/CHANGED create mode 100644 contrib/contrib/voip2fhem/HISTORY create mode 100644 contrib/contrib/voip2fhem/README create mode 100644 contrib/contrib/voip2fhem/README.html create mode 100644 contrib/contrib/voip2fhem/extensions.conf create mode 100644 contrib/contrib/voip2fhem/sip.conf create mode 100755 contrib/contrib/voip2fhem/voip2fhem create mode 100755 contrib/contrib/voip2fhem/voip2fhem_create_telefonlist create mode 100755 contrib/contrib/voip2fhem/voip2fhem_create_txt2gsm create mode 100644 contrib/contrib/ws2000_reader.pl diff --git a/contrib/contrib/00_TAHR.pm b/contrib/contrib/00_TAHR.pm new file mode 100644 index 000000000..4b460078e --- /dev/null +++ b/contrib/contrib/00_TAHR.pm @@ -0,0 +1,153 @@ +############################################### +# Sample fhem module, one-level approach, controlling a single device like a +# directly attached heating regulator. +# The alternative is a two level approach, where a physical device like a CUL +# is a bridge to a large number of logical devices (like FS20 actors, S300 +# sensors, etc) + +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); + +sub TAHR_Read($); +sub TAHR_Ready($); +sub TAHR_setbits($$); +sub TAHR_SetReading($$$$); + +my %tahr_sets = ( + "ww_soll" => "0C07656565%02x6565", + "ww_betriebsart" => "0C0E%02x6565656565", +); + + +sub +TAHR_Initialize($) +{ + my ($hash) = @_; + + require "$attr{global}{modpath}/FHEM/DevIo.pm"; + + $hash->{ReadFn} = "TAHR_Read"; + $hash->{ReadyFn} = "TAHR_Ready"; + $hash->{DefFn} = "TAHR_Define"; + $hash->{UndefFn} = "TAHR_Undef"; + $hash->{SetFn} = "TAHR_Set"; + $hash->{AttrList}= "do_not_notify:1,0 loglevel:0,1,2,3,4,5,6"; +} + +##################################### +sub +TAHR_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + return "wrong syntax: define TAHR [devicename|none]" + if(@a != 3); + + DevIo_CloseDev($hash); + my $name = $a[0]; + my $dev = $a[2]; + + if($dev eq "none") { + Log 1, "TAHR device is none, commands will be echoed only"; + return undef; + } + + $hash->{DeviceName} = $dev; + my $ret = DevIo_OpenDev($hash, 0, "TAHR_Poll"); + return $ret; +} + + +##################################### +sub +TAHR_Undef($$) +{ + my ($hash, $arg) = @_; + DevIo_CloseDev($hash); + RemoveInternalTimer($hash); + return undef; +} + +##################################### +sub +TAHR_Set($@) +{ + my ($hash, @a) = @_; + + return "\"set TAHR\" needs at least an argument" if(@a < 2); + + my $cmd = $tahr_sets{$a[1]}; + return "Unknown argument $a[1], choose one of " . + join(" ", sort keys %tahr_sets) if(!defined($cmd)); + # FIXME + DevIo_SimpleWrite($hash, $cmd); + return undef; +} + + +##################################### +# called from the global loop, when the select for hash->{FD} reports data +sub +TAHR_Read($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my ($data, $crc); + + my $buf = DevIo_SimpleRead($hash); + return "" if(!defined($buf)); + + $buf = unpack('H*', $buf); + Log 5, "RAW: $buf"; + + ###################################### + # Analyze the data + my $tn = TimeNow(); + my ($key, $val) = ("key", "val"); + + # FIXME + TAHR_SetReading($hash, $tn, $key, $val); +} + +##################################### +sub +TAHR_Ready($) +{ + my ($hash) = @_; + + return DevIo_OpenDev($hash, 1, undef) + if($hash->{STATE} eq "disconnected"); + + # This is relevant for windows/USB only + my $po = $hash->{USBDev}; + my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; + return ($InBytes>0); +} + +sub +TAHR_Poll($) +{ + my ($hash) = @_; + return if($hash->{STATE} eq "disconnected"); + # FIXME + DevIo_SimpleWrite($hash, "02"); # Request data + InternalTimer(gettimeofday()+5, "TAHR_Poll", $hash, 0); +} + +sub +TAHR_SetReading($$$$) +{ + my ($hash,$tn,$key,$val) = @_; + my $name = $hash->{NAME}; + Log GetLogLevel($name,4), "$name: $key $val"; + $hash->{READINGS}{$key}{TIME} = $tn; + $hash->{READINGS}{$key}{VAL} = $val; + DoTrigger($name, "$key: $val"); +} + + +1; diff --git a/contrib/contrib/1-Wire/00_OWX.pm b/contrib/contrib/1-Wire/00_OWX.pm new file mode 100644 index 000000000..c2effa4ee --- /dev/null +++ b/contrib/contrib/1-Wire/00_OWX.pm @@ -0,0 +1,1590 @@ +######################################################################################## +# +# OWX.pm +# +# FHEM module to commmunicate directly with 1-Wire bus devices +# via an active DS2480/DS2490/DS9097U bus master interface or +# via a passive DS9097 interface +# +# Version 1.11 - March, 2012 +# +# Prof. Dr. Peter A. Henning, 2012 +# +# Setup interface as: +# +# define OWX +# +# where may be replaced by any name string +# is a serial (USB) device +# +# get alarms => find alarmed 1-Wire devices +# get devices => find all 1-Wire devices +# +# set interval => set period for temperature conversion and alarm testing +# set followAlarms on/off => determine whether an alarm is followed by a search for +# alarmed devices +# +# attr buspower real/parasitic - whether the 1-Wire bus is really powered or +# the 1-Wire devices take their power from the data wire (parasitic is default !) +# +# Ordering of subroutines in this module +# 1. Subroutines independent of bus interface type +# 2. Subroutines for a specific type of the interface +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +use strict; +use warnings; +use Device::SerialPort; + +# Prototypes to make komodo happy +use vars qw{%attr %defs}; +sub Log($$); + +# Line counter +my $cline=0; + +# These we may get on request +my %gets = ( + "alarms" => "A", + "devices" => "D" +); + +# These occur in a pulldown menu as settable values for the bus master +my %sets = ( + "interval" => "T", + "followAlarms" => "F" +); + +# These are attributes +my %attrs = ( +); + +#-- some globals needed for the 1-Wire module +my $owx_serport; +#-- baud rate serial interface +my $owx_baud=9600; +#-- Debugging +my $owx_debug=0; +#-- bus master mode +my $owx_mode="undef"; +#-- bus interface +my $owx_interface=""; +#-- 8 byte 1-Wire device address +my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); +#-- List of addresses found on the bus +my @owx_devs=(); +my @owx_fams=(); +my @owx_alarm_devs=(); +#-- 16 byte search string +my @owx_search=(0,0,0,0 ,0,0,0,0, 0,0,0,0, 0,0,0,0); +#-- search state for 1-Wire bus search +my $owx_LastDiscrepancy = 0; +my $owx_LastFamilyDiscrepancy = 0; +my $owx_LastDeviceFlag = 0; + + +######################################################################################## +# +# The following subroutines are independent of the bus interface +# +######################################################################################## +# +# OWX_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWX_Initialize ($) { + my ($hash) = @_; + #-- Provider + #$hash->{Clients} = ":OWCOUNT:OWHUB:OWLCD:OWMULTI:OWSWITCH:OWTEMP:"; + $hash->{Clients} = ":OWAD:OWCOUNT:OWID:OWLCD:OWTEMP:"; + + #-- Normal Devices + $hash->{DefFn} = "OWX_Define"; + $hash->{UndefFn} = "OWX_Undef"; + $hash->{GetFn} = "OWX_Get"; + $hash->{SetFn} = "OWX_Set"; + $hash->{AttrList}= "loglevel:0,1,2,3,4,5,6 buspower:real,parasitic"; +} + +######################################################################################## +# +# OWX_Alarms - Find devices on the 1-Wire bus, +# which have the alarm flag set +# +# Parameter hash = hash of bus master +# +# Return 1 : OK +# 0 : no device present +# +######################################################################################## + +sub OWX_Alarms ($) { + my ($hash) = @_; + my @owx_alarm_names=(); + + #-- Discover all alarmed devices on the 1-Wire bus + @owx_alarm_devs=(); + my $res = OWX_First($hash,"alarm"); + while( $owx_LastDeviceFlag==0 && $res != 0){ + $res = $res & OWX_Next($hash,"alarm"); + } + if( @owx_alarm_devs == 0){ + return "OWX: No alarmed 1-Wire devices found "; + } + + #-- walk through all the devices to get their proper fhem names + foreach my $fhem_dev (sort keys %main::defs) { + #-- skip if busmaster + next if( $hash->{NAME} eq $main::defs{$fhem_dev}{NAME} ); + #-- all OW types start with OW + next if( substr($main::defs{$fhem_dev}{TYPE},0,2) ne "OW"); + foreach my $owx_dev (@owx_alarm_devs) { + #-- two pieces of the ROM ID found on the bus + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + my $id_owx = $owx_f.".".$owx_rnf; + + #-- skip if not in alarm list + if( $owx_dev eq $main::defs{$fhem_dev}{ROM_ID} ){ + $main::defs{$fhem_dev}{STATE} = "Alarmed"; + push(@owx_alarm_names,$main::defs{$fhem_dev}{NAME}); + } + } + } + #-- so far, so good - what do we want to do with this ? + return "OWX: Alarmed 1-Wire devices found (".join(",",@owx_alarm_names).")"; +} + +######################################################################################## +# +# OWX_Block - Send data block +# +# Parameter hash = hash of bus master, data = string to send +# +# Return response, if OK +# 0 if not OK +# +######################################################################################## + +sub OWX_Block ($$) { + my ($hash,$data) =@_; + + if( $owx_interface eq "DS2480" ){ + return OWX_Block_2480($hash,$data); + }elsif( $owx_interface eq "DS9097" ){ + return OWX_Block_9097($hash,$data); + }else{ + Log 1,"OWX: Block called with unknown interface"; + return 0; + } +} + +######################################################################################## +# +# OWX_CRC - Check the CRC8 code of a device address in @owx_ROM_ID +# +# Parameter romid = if not zero, return the CRC8 value instead of checking it +# +######################################################################################## + +sub OWX_CRC ($) { + my ($romid) = @_; + + my @crc8_table = ( + 0, 94,188,226, 97, 63,221,131,194,156,126, 32,163,253, 31, 65, + 157,195, 33,127,252,162, 64, 30, 95, 1,227,189, 62, 96,130,220, + 35,125,159,193, 66, 28,254,160,225,191, 93, 3,128,222, 60, 98, + 190,224, 2, 92,223,129, 99, 61,124, 34,192,158, 29, 67,161,255, + 70, 24,250,164, 39,121,155,197,132,218, 56,102,229,187, 89, 7, + 219,133,103, 57,186,228, 6, 88, 25, 71,165,251,120, 38,196,154, + 101, 59,217,135, 4, 90,184,230,167,249, 27, 69,198,152,122, 36, + 248,166, 68, 26,153,199, 37,123, 58,100,134,216, 91, 5,231,185, + 140,210, 48,110,237,179, 81, 15, 78, 16,242,172, 47,113,147,205, + 17, 79,173,243,112, 46,204,146,211,141,111, 49,178,236, 14, 80, + 175,241, 19, 77,206,144,114, 44,109, 51,209,143, 12, 82,176,238, + 50,108,142,208, 83, 13,239,177,240,174, 76, 18,145,207, 45,115, + 202,148,118, 40,171,245, 23, 73, 8, 86,180,234,105, 55,213,139, + 87, 9,235,181, 54,104,138,212,149,203, 41,119,244,170, 72, 22, + 233,183, 85, 11,136,214, 52,106, 43,117,151,201, 74, 20,246,168, + 116, 42,200,150, 21, 75,169,247,182,232, 10, 84,215,137,107, 53); + + my $crc8=0; + + if( $romid eq "0" ){ + for(my $i=0; $i<8; $i++){ + $crc8 = $crc8_table[ $crc8 ^ $owx_ROM_ID[$i] ]; + } + return $crc8; + } else { + #-- from search string to byte id + $romid=~s/\.//g; + for(my $i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($romid,2*$i,2)); + } + for(my $i=0; $i<7; $i++){ + $crc8 = $crc8_table[ $crc8 ^ $owx_ROM_ID[$i] ]; + } + return $crc8; + } +} + +######################################################################################## +# +# OWX_CRC16 - Calculate the CRC16 code of a string +# +# Parameter crc - previous CRC code, c next character +# +######################################################################################## + +sub OWX_CRC16($) { + my ($data) = @_; + + my $crc=0; + for( my $i=0; $i New CRC value = %x",$crc; + } + return $crc; +} + + +sub OWX_DOCRC16($$) { + my ($crc,$c) = @_; + + #-- polynomial for x^16 + x^15 + x^2 + 1 + my $mask = 0xA001; + + my $i; + for($i=0;$i<8;$i++) { + if(($crc ^ ord($c)) & 1) { + $crc=($crc>>1)^$mask; + } else { + $crc>>=1; + } + $c>>=1; + } + return ($crc); +} + +#//-------------------------------------------------------------------# + +#Aufruf der Funktion im Programm: + +#{ +#//... +# unsigned int DEVICE_CRC16=0; +# DEVICE_CRC16 = calcCRC16r (DEVICE_CRC16,chr,0xA001); +#//... +#} + + + +######################################################################################## +# +# OWX_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################## + +sub OWX_Define ($$) { + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + if(int(@a) >= 3){ + #-- check syntax + Log 1,"OWX: Warning - Some parameter(s) ignored, must be define OWX" + if(int(@a) > 3); + #-- If this line contains 3 parameters, it is the bus master definition + my $dev = $a[2]; + $hash->{DeviceName} = $dev; + #-- Dummy 1-Wire ROM identifier + $hash->{ROM_ID} = "FF"; + + #-- First step: open the serial device to test it + #Log 3, "OWX opening device $dev"; + $owx_serport = new Device::SerialPort ($dev); + return "OWX: Can't open $dev: $!" if(!$owx_serport); + Log 3, "OWX: opened device $dev"; + $owx_serport->reset_error(); + $owx_serport->baudrate(9600); + $owx_serport->databits(8); + $owx_serport->parity('none'); + $owx_serport->stopbits(1); + $owx_serport->handshake('none'); + $owx_serport->write_settings; + #-- sleeping for some time + select(undef,undef,undef,0.1); + #$owx_serport->close(); + + #-- Second step: see, if a bus interface is detected + if (!OWX_Detect($hash)){ + $hash->{STATE} = "Failed"; + $hash->{PRESENT} = 0; + $init_done = 1; + return undef; + } + + #-- In 10 seconds discover all devices on the 1-Wire bus + InternalTimer(gettimeofday()+5, "OWX_Discover", $hash,0); + + #-- Default settings + $hash->{interval} = 60; # kick every minute + $hash->{followAlarms} = "off"; + $hash->{ALARMED} = "no"; + + #-- InternalTimer blocks if init_done is not true + my $oid = $init_done; + $hash->{PRESENT} = 1; + #$hash->{TYPE} = "OWX"; + #$hash->{T} = "OWX"; + $hash->{STATE} = "Initialized"; + $hash->{INTERFACE} = $owx_interface; + $init_done = 1; + + #-- Intiate first alarm detection and eventually conversion in a minute or so + InternalTimer(gettimeofday() + 60, "OWX_Kick", $hash,1); + $init_done = $oid; + $hash->{STATE} = "Active"; + return undef; + } else { + #-- check syntax + return "OWX: Syntax error - must be define OWX" + } +} + +######################################################################################## +# +# OWX_Detect - Detect 1-Wire interface +# +# Method rather crude - treated as an 2480, and see whatis returned +# +# Parameter hash = hash of bus master +# +# Return 1 : OK +# 0 : not OK +# +######################################################################################## + +sub OWX_Detect ($) { + my ($hash) = @_; + + my ($i,$j,$k,$l,$res,$ret,$ress); + #-- timing byte for DS2480 + OWX_Query_2480($hash,"\xC1\xC1"); + + #-- Max 4 tries to detect an interface + for($l=0;$l<4;$l++) { + #-- write 1-Wire bus (Fig. 2 of Maxim AN192) + $res = OWX_Query_2480($hash,"\x17\x45\x5B\x0F\x91"); + #$ress = "OWX: Answer was "; + #for($i=0;$i{NAME} eq $main::defs{$fhem_dev}{NAME} ); + #-- all OW types start with OW + next if( substr($main::defs{$fhem_dev}{TYPE},0,2) ne "OW"); + my $id_fhem = substr($main::defs{$fhem_dev}{ROM_ID},0,15); + #-- skip interface device + next if( length($id_fhem) != 15 ); + #-- testing if present in defined devices + # even with improper family + #print " FHEM-Device = ".substr($id_fhem,3,12)." OWX discovered device ".substr($id_owx,3,12)."\n"; + if( substr($id_fhem,3,12) eq substr($id_owx,3,12) ) { + #-- warn if improper family id + if( substr($id_fhem,0,2) ne substr($id_owx,0,2) ){ + Log 1, "OWX: Warning, $fhem_dev is defined with improper family id ".substr($id_fhem,0,2). + ", correcting to ".substr($id_owx,0,2); + $main::defs{$fhem_dev}{OW_FAMILY} = substr($id_owx,0,2); + } + push(@owx_names,$main::defs{$fhem_dev}{NAME}); + #-- replace the ROM ID by the proper value including CRC + $main::defs{$fhem_dev}{ROM_ID}=$owx_dev; + $main::defs{$fhem_dev}{PRESENT}=1; + $match = 1; + last; + } + # + } + + #-- autocreate the device + if( $match==0 ){ + #-- Default name OWX_FF_XXXXXXXXXXXX, default type = OWX_FF + my $name = sprintf "OWX_%s_%s",$owx_f,$owx_rnf; + #-- Family 10 = Temperature sensor, assume DS1820 as default + if( $owx_f eq "10" ){ + CommandDefine(undef,"$name OWTEMP DS1820 $owx_rnf"); + #-- Family 1D = Counter/RAM, assume DS2423 as default + }elsif( $owx_f eq "1D" ){ + CommandDefine(undef,"$name OWCOUNT DS2423 $owx_rnf"); + #-- Family 20 = A/D converter, assume DS2450 as default + } elsif( $owx_f eq "20" ){ + CommandDefine(undef,"$name OWAD DS2450 $owx_rnf"); + #-- Family 22 = Temperature sensor, assume DS1822 as default + }elsif( $owx_f eq "22" ){ + CommandDefine(undef,"$name OWTEMP DS1822 $owx_rnf"); + #-- Family 10 28 = Temperature sensor, assume DS18B20 as default + }elsif( $owx_f eq "28" ){ + CommandDefine(undef,"$name OWTEMP DS18B20 $owx_rnf"); + #-- Family FF = LCD display + }elsif( $owx_f eq "FF" ){ + CommandDefine(undef,"$name OWLCD $owx_rnf"); + #-- All unknown families are ID only + } else { + CommandDefine(undef,"$name OWID $owx_f $owx_rnf"); + } + #-- yes, it is on the bus and therefore present + push(@owx_names,$name); + $main::defs{$name}{PRESENT}=1; + #-- default room + CommandAttr (undef,"$name IODev $hash->{NAME}"); + CommandAttr (undef,"$name room OWX"); + #-- replace the ROM ID by the proper value + $main::defs{$name}{ROM_ID}=$owx_dev; + } + } + + #-- final step: Undefine all 1-Wire devices which are not on the bus + # TODO: IF WE HAVE MULTIPLE IO Devices ??? + foreach my $fhem_dev (sort keys %main::defs) { + #-- skip if malformed device + #next if( !defined($main::defs{$fhem_dev}{NAME}) ); + #-- all OW types start with OW + next if( substr($main::defs{$fhem_dev}{TYPE},0,2) ne "OW"); + #-- skip if the device is present. + next if( $main::defs{$fhem_dev}{PRESENT} == 1); + Log 1, "OWX: Deleting unused 1-Wire device $main::defs{$fhem_dev}{NAME} of type $main::defs{$fhem_dev}{TYPE}"; + CommandDelete(undef,$main::defs{$fhem_dev}{NAME}); + } + Log 1, "OWX: 1-Wire devices found (".join(",",@owx_names).")"; + return "OWX: 1-Wire devices found (".join(",",@owx_names).")"; +} + +######################################################################################## +# +# OWX_First - Find the 'first' devices on the 1-Wire bus +# +# Parameter hash = hash of bus master, mode +# +# Return 1 : device found, ROM number pushed to list +# 0 : no device present +# +######################################################################################## + +sub OWX_First ($$) { + my ($hash,$mode) = @_; + + #-- clear 16 byte of search data + @owx_search=(0,0,0,0 ,0,0,0,0, 0,0,0,0, 0,0,0,0); + #-- reset the search state + $owx_LastDiscrepancy = 0; + $owx_LastDeviceFlag = 0; + $owx_LastFamilyDiscrepancy = 0; + #-- now do the search + return OWX_Search($hash,$mode); +} + +######################################################################################## +# +# OWX_Get - Implements GetFn function +# +# Parameter hash = hash of the bus master a = argument array +# +######################################################################################## + +sub OWX_Get($@) { + my ($hash, @a) = @_; + return "OWX: Get needs exactly one parameter" if(@a != 2); + + my $name = $hash->{NAME}; + my $owx_dev = $hash->{ROM_ID}; + + if( $a[1] eq "alarms") { + my $res = OWX_Alarms($hash); + #-- process result + return $res + + } elsif( $a[1] eq "devices") { + my $res = OWX_Discover($hash); + #-- process result + return $res + + } else { + return "OWX: Get with unknown argument $a[1], choose one of ". + join(",", sort keys %gets); + } +} + +######################################################################################## +# +# OWX_Kick - Initiate some processes in all devices +# +# Parameter hash = hash of bus master +# +# Return 1 : OK +# 0 : Not OK +# +######################################################################################## + +sub OWX_Kick($) { + + my($hash) = @_; + + my $ret; + #-- Call us in n seconds again. + InternalTimer(gettimeofday()+ $hash->{interval}, "OWX_Kick", $hash,1); + #-- During reset we see if an alarmed device is present. + OWX_Reset($hash); + + #-- Only if we have real power on the bus + if( defined($attr{$hash->{NAME}}{buspower}) && ($attr{$hash->{NAME}}{buspower} eq "real") ){ + #-- issue the skip ROM command \xCC followed by start conversion command \x44 + $ret = OWX_Block($hash,"\xCC\x44"); + if( $ret eq 0 ){ + Log 3, "OWX: Failure in temperature conversion\n"; + return 0; + } + #-- sleeping for some time + select(undef,undef,undef,0.5); + } + + return 1; +} + +######################################################################################## +# +# OWX_Next - Find the 'next' devices on the 1-Wire bus +# +# Parameter hash = hash of bus master, mode +# +# Return 1 : device found, ROM number in owx_ROM_ID and pushed to list (LastDeviceFlag=0) +# or only in owx_ROM_ID (LastDeviceFlag=1) +# 0 : device not found, or ot searched at all +# +######################################################################################## + +sub OWX_Next ($$) { + my ($hash,$mode) = @_; + #-- now do the search + return OWX_Search($hash,$mode); +} + +######################################################################################## +# +# OWX_Reset - Reset the 1-Wire bus +# +# Parameter hash = hash of bus master +# +# Return 1 : OK +# 0 : not OK +# +######################################################################################## + +sub OWX_Reset ($) { + + my ($hash)=@_; + if( $owx_interface eq "DS2480" ){ + return OWX_Reset_2480($hash); + }elsif( $owx_interface eq "DS9097" ){ + return OWX_Reset_9097($hash); + }else{ + Log 1,"OWX: Reset called with unknown interface"; + return 0; + } +} + +######################################################################################## +# +# OWX_Search - Perform the 1-Wire Search Algorithm on the 1-Wire bus using the existing +# search state. +# +# Parameter hash = hash of bus master, mode=alarm,discover or verify +# +# Return 1 : device found, ROM number in owx_ROM_ID and pushed to list (LastDeviceFlag=0) +# or only in owx_ROM_ID (LastDeviceFlag=1) +# 0 : device not found, or ot searched at all +# +######################################################################################## + +sub OWX_Search ($$) { + my ($hash,$mode)=@_; + + #-- if the last call was the last one, no search + if ($owx_LastDeviceFlag==1){ + return 0; + } + #-- 1-Wire reset + if (OWX_Reset($hash)==0){ + #-- reset the search + Log 1, "OWX: Search reset failed"; + $owx_LastDiscrepancy = 0; + $owx_LastDeviceFlag = 0; + $owx_LastFamilyDiscrepancy = 0; + return 0; + } + + #-- Here we call the device dependent part + if( $owx_interface eq "DS2480" ){ + OWX_Search_2480($hash,$mode); + }elsif( $owx_interface eq "DS9097" ){ + OWX_Search_9097($hash,$mode); + }else{ + Log 1,"OWX: Search called with unknown interface"; + return 0; + } + #--check if we really found a device + if( OWX_CRC(0)!= 0){ + #-- reset the search + Log 1, "OWX: Search CRC failed "; + $owx_LastDiscrepancy = 0; + $owx_LastDeviceFlag = 0; + $owx_LastFamilyDiscrepancy = 0; + return 0; + } + + #-- character version of device ROM_ID, first byte = family + my $dev=sprintf("%02X.%02X%02X%02X%02X%02X%02X.%02X",@owx_ROM_ID); + + #-- for some reason this does not work - replaced by another test, see below + #if( $owx_LastDiscrepancy==0 ){ + # $owx_LastDeviceFlag=1; + #} + #-- + if( $owx_LastDiscrepancy==$owx_LastFamilyDiscrepancy ){ + $owx_LastFamilyDiscrepancy=0; + } + + #-- mode was to verify presence of a device + if ($mode eq "verify") { + Log 5, "OWX: Device verified $dev"; + return 1; + #-- mode was to discover devices + } elsif( $mode eq "discover" ){ + #-- check families + my $famfnd=0; + foreach (@owx_fams){ + if( substr($dev,0,2) eq $_ ){ + #-- if present, set the fam found flag + $famfnd=1; + last; + } + } + push(@owx_fams,substr($dev,0,2)) if( !$famfnd ); + foreach (@owx_devs){ + if( $dev eq $_ ){ + #-- if present, set the last device found flag + $owx_LastDeviceFlag=1; + last; + } + } + if( $owx_LastDeviceFlag!=1 ){ + #-- push to list + push(@owx_devs,$dev); + Log 5, "OWX: New device found $dev"; + } + return 1; + + #-- mode was to discover alarm devices + } else { + for(my $i=0;$i<@owx_alarm_devs;$i++){ + if( $dev eq $owx_alarm_devs[$i] ){ + #-- if present, set the last device found flag + $owx_LastDeviceFlag=1; + last; + } + } + if( $owx_LastDeviceFlag!=1 ){ + #--push to list + push(@owx_alarm_devs,$dev); + Log 5, "OWX: New alarm device found $dev"; + } + return 1; + } +} + +######################################################################################## +# +# OWX_Set - Implements SetFn function +# +# Parameter hash , a = argument array +# +######################################################################################## + +sub OWX_Set($@) { + my ($hash, @a) = @_; + my $name = shift @a; + my $res; + + #-- First we need to find the ROM ID corresponding to the device name + my $owx_romid = $hash->{ROM_ID}; + Log 5, "OWX_Set request $name $owx_romid ".join(" ",@a); + + #-- for the selector: which values are possible + return join(" ", sort keys %sets) if(@a != 2); + return "OWX_Set: With unknown argument $a[0], choose one of " . join(" ", sort keys %sets) + if(!defined($sets{$a[0]})); + + #-- Set timer value + if( $a[0] eq "interval" ){ + #-- only values >= 15 secs allowed + if( $a[1] >= 15){ + $hash->{interval} = $a[1]; + $res = 1; + } else { + $res = 0; + } + } + + #-- Set alarm behaviour + if( $a[0] eq "followAlarms" ){ + #-- only values >= 15 secs allowed + if( (lc($a[1]) eq "off") && ($hash->{followAlarms} eq "on") ){ + $hash->{interval} = "off"; + $res = 1; + }elsif( (lc($a[1]) eq "on") && ($hash->{followAlarms} eq "off") ){ + $hash->{interval} = "off"; + $res = 1; + } else { + $res = 0; + } + + } + Log GetLogLevel($name,3), "OWX_Set $name ".join(" ",@a)." => $res"; + DoTrigger($name, undef) if($init_done); + return "OWX_Set => $name ".join(" ",@a)." => $res"; +} + +######################################################################################## +# +# OWX_Undef - Implements UndefFn function +# +# Parameter hash = hash of the bus master, name +# +######################################################################################## + +sub OWX_Undef ($$) { + my ($hash, $name) = @_; + RemoveInternalTimer($hash); + return undef; +} + +######################################################################################## +# +# OWX_Verify - Verify a particular device on the 1-Wire bus +# +# Parameter hash = hash of bus master, dev = 8 Byte ROM ID of device to be tested +# +# Return 1 : device found +# 0 : device not +# +######################################################################################## + +sub OWX_Verify ($$) { + my ($hash,$dev) = @_; + my $i; + #-- from search string to byte id + my $devs=$dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + #-- reset the search state + $owx_LastDiscrepancy = 64; + $owx_LastDeviceFlag = 0; + #-- now do the search + my $res=OWX_Search($hash,"verify"); + my $dev2=sprintf("%02X.%02X%02X%02X%02X%02X%02X.%02X",@owx_ROM_ID); + #-- reset the search state + $owx_LastDiscrepancy = 0; + $owx_LastDeviceFlag = 0; + #-- check result + if ($dev eq $dev2){ + return 1; + }else{ + return 0; + } +} + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a DS2480 bus interface +# +######################################################################################### +# +# OWX_Block_2480 - Send data block (Fig. 6 of Maxim AN192) +# +# Parameter hash = hash of bus master, data = string to send +# +# Return response, if OK +# 0 if not OK +# +######################################################################################## + +sub OWX_Block_2480 ($$) { + my ($hash,$data) =@_; + + my $data2=""; + #-- if necessary, prepend E1 character for data mode + if( ($owx_mode ne "data") && (substr($data,0,1) ne '\xE1')) { + $data2 = "\xE1"; + } + #-- all E3 characters have to be duplicated + for(my $i=0;$i{DeviceName}; + + $owx_serport->baudrate($owx_baud); + $owx_serport->write_settings; + + if( $owx_debug > 1){ + my $res = "OWX: Sending out "; + for($i=0;$iwrite($cmd); + + Log 1, "OWX: Write incomplete $count_out ne ".(length($cmd))."" if ( $count_out != length($cmd) ); + #-- sleeping for some time + select(undef,undef,undef,0.04); + + #-- read the data + my ($count_in, $string_in) = $owx_serport->read(48); + + if( $owx_debug > 1){ + my $res = "OWX: Receiving "; + for($i=0;$i<$count_in;$i++){ + $j=int(ord(substr($string_in,$i,1))/16); + $k=ord(substr($string_in,$i,1))%16; + $res.=sprintf "0x%1x%1x ",$j,$k; + } + Log 3, $res; + } + + #-- sleeping for some time + select(undef,undef,undef,0.04); + + #$owx_serport->close(); + return($string_in); +} + +######################################################################################## +# +# OWX_Reset_2480 - Reset the 1-Wire bus (Fig. 4 of Maxim AN192) +# +# Parameter hash = hash of bus master +# +# Return 1 : OK +# 0 : not OK +# +######################################################################################## + +sub OWX_Reset_2480 ($) { + + my ($hash)=@_; + my $cmd=""; + + my ($res,$r1,$r2); + #-- if necessary, prepend \xE3 character for command mode + if( $owx_mode ne "command" ) { + $cmd = "\xE3"; + } + #-- Reset command \xC5 + $cmd = $cmd."\xC5"; + #-- write 1-Wire bus + $res =OWX_Query_2480($hash,$cmd); + + #-- if not ok, try for max. a second time + $r1 = ord(substr($res,0,1)) & 192; + if( $r1 != 192){ + $res =OWX_Query_2480($hash,$cmd); + } + + #-- process result + $r1 = ord(substr($res,0,1)) & 192; + if( $r1 != 192){ + Log 3, "OWX: Reset failure"; + return 0; + } + $hash->{ALARMED} = "no"; + + $r2 = ord(substr($res,0,1)) & 3; + + if( $r2 == 3 ){ + Log 3, "OWX: No presence detected"; + return 0; + }elsif( $r2 ==2 ){ + Log 1, "OWX: Alarm presence detected"; + $hash->{ALARMED} = "yes"; + } + return 1; +} + +######################################################################################## +# +# OWX_Search_2480 - Perform the 1-Wire Search Algorithm on the 1-Wire bus using the existing +# search state. +# +# Parameter hash = hash of bus master, mode=alarm,discover or verify +# +# Return 1 : device found, ROM number in owx_ROM_ID and pushed to list (LastDeviceFlag=0) +# or only in owx_ROM_ID (LastDeviceFlag=1) +# 0 : device not found, or ot searched at all +# +######################################################################################## + +sub OWX_Search_2480 ($$) { + my ($hash,$mode)=@_; + + my ($sp1,$sp2,$response,$search_direction,$id_bit_number); + + #-- Response search data parsing operates bytewise + $id_bit_number = 1; + + select(undef,undef,undef,0.5); + + #-- clear 16 byte of search data + @owx_search=(0,0,0,0 ,0,0,0,0, 0,0,0,0, 0,0,0,0); + #-- Output search data construction (Fig. 9 of Maxim AN192) + # operates on a 16 byte search response = 64 pairs of two bits + while ( $id_bit_number <= 64) { + #-- address single bits in a 16 byte search string + my $newcpos = int(($id_bit_number-1)/4); + my $newimsk = ($id_bit_number-1)%4; + #-- address single bits in a 8 byte id string + my $newcpos2 = int(($id_bit_number-1)/8); + my $newimsk2 = ($id_bit_number-1)%8; + + if( $id_bit_number <= $owx_LastDiscrepancy){ + #-- first use the ROM ID bit to set the search direction + if( $id_bit_number < $owx_LastDiscrepancy ) { + $search_direction = ($owx_ROM_ID[$newcpos2]>>$newimsk2) & 1; + #-- at the last discrepancy search into 1 direction anyhow + } else { + $search_direction = 1; + } + #-- fill into search data; + $owx_search[$newcpos]+=$search_direction<<(2*$newimsk+1); + } + #--increment number + $id_bit_number++; + } + #-- issue data mode \xE1, the normal search command \xF0 or the alarm search command \xEC + # and the command mode \xE3 / start accelerator \xB5 + if( $mode ne "alarm" ){ + $sp1 = "\xE1\xF0\xE3\xB5"; + } else { + $sp1 = "\xE1\xEC\xE3\xB5"; + } + #-- issue data mode \xE1, device ID, command mode \xE3 / end accelerator \xA5 + $sp2=sprintf("\xE1%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c\xE3\xA5",@owx_search); + $response = OWX_Query_2480($hash,$sp1); + $response = OWX_Query_2480($hash,$sp2); + + #-- interpret the return data + if( length($response)!=16 ) { + Log 3, "OWX: Search 2nd return has wrong parameter with length = ".length($response).""; + return 0; + } + #-- Response search data parsing (Fig. 11 of Maxim AN192) + # operates on a 16 byte search response = 64 pairs of two bits + $id_bit_number = 1; + #-- clear 8 byte of device id for current search + @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + + while ( $id_bit_number <= 64) { + #-- adress single bits in a 16 byte string + my $newcpos = int(($id_bit_number-1)/4); + my $newimsk = ($id_bit_number-1)%4; + + #-- retrieve the new ROM_ID bit + my $newchar = substr($response,$newcpos,1); + + #-- these are the new bits + my $newibit = (( ord($newchar) >> (2*$newimsk) ) & 2) / 2; + my $newdbit = ( ord($newchar) >> (2*$newimsk) ) & 1; + + #-- output for test purpose + #print "id_bit_number=$id_bit_number => newcpos=$newcpos, newchar=0x".int(ord($newchar)/16). + # ".".int(ord($newchar)%16)." r$id_bit_number=$newibit d$id_bit_number=$newdbit\n"; + + #-- discrepancy=1 and ROM_ID=0 + if( ($newdbit==1) and ($newibit==0) ){ + $owx_LastDiscrepancy=$id_bit_number; + if( $id_bit_number < 9 ){ + $owx_LastFamilyDiscrepancy=$id_bit_number; + } + } + #-- fill into device data; one char per 8 bits + $owx_ROM_ID[int(($id_bit_number-1)/8)]+=$newibit<<(($id_bit_number-1)%8); + + #-- increment number + $id_bit_number++; + } + return 1; +} + +######################################################################################## +# +# OWX_WriteBytePower_2480 - Send byte to bus with power increase (Fig. 16 of Maxim AN192) +# +# Parameter hash = hash of bus master, dbyte = byte to send +# +# Return 1 : OK +# 0 : not OK +# +######################################################################################## + +sub OWX_WriteBytePower_2480 ($$) { + + my ($hash,$dbyte) =@_; + my $cmd="\x3F"; + my $ret="\x3E"; + #-- if necessary, prepend \xE3 character for command mode + if( $owx_mode ne "command") { + $cmd = "\xE3".$cmd; + } + #-- distribute the bits of data byte over several command bytes + for (my $i=0;$i<8;$i++){ + my $newbit = (ord($dbyte) >> $i) & 1; + my $newchar = 133 | ($newbit << 4); + my $newchar2 = 132 | ($newbit << 4) | ($newbit << 1) | $newbit; + #-- last command byte still different + if( $i == 7){ + $newchar = $newchar | 2; + } + $cmd = $cmd.chr($newchar); + $ret = $ret.chr($newchar2); + } + #-- write 1-Wire bus + my $res = OWX_Query($hash,$cmd); + #-- process result + if( $res eq $ret ){ + Log 5, "OWX: WriteBytePower OK"; + return 1; + } else { + Log 3, "OWX: WriteBytePower failure"; + return 0; + } +} + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a DS9097 bus interface +# +######################################################################################## +# +# OWX_Block_9097 - Send data block ( +# +# Parameter hash = hash of bus master, data = string to send +# +# Return response, if OK +# 0 if not OK +# +######################################################################################## + +sub OWX_Block_9097 ($$) { + my ($hash,$data) =@_; + + my $data2=""; + my $res=0; + for (my $i=0; $i{DeviceName}; + + $owx_serport->baudrate($owx_baud); + $owx_serport->write_settings; + + if( $owx_debug > 1){ + my $res = "OWX: Sending out "; + for($i=0;$iwrite($cmd); + + Log 1, "OWX: Write incomplete $count_out ne ".(length($cmd))."" if ( $count_out != length($cmd) ); + #-- sleeping for some time + select(undef,undef,undef,0.01); + + #-- read the data + my ($count_in, $string_in) = $owx_serport->read(48); + + if( $owx_debug > 1){ + my $res = "OWX: Receiving "; + for($i=0;$i<$count_in;$i++){ + $j=int(ord(substr($string_in,$i,1))/16); + $k=ord(substr($string_in,$i,1))%16; + $res.=sprintf "0x%1x%1x ",$j,$k; + } + Log 3, $res; + } + + #-- sleeping for some time + select(undef,undef,undef,0.01); + + #$owx_serport->close(); + return($string_in); +} + +######################################################################################## +# +# OWX_ReadBit_9097 - Read 1 bit from 1-wire bus (Fig. 5/6 from Maxim AN214) +# +# Parameter hash = hash of bus master +# +# Return bit value +# +######################################################################################## + +sub OWX_ReadBit_9097 ($) { + my ($hash) = @_; + + #-- set baud rate to 115200 and query!!! + my $sp1="\xFF"; + $owx_baud=115200; + my $res=OWX_Query_9097($hash,$sp1); + $owx_baud=9600; + #-- process result + if( substr($res,0,1) eq "\xFF" ){ + return 1; + } else { + return 0; + } +} + +######################################################################################## +# +# OWX_Reset_9097 - Reset the 1-Wire bus (Fig. 4 of Maxim AN192) +# +# Parameter hash = hash of bus master +# +# Return 1 : OK +# 0 : not OK +# +######################################################################################## + +sub OWX_Reset_9097 ($) { + + my ($hash)=@_; + my $cmd=""; + + #-- Reset command \xF0 + $cmd="\xF0"; + #-- write 1-Wire bus + my $res =OWX_Query_9097($hash,$cmd); + #-- TODO: process result + #-- may vary between 0x10, 0x90, 0xe0 + return 1; +} + +######################################################################################## +# +# OWX_Search_9097 - Perform the 1-Wire Search Algorithm on the 1-Wire bus using the existing +# search state. +# +# Parameter hash = hash of bus master, mode=alarm,discover or verify +# +# Return 1 : device found, ROM number in owx_ROM_ID and pushed to list (LastDeviceFlag=0) +# or only in owx_ROM_ID (LastDeviceFlag=1) +# 0 : device not found, or ot searched at all +# +######################################################################################## + +sub OWX_Search_9097 ($$) { + + my ($hash,$mode)=@_; + + my ($sp1,$sp2,$response,$search_direction,$id_bit_number); + + #-- Response search data parsing operates bitwise + $id_bit_number = 1; + my $rom_byte_number = 0; + my $rom_byte_mask = 1; + my $last_zero = 0; + + #-- issue search command + $owx_baud=115200; + $sp2="\x00\x00\x00\x00\xFF\xFF\xFF\xFF"; + $response = OWX_Query_9097($hash,$sp2); + $owx_baud=9600; + #-- issue the normal search command \xF0 or the alarm search command \xEC + #if( $mode ne "alarm" ){ + # $sp1 = 0xF0; + #} else { + # $sp1 = 0xEC; + #} + + #$response = OWX_TouchByte($hash,$sp1); + + #-- clear 8 byte of device id for current search + @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + + while ( $id_bit_number <= 64) { + #loop until through all ROM bytes 0-7 + my $id_bit = OWX_TouchBit_9097($hash,1); + my $cmp_id_bit = OWX_TouchBit_9097($hash,1); + + #print "id_bit = $id_bit, cmp_id_bit = $cmp_id_bit\n"; + + if( ($id_bit == 1) && ($cmp_id_bit == 1) ){ + #print "no devices present at id_bit_number=$id_bit_number \n"; + next; + } + if ( $id_bit != $cmp_id_bit ){ + $search_direction = $id_bit; + } else { + # hä ? if this discrepancy if before the Last Discrepancy + # on a previous next then pick the same as last time + if ( $id_bit_number < $owx_LastDiscrepancy ){ + if (($owx_ROM_ID[$rom_byte_number] & $rom_byte_mask) > 0){ + $search_direction = 1; + } else { + $search_direction = 0; + } + } else { + # if equal to last pick 1, if not then pick 0 + if ($id_bit_number == $owx_LastDiscrepancy){ + $search_direction = 1; + } else { + $search_direction = 0; + } + } + # if 0 was picked then record its position in LastZero + if ($search_direction == 0){ + $last_zero = $id_bit_number; + # check for Last discrepancy in family + if ($last_zero < 9) { + $owx_LastFamilyDiscrepancy = $last_zero; + } + } + } + # print "search_direction = $search_direction, last_zero=$last_zero\n"; + # set or clear the bit in the ROM byte rom_byte_number + # with mask rom_byte_mask + #print "ROM byte mask = $rom_byte_mask, search_direction = $search_direction\n"; + if ( $search_direction == 1){ + $owx_ROM_ID[$rom_byte_number] |= $rom_byte_mask; + } else { + $owx_ROM_ID[$rom_byte_number] &= ~$rom_byte_mask; + } + # serial number search direction write bit + $response = OWX_WriteBit_9097($hash,$search_direction); + # increment the byte counter id_bit_number + # and shift the mask rom_byte_mask + $id_bit_number++; + $rom_byte_mask <<= 1; + #-- if the mask is 0 then go to new rom_byte_number and + if ($rom_byte_mask == 256){ + $rom_byte_number++; + $rom_byte_mask = 1; + } + $owx_LastDiscrepancy = $last_zero; + } + return 1; +} + +######################################################################################## +# +# OWX_TouchBit_9097 - Write/Read 1 bit from 1-wire bus (Fig. 5-8 from Maxim AN 214) +# +# Parameter hash = hash of bus master +# +# Return bit value +# +######################################################################################## + +sub OWX_TouchBit_9097 ($$) { + my ($hash,$bit) = @_; + + my $sp1; + #-- set baud rate to 115200 and query!!! + if( $bit == 1 ){ + $sp1="\xFF"; + } else { + $sp1="\x00"; + } + $owx_baud=115200; + my $res=OWX_Query_9097($hash,$sp1); + $owx_baud=9600; + #-- process result + my $sp2=substr($res,0,1); + if( $sp1 eq $sp2 ){ + return 1; + }else { + return 0; + } +} + +######################################################################################## +# +# OWX_TouchByte_9097 - Write/Read 8 bit from 1-wire bus +# +# Parameter hash = hash of bus master +# +# Return bit value +# +######################################################################################## + +sub OWX_TouchByte_9097 ($$) { + my ($hash,$byte) = @_; + + my $loop; + my $result=0; + my $bytein=$byte; + + for( $loop=0; $loop < 8; $loop++ ){ + #-- shift result to get ready for the next bit + $result >>=1; + #-- if sending a 1 then read a bit else write 0 + if( $byte & 0x01 ){ + if( OWX_ReadBit_9097($hash) ){ + $result |= 0x80; + } + } else { + OWX_WriteBit_9097($hash,0); + } + $byte >>= 1; + } + #print "-----------------------\n"; + #printf "Sending byte /%02x/\n",$bytein; + #printf "Receiving byte /%02x/\n",$result; + #print "-----------------------\n"; + return $result; +} + +######################################################################################## +# +# OWX_WriteBit_9097 - Write 1 bit to 1-wire bus (Fig. 7/8 from Maxim AN 214) +# +# Parameter hash = hash of bus master +# +# Return bit value +# +######################################################################################## + +sub OWX_WriteBit_9097 ($$) { + my ($hash,$bit) = @_; + + my $sp1; + #-- set baud rate to 115200 and query!!! + if( $bit ==1 ){ + $sp1="\xFF"; + } else { + $sp1="\x00"; + } + $owx_baud=115200; + my $res=OWX_Query_9097($hash,$sp1); + $owx_baud=9600; + #-- process result + if( substr($res,0,1) eq $sp1 ){ + return 1; + } else { + return 0; + } +} + +1; diff --git a/contrib/contrib/1-Wire/21_OWAD.pm b/contrib/contrib/1-Wire/21_OWAD.pm new file mode 100644 index 000000000..bb77b8034 --- /dev/null +++ b/contrib/contrib/1-Wire/21_OWAD.pm @@ -0,0 +1,1033 @@ +######################################################################################## +# +# OWAD.pm +# +# FHEM module to commmunicate with 1-Wire A/D converters DS2450 +# +# Attention: This module may communicate with the OWX module, +# but currently not with the 1-Wire File System OWFS +# +# Prefixes for subroutines of this module: +# OW = General 1-Wire routines Peter Henning) +# OWX = 1-Wire bus master interface (Peter Henning) +# OWFS = 1-Wire file system (??) +# +# Prof. Dr. Peter A. Henning, 2012 +# +# Version 1.11 - March, 2012 +# +# Setup bus device in fhem.cfg as +# +# define OWAD [] [interval] +# +# where may be replaced by any name string +# +# is a 1-Wire device type. If omitted, we assume this to be an +# DS2450 A/D converter +# is a 12 character (6 byte) 1-Wire ROM ID +# without Family ID, e.g. A2D90D000800 +# [interval] is an optional query interval in seconds +# +# get id => FAM_ID.ROM_ID.CRC +# get present => 1 if device present, 0 if not +# get interval => query interval +# get reading => measurement for all channels +# get alarm => alarm measurement settings for all channels +# get status => alarm and i/o status for all channels +# +# set interval => set period for measurement +# +# Additional attributes are defined in fhem.cfg, in some cases per channel, where =A,B,C,D +# Note: attributes are read only during initialization procedure - later changes are not used. +# +# attr stateAL0 "" = character string for denoting low normal condition, default is green down triangle +# attr stateAH0 "" = character string for denoting high normal condition, default is green up triangle +# attr stateAL1 "" = character string for denoting low alarm condition, default is red down triangle +# attr stateAH1 "" = character string for denoting high alarm condition, default is red up triangle +# attr Name | = name for the channel | a type description for the measured value +# attr Unit | = unit of measurement for this channel | its abbreviation +# attr Offset = offset added to the reading in this channel +# attr Factor = factor multiplied to (reading+offset) in this channel +# attr Alarm = alarm setting in this channel, either both, low, high or none (default) +# attr Low = measurement value for low alarm +# attr High = measurement for high alarm +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +#-- Prototypes to make komodo happy +use vars qw{%attr %defs}; +use strict; +use warnings; +sub Log($$); + +#-- value globals +my @owg_status; +my $owg_state; +#-- channel name - fixed is the first array, variable the second +my @owg_fixed = ("A","B","C","D"); +my @owg_channel; +#-- channel values - always the raw values from the device +my @owg_val; +#-- channel mode - fixed for now +my @owg_mode = ("input","input","input","input"); +#-- resolution in bit - fixed for now +my @owg_resoln = (16,16,16,16); +#-- raw range in mV - fixed for now +my @owg_range = (5100,5100,5100,5100); +#-- alarm status 0 = disabled, 1 = enabled, but not alarmed, 2 = alarmed +my @owg_slow; +my @owg_shigh; +#-- alarm values - always the raw values committed to the device +my @owg_vlow; +my @owg_vhigh; +#-- variables for display strings +my ($stateal1,$stateah1,$stateal0,$stateah0); + +my %gets = ( + "id" => "", + "present" => "", + "interval" => "", + "reading" => "", + "alarm" => "", + "status" => "", +); + +my %sets = ( + "interval" => "" +); + +my %updates = ( + "present" => "", + "reading" => "", + "alarm" => "", + "status" => "" +); + + +######################################################################################## +# +# The following subroutines are independent of the bus interface +# +# Prefix = OWAD +# +######################################################################################## +# +# OWAD_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWAD_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "OWAD_Define"; + $hash->{UndefFn} = "OWAD_Undef"; + $hash->{GetFn} = "OWAD_Get"; + $hash->{SetFn} = "OWAD_Set"; + #Name = channel name + #Offset = a v(oltage) offset added to the reading + #Factor = a v(oltage) factor multiplied with (reading+offset) + #Unit = a unit of measure + my $attlist = "IODev do_not_notify:0,1 showtime:0,1 model:DS2450 loglevel:0,1,2,3,4,5 ". + "stateAL0 stateAL1 stateAH0 stateAH1 "; + + for( my $i=0;$i<4;$i++ ){ + $attlist .= " ".$owg_fixed[$i]."Name"; + $attlist .= " ".$owg_fixed[$i]."Offset"; + $attlist .= " ".$owg_fixed[$i]."Factor"; + $attlist .= " ".$owg_fixed[$i]."Unit"; + $attlist .= " ".$owg_fixed[$i]."Alarm"; + $attlist .= " ".$owg_fixed[$i]."Low"; + $attlist .= " ".$owg_fixed[$i]."High"; + } + $hash->{AttrList} = $attlist; +} + +######################################################################################### +# +# OWAD_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################### + +sub OWAD_Define ($$) { + my ($hash, $def) = @_; + + # define OWAD [] [interval] + # e.g.: define flow OWAD 525715020000 300 + my @a = split("[ \t][ \t]*", $def); + + my ($name,$model,$fam,$id,$crc,$interval,$scale,$ret); + + #-- default + $name = $a[0]; + $interval = 300; + $scale = ""; + $ret = ""; + + #-- check syntax + return "OWAD: Wrong syntax, must be define OWAD [] [interval]" + if(int(@a) < 2 || int(@a) > 5); + + #-- check if this is an old style definition, e.g. is missing + my $a2 = $a[2]; + my $a3 = defined($a[3]) ? $a[3] : ""; + if( $a2 =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $model = "DS2450"; + $id = $a[2]; + if(int(@a)>=4) { $interval = $a[3]; } + } elsif( $a3 =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $model = $a[2]; + return "OWAD: Wrong 1-Wire device model $model" + if( $model ne "DS2450"); + $id = $a[3]; + } else { + return "OWAD: $a[0] ID $a[2] invalid, specify a 12 digit value"; + } + + #-- 1-Wire ROM identifier in the form "FF.XXXXXXXXXXXX.YY" + # determine CRC Code - only if this is a direct interface + $crc = defined($hash->{IODev}->{INTERFACE}) ? sprintf("%02x",OWX_CRC("20.".$id."00")) : "00"; + + #-- Define device internals + $hash->{ROM_ID} = "20.".$id.$crc; + $hash->{OW_ID} = $id; + $hash->{OW_FAMILY} = "20"; + $hash->{PRESENT} = 0; + $hash->{INTERVAL} = $interval; + + #-- Couple to I/O device + AssignIoPort($hash); + Log 3, "OWAD: Warning, no 1-Wire I/O device found for $name." + if(!defined($hash->{IODev}->{NAME})); + $modules{OWAD}{defptr}{$id} = $hash; + $hash->{STATE} = "Defined"; + Log 3, "OWAD: Device $name defined."; + + #-- Initialization reading according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- Start timer for initialization in a few seconds + InternalTimer(time()+1, "OWAD_InitializeDevice", $hash, 0); + + #-- Start timer for updates + InternalTimer(time()+$hash->{INTERVAL}, "OWAD_GetValues", $hash, 0); + + return undef; +} + +######################################################################################## +# +# OWAD_InitializeDevice - delayed setting of initial readings and channel names +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWAD_InitializeDevice($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + $stateal1 = defined($attr{$name}{stateAL1}) ? $attr{$name}{stateAL1} : ""; + $stateah1 = defined($attr{$name}{stateAH1}) ? $attr{$name}{stateAH1} : ""; + $stateal0 = defined($attr{$name}{stateAL0}) ? $attr{$name}{stateAL0} : ""; + $stateah0 = defined($attr{$name}{stateAH0}) ? $attr{$name}{stateAH0} : ""; + + #-- Initial readings + @owg_val = (0.0,0.0,0.0,0.0); + @owg_slow = (0,0,0,0); + @owg_shigh = (0,0,0,0); + + #-- Set channel names, channel units and alarm values + for( my $i=0;$i<4;$i++) { + #-- name + my $cname = defined($attr{$name}{$owg_fixed[$i]."Name"}) ? $attr{$name}{$owg_fixed[$i]."Name"} : $owg_fixed[$i]."|voltage"; + my @cnama = split(/\|/,$cname); + Log 1, "OWAD: InitializeDevice with insufficient name specification $cname" + if( int(@cnama)!=2 ); + $owg_channel[$i] = $cnama[0]; + #-- unit + my $unit = defined($attr{$name}{$owg_fixed[$i]."Unit"}) ? $attr{$name}{$owg_fixed[$i]."Unit"} : "Volt|V"; + my @unarr= split(/\|/,$unit); + Log 1, "OWAD: InitializeDevice with insufficient unit specification $unit" + if( int(@unarr)!=2 ); + #-- offset and scale factor + my $offset = defined($attr{$name}{$owg_fixed[$i]."Offset"}) ? $attr{$name}{$owg_fixed[$i]."Offset"} : 0.0; + my $factor = defined($attr{$name}{$owg_fixed[$i]."Factor"}) ? $attr{$name}{$owg_fixed[$i]."Factor"} : 1.0; + #-- put into readings + $hash->{READINGS}{"$owg_channel[$i]"}{TYPE} = defined($cnama[1]) ? $cnama[1] : "unknown"; + $hash->{READINGS}{"$owg_channel[$i]"}{UNIT} = $unarr[0]; + $hash->{READINGS}{"$owg_channel[$i]"}{UNITABBR} = defined($unarr[1]) ? $unarr[1] : "?"; + $hash->{READINGS}{"$owg_channel[$i]"}{OFFSET} = $offset; + $hash->{READINGS}{"$owg_channel[$i]"}{FACTOR} = $factor; + + #-- alarm + my $alarm = defined($attr{$name}{$owg_fixed[$i]."Alarm"}) ? $attr{$name}{$owg_fixed[$i]."Alarm"} : "none"; + my $vlow = defined($attr{$name}{$owg_fixed[$i]."Low"}) ? $attr{$name}{$owg_fixed[$i]."Low"} : 0.0; + my $vhigh = defined($attr{$name}{$owg_fixed[$i]."High"}) ? $attr{$name}{$owg_fixed[$i]."High"} : 5.0; + if( $alarm eq "low" || $alarm eq "both" ){ + $owg_slow[$i]=1; + } + if( $alarm eq "high" || $alarm eq "both" ){ + $owg_shigh[$i]=1; + }; + $owg_vlow[$i] = ($vlow/$factor - $offset); + $owg_vhigh[$i] = ($vhigh/$factor - $offset); + } + + #-- set status according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- OWX interface + if( !defined($interface) ){ + return "OWAD: Interface missing"; + } elsif( $interface eq "OWX" ){ + OWXAD_SetPage($hash,"alarm"); + OWXAD_SetPage($hash,"status"); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetPage($hash,"reading"); + #-- Unknown interface + }else{ + return "OWAD: InitializeDevice with wrong IODev type $interface"; + } + + #-- Initialize all the display stuff + OWAD_FormatValues($hash); +} + +######################################################################################## +# +# OWAD_FormatValues - put together various format strings +# +# Parameter hash = hash of device addressed, fs = format string +# +######################################################################################## + +sub OWAD_FormatValues($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + my ($offset,$factor,$vval,$vlow,$vhigh); + my ($value1,$value2,$value3) = ("","",""); + my $galarm = 0; + + my $tn = TimeNow(); + + #-- formats for output + for (my $i=0;$i<4;$i++){ + $offset = $hash->{READINGS}{"$owg_channel[$i]"}{OFFSET}; + $factor = $hash->{READINGS}{"$owg_channel[$i]"}{FACTOR}; + #-- correct values for proper offset, factor + $vval = int(($owg_val[$i] + $offset)*$factor*1000)/1000;; + #-- put into READINGS + $hash->{READINGS}{"$owg_channel[$i]"}{VAL} = $vval; + $hash->{READINGS}{"$owg_channel[$i]"}{TIME} = $tn; + + #-- correct alarm values for proper offset, factor + $vlow = int(($owg_vlow[$i] + $offset)*$factor*1000)/1000; + $vhigh = int(($owg_vhigh[$i] + $offset)*$factor*1000)/1000; + + #-- put into READINGS + $hash->{READINGS}{$owg_channel[$i]."Low"}{VAL} = $vlow; + $hash->{READINGS}{$owg_channel[$i]."Low"}{TIME} = $tn; + $hash->{READINGS}{$owg_channel[$i]."High"}{VAL} = $vhigh; + $hash->{READINGS}{$owg_channel[$i]."High"}{TIME} = $tn; + + #-- string buildup for return value, STATE and alarm + $value1 .= sprintf( "%s: %5.3f %s", $owg_channel[$i], $vval,$hash->{READINGS}{"$owg_channel[$i]"}{UNITABBR}); + $value2 .= sprintf( "%s: %5.2f %s ", $owg_channel[$i], $vval,$hash->{READINGS}{"$owg_channel[$i]"}{UNITABBR}); + $value3 .= sprintf( "%s: " , $owg_channel[$i]); + + #-- Test for alarm condition + #-- alarm signature low + if( $owg_slow[$i] == 0 ) { + #$value2 .= " "; + $value3 .= "-"; + } else { + if( $vval > $vlow ){ + $owg_slow[$i] = 1; + $value2 .= $stateal0; + $value3 .= $stateal0; + } else { + $galarm = 1; + $owg_slow[$i] = 2; + $value2 .= $stateal1; + $value3 .= $stateal1; + } + } + #-- alarm signature high + if( $owg_shigh[$i] == 0 ) { + #$value2 .= " "; + $value3 .= "-"; + } else { + if( $vval < $vhigh ){ + $owg_shigh[$i] = 1; + $value2 .= $stateah0; + $value3 .= $stateah0; + } else { + $galarm = 1; + $owg_shigh[$i] = 2; + $value2 .= $stateah1; + $value3 .= $stateah1; + } + } + + #-- insert comma + if( $i<3 ){ + $value1 .= " "; + $value2 .= ", "; + $value3 .= ", "; + } + } + #-- STATE + $hash->{STATE} = $value2; + #-- alarm + $hash->{ALARM} = $galarm; + $hash->{READINGS}{alarms}{VAL} = $value3; + $hash->{READINGS}{alarms}{TIME} = $tn; + return $value1; +} + +######################################################################################## +# +# OWAD_Get - Implements GetFn function +# +# Parameter hash = hash of device addressed, a = argument array +# +######################################################################################## + +sub OWAD_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my ($value,$value2,$value3) = (undef,undef,undef); + my $ret = ""; + my $offset; + my $factor; + + #-- check syntax + return "OWAD: Get argument is missing @a" + if(int(@a) != 2); + + #-- check argument + return "OWAD: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + #-- get id + if($a[1] eq "id") { + $value = $hash->{ROM_ID}; + return "$a[0] $reading => $value"; + } + + #-- get present + if($a[1] eq "present") { + #-- hash of the busmaster + my $master = $hash->{IODev}; + $value = OWX_Verify($master,$hash->{ROM_ID}); + $hash->{PRESENT} = $value; + return "$a[0] $reading => $value"; + } + + #-- get interval + if($a[1] eq "interval") { + $value = $hash->{INTERVAL}; + return "$a[0] $reading => $value"; + } + + #-- reset presence + $hash->{PRESENT} = 0; + + #-- get reading according to interface type + my $interface= $hash->{IODev}->{TYPE}; + if($a[1] eq "reading") { + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXAD_GetPage($hash,"reading"); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetPage($hash,"reading"); + #-- Unknown interface + }else{ + return "OWAD: Get with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWAD: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + return "OWAD: $name.$reading => ".OWAD_FormatValues($hash); + } + + #-- get alarm values according to interface type + if($a[1] eq "alarm") { + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXAD_GetPage($hash,"alarm"); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetPage($hash,"alarm"); + #-- Unknown interface + }else{ + return "OWAD: Get with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWAD: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + OWAD_FormatValues($hash); + + #-- output string looks differently here + $value = ""; + for (my $i=0;$i<4;$i++){ + $value .= sprintf "%s:[%4.2f,%4.2f] ",$owg_channel[$i], + $hash->{READINGS}{$owg_channel[$i]."Low"}{VAL}, + $hash->{READINGS}{$owg_channel[$i]."High"}{VAL}; + } + return "OWAD: $name.$reading => $value"; + } + + #-- get status values according to interface type + if($a[1] eq "status") { + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXAD_GetPage($hash,"status"); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetPage($hash,"status"); + #-- Unknown interface + }else{ + return "OWAD: Get with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWAD: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + OWAD_FormatValues($hash); + return "OWAD: $name.$reading => ".$hash->{READINGS}{alarms}{VAL}; + } +} + +####################################################################################### +# +# OWAD_GetValues - Updates the reading from one device +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWAD_GetValues($) { + my $hash = shift; + + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my $value = ""; + my $ret = ""; + my $offset; + my $factor; + + #-- define warnings + my $warn = "none"; + $hash->{ALARM} = "0"; + + #-- restart timer for updates + RemoveInternalTimer($hash); + InternalTimer(time()+$hash->{INTERVAL}, "OWAD_GetValues", $hash, 1); + + #-- reset presence + $hash->{PRESENT} = 0; + + #-- Get readings, alarms and stati according to interface type + my $interface= $hash->{IODev}->{TYPE}; + if( $interface eq "OWX" ){ + $ret = OWXAD_GetPage($hash,"reading"); + $ret = OWXAD_GetPage($hash,"alarm"); + $ret = OWXAD_GetPage($hash,"status"); + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetValues($hash); + }else{ + return "OWAD: GetValues with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWAD: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + $value=OWAD_FormatValues($hash); + #--logging + Log 5, $value; + $hash->{CHANGED}[0] = $value; + + DoTrigger($name, undef); + + return undef; +} + +####################################################################################### +# +# OWAD_Set - Set one value for device +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## + +sub OWAD_Set($@) { + my ($hash, @a) = @_; + + my $key = $a[1]; + my $value = $a[2]; + + #-- for the selector: which values are possible + if (@a == 2){ + my $newkeys = join(" ", sort keys %sets); + for( my $i=0;$i<4;$i++ ){ + $newkeys .= " ".$owg_channel[$i]."Alarm"; + $newkeys .= " ".$owg_channel[$i]."Low"; + $newkeys .= " ".$owg_channel[$i]."High"; + } + return $newkeys ; + } + + #-- check syntax + return "OWAD: Set needs one parameter when setting this value" + if( int(@a)!=3 ); + + #-- check argument + if( !defined($sets{$a[1]}) && !($key =~ m/.*(Alarm|Low|High)/) ){ + return "OWAD: Set with unknown argument $a[1]"; + } + + #-- define vars + my $ret = undef; + my $channel = undef; + my $channo = undef; + my $factor; + my $offset; + my $condx; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + + #-- set new timer interval + if($key eq "interval") { + # check value + return "OWAD: Set with short interval, must be > 1" + if(int($value) < 1); + # update timer + $hash->{INTERVAL} = $value; + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "OWAD_GetValues", $hash, 1); + return undef; + } + + #-- find out which channel we have + my $tc =$key; + if( $tc =~ s/(.*)(Alarm|Low|High)/$channel=$1/se ) { + for (my $i=0;$i<4;$i++){ + if( $tc eq $owg_channel[$i] ){ + $channo = $i; + $channel = $tc; + last; + } + } + } + return "OWAD: Cannot determine channel from parameter $a[1]" + if( !(defined($channo))); + + #-- set these values depending on interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- check alarm values + if( $key =~ m/(.*)(Alarm)/ ) { + return "OWAD: Set with wrong value $value for $key, allowed is none/low/high/both" + if($value ne "none" && $value ne "low" && $value ne "high" && $value ne "both"); + if( $value eq "low" || $value eq "both" ){ + $owg_slow[$channo]=1; + } else{ + $owg_slow[$channo]=0; + } + if( $value eq "high" || $value eq "both" ){ + $owg_shigh[$channo]=1; + } else{ + $owg_shigh[$channo]=0; + } + + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXAD_SetPage($hash,"status"); + return $ret + if(defined($ret)); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_SetValues($hash,@a); + # return $ret + # if(defined($ret)); + } else { + return "OWAD: Set with wrong IODev type $interface"; + } + }elsif( $key =~ m/(.*)(Low|High)/ ) { + $offset = $attr{$name}{$owg_fixed[$channo]."Offset"}; + $factor = $attr{$name}{$owg_fixed[$channo]."Factor"}; + + #-- find upper and lower boundaries for given offset/factor + my $mmin = 0.0; + + $mmin += $offset if ( $offset ); + $mmin *= $factor if ( $factor ); + + my $mmax = $owg_range[$channo]/1000; + $mmax += $offset if ( $offset ); + $mmax *= $factor if ( $factor ); + + return sprintf("OWAD: Set with wrong value $value for $key, range is [%3.1f,%3.1f]",$mmin,$mmax) + if($value < $mmin || $value > $mmax); + + $value /= $factor if ( $factor ); + $value -= $offset if ( $offset ); + #-- round to those numbers understood by the device + my $value2 = int($value*255000/$owg_range[$channo])*$owg_range[$channo]/255000; + + #-- set alarm value in the device + if( $key =~ m/(.*)Low/ ){ + $owg_vlow[$channo] = $value2; + } elsif( $key =~ m/(.*)High/ ){ + $owg_vhigh[$channo] = $value2; + } + + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXAD_SetPage($hash,"alarm"); + return $ret + if(defined($ret)); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_SetValues($hash,@a); + # return $ret + # if(defined($ret)); + } else { + return "OWAD: Set with wrong IODev type $interface"; + } + } + #-- process results + + $hash->{PRESENT} = 1; + OWAD_FormatValues($hash); + + Log 4, "OWAD: Set $hash->{NAME} $key $value"; + return undef; +} + +######################################################################################## +# +# OWAD_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWAD_Undef ($) { + my ($hash) = @_; + delete($modules{OWAD}{defptr}{$hash->{OW_ID}}); + RemoveInternalTimer($hash); + return undef; +} + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a 1-Wire bus connected +# via OWFS +# +# Prefix = OWFSAD +# +######################################################################################## + + + + + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a 1-Wire bus connected +# directly to the FHEM server +# +# Prefix = OWXAD +# +######################################################################################## +# +# OWXAD_GetPage - Get one memory page from device +# +# Parameter hash = hash of device addressed +# page = "reading", "alarm" or "status" +# +######################################################################################## + +sub OWXAD_GetPage($$) { + + my ($hash,$page) = @_; + + #-- For now, switch on conversion command + my $con=1; + + my ($select, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #=============== get the voltage reading =============================== + if( $page eq "reading"){ + #-- if the conversion has not been called before + if( $con==1 ){ + OWX_Reset($master); + #-- issue the match ROM command \x55 and the start conversion command + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x3C\x0F\x00\xFF\xFF",@owx_ROM_ID); + $res= OWX_Block($master,$select); + if( $res eq 0 ){ + return "OWXAD: Device $owx_dev not accessible for conversion"; + } + #-- conversion needs some 5 ms per channel + select(undef,undef,undef,0.02); + } + #-- issue the match ROM command \x55 and the read conversion page command + # \xAA\x00\x00 reading 8 data bytes and 2 CRC bytes + $select=sprintf("\x55%c%c%c%c%c%c%c%c\xAA\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + @owx_ROM_ID); + #=============== get the alarm reading =============================== + } elsif ( $page eq "alarm" ) { + #-- issue the match ROM command \x55 and the read alarm page command + # \xAA\x10\x00 reading 8 data bytes and 2 CRC bytes + $select=sprintf("\x55%c%c%c%c%c%c%c%c\xAA\x10\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + @owx_ROM_ID); + #=============== get the status reading =============================== + } elsif ( $page eq "status" ) { + #-- issue the match ROM command \x55 and the read status memory page command + # \xAA\x08\x00 reading 8 data bytes and 2 CRC bytes + $select=sprintf("\x55%c%c%c%c%c%c%c%c\xAA\x08\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + @owx_ROM_ID); + #=============== wrong value requested =============================== + } else { + return "OWXAD: Wrong memory page requested"; + } + + #-- reset the bus + OWX_Reset($master); + #-- read the data + $res=OWX_Block($master,$select); + if( $res eq 0 ){ + return "OWXAD: Device $owx_dev not accessible in reading $page page"; + } + + #-- process results + @data=split(//,$res); + if ( (@data != 22) ){ + return "OWXAD: Device $owx_dev returns invalid data"; + } + + #=============== get the voltage reading =============================== + if( $page eq "reading"){ + for( $i=0;$i<4;$i++){ + $owg_val[$i]= int((ord($data[12+2*$i])+256*ord($data[13+2*$i]))/((1<<$owg_resoln[$i])-1) * $owg_range[$i])/1000; + } + #=============== get the alarm reading =============================== + } elsif ( $page eq "alarm" ) { + for( $i=0;$i<4;$i++){ + $owg_vlow[$i] = int(ord($data[12+2*$i])/255 * $owg_range[$i])/1000; + $owg_vhigh[$i] = int(ord($data[13+2*$i])/255 * $owg_range[$i])/1000; + } + #=============== get the status reading =============================== + } elsif ( $page eq "status" ) { + my ($sb1,$sb2); + for( $i=0;$i<4;$i++){ + $sb1 = ord($data[12+2*$i]); + $sb2 = ord($data[12+2*$i+1]); + + #-- normal operation + if( $sb1 && 128) { + #-- put into globals + $owg_mode[$i] = "input"; + $owg_resoln[$i] = ($sb1 & 15) + 1 ; + $owg_range[$i] = ($sb2 & 1) ? 5100 : 2550; + #-- low alarm disabled + if( ($sb2 & 4)==0 ){ + $owg_slow[$i] = 0; + }else { + #-- low alarm enabled and not set + if ( ($sb2 & 16)==0 ){ + $owg_slow[$i] = 1; + #-- low alarm enabled and set + }else{ + $owg_slow[$i] = 2; + } + } + #-- high alarm disabled + if( ($sb2 & 8)==0 ){ + $owg_shigh[$i] = 0; + }else { + #-- high alarm enabled and not set + if ( ($sb2 & 32)==0 ){ + $owg_shigh[$i] = 1; + #-- high alarm enabled and set + }else{ + $owg_shigh[$i] = 2; + } + } + + #-- assemble status string + $owg_status[$i] = $owg_mode[$i].", "; + $owg_status[$i] .= "disabled ," + if ( !($sb2 && 128) ); + $owg_status[$i] .= sprintf "raw range %3.1f V, ",$owg_range[$i]/1000; + $owg_status[$i] .= sprintf "resolution %d bit, ",$owg_resoln[$i]; + $owg_status[$i] .= sprintf "low alarm disabled, " + if( $owg_slow[$i]==0 ); + $owg_status[$i] .= sprintf "low alarm enabled, " + if( $owg_slow[$i]==1 ); + $owg_status[$i] .= sprintf "alarmed low, " + if( $owg_slow[$i]==2 ); + $owg_status[$i] .= sprintf "high alarm disabled" + if( $owg_shigh[$i]==0 ); + $owg_status[$i] .= sprintf "high alarm enabled" + if( $owg_shigh[$i]==1 ); + $owg_status[$i] .= sprintf "alarmed high" + if( $owg_shigh[$i]==2 ); + + } else { + $owg_mode[$i] = "output"; + #-- assemble status string + $owg_status[$i] = $owg_mode[$i].", "; + $owg_status[$i] .= ($sb1 & 64 ) ? "ON" : "OFF"; + } + } + } + return undef +} + +######################################################################################## +# +# OWXAD_SetPage - Set one page of device +# +# Parameter hash = hash of device addressed +# page = "alarm" or "status" +# +######################################################################################## + +sub OWXAD_SetPage($$) { + + my ($hash,$page) = @_; + + my ($select, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #=============== set the alarm values =============================== + if ( $page eq "test" ) { + #-- issue the match ROM command \x55 and the set alarm page command + # \x55\x10\x00 reading 8 data bytes and 2 CRC bytes + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x55\x10\x00", + @owx_ROM_ID); + for( $i=0;$i<4;$i++){ + $select .= sprintf "%c\xFF\xFF\xFF",int($owg_vlow[$i]*255000/$owg_range[$i]); + $select .= sprintf "%c\xFF\xFF\xFF",int($owg_vhigh[$i]*255000/$owg_range[$i]); + } + #=============== set the status =============================== + } elsif ( $page eq "status" ) { + my ($sb1,$sb2); + #-- issue the match ROM command \x55 and the set status memory page command + # \x55\x08\x00 reading 8 data bytes and 2 CRC bytes + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x55\x08\x00", + @owx_ROM_ID); + for( $i=0;$i<4;$i++){ + if( $owg_mode[$i] eq "input" ){ + #-- resolution (TODO: check !) + $sb1 = $owg_resoln[$i]-1; + #-- alarm enabled + $sb2 = ( $owg_slow[$i] > 0 ) ? 4 : 0; + $sb2 += ( $owg_shigh[$i] > 0 ) ? 8 : 0; + #-- range + $sb2 |= 1 + if( $owg_range[$i] > 2550 ); + } else { + $sb1 = 128; + $sb2 = 0; + } + $select .= sprintf "%c\xFF\xFF\xFF",$sb1; + $select .= sprintf "%c\xFF\xFF\xFF",$sb2; + } + #=============== wrong page write attempt =============================== + } else { + return "OWXAD: Wrong memory page write attempt"; + } + + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- process results + if( $res eq 0 ){ + return "OWXAD: Device $owx_dev not accessible for writing"; + } + + return undef; +} + +1; diff --git a/contrib/contrib/1-Wire/21_OWCOUNT.pm b/contrib/contrib/1-Wire/21_OWCOUNT.pm new file mode 100644 index 000000000..af2ff6b25 --- /dev/null +++ b/contrib/contrib/1-Wire/21_OWCOUNT.pm @@ -0,0 +1,706 @@ +######################################################################################## +# +# OWCOUNT.pm BETA Version +# +# FHEM module to commmunicate with 1-Wire Counter/RAM DS2423 +# +# Attention: This module may communicate with the OWX module, +# but currently not with the 1-Wire File System OWFS +# +# +# SO FAR ONLY external counter inputs A,B are available ! Neither memory content, nor internal counters are questioned. +# +# +# Prefixes for subroutines of this module: +# OW = General 1-Wire routines Peter Henning) +# OWX = 1-Wire bus master interface (Peter Henning) +# OWFS = 1-Wire file system (??) +# +# Prof. Dr. Peter A. Henning, 2012 +# +# Version 1.11 - March, 2012 +# +# Setup bus device in fhem.cfg as +# +# define OWCOUNT [] [interval] +# +# where may be replaced by any name string +# +# is a 1-Wire device type. If omitted, we assume this to be an +# DS2423 Counter/RAM +# is a 12 character (6 byte) 1-Wire ROM ID +# without Family ID, e.g. A2D90D000800 +# [interval] is an optional query interval in seconds +# +# get id => FAM_ID.ROM_ID.CRC +# get present => 1 if device present, 0 if not +# get interval => query interval +# get counter A,B => value for counter +# +# set interval => set period for measurement +# +# Additional attributes are defined in fhem.cfg, in some cases per channel, where =A,B +# Note: attributes are read only during initialization procedure - later changes are not used. +# +# attr Name | = name for the channel | a type description for the measured value +# attr Unit | = unit of measurement for this channel | its abbreviation +# attr Offset = offset added to the reading in this channel +# attr Factor = factor multiplied to (reading+offset) in this channel +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +#-- Prototypes to make komodo happy +use vars qw{%attr %defs}; +use strict; +use warnings; +sub Log($$); + +#-- channel name - fixed is the first array, variable the second +my @owg_fixed = ("A","B"); +my @owg_channel; +#-- channel values - always the raw values from the device +my @owg_val; + +my %gets = ( + "id" => "", + "present" => "", + "interval" => "", + #"page" => "", + "counter" => "", +); + +my %sets = ( + "interval" => "" + #"page" => "" +); + +my %updates = ( + "present" => "", + "counter" => "" +); + + +######################################################################################## +# +# The following subroutines are independent of the bus interface +# +# Prefix = OWCOUNT +# +######################################################################################## +# +# OWCOUNT_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWCOUNT_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "OWCOUNT_Define"; + $hash->{UndefFn} = "OWCOUNT_Undef"; + $hash->{GetFn} = "OWCOUNT_Get"; + $hash->{SetFn} = "OWCOUNT_Set"; + #Name = channel name + #Offset = an offset added to the reading + #Factor = a factor multiplied with (reading+offset) + #Unit = a unit of measure + my $attlist = "IODev do_not_notify:0,1 showtime:0,1 model:DS2450 loglevel:0,1,2,3,4,5 "; + + for( my $i=0;$i{AttrList} = $attlist; +} + +######################################################################################### +# +# OWCOUNT_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################### + +sub OWCOUNT_Define ($$) { + my ($hash, $def) = @_; + + # define OWCOUNT [] [interval] + # e.g.: define flow OWCOUNT 525715020000 300 + my @a = split("[ \t][ \t]*", $def); + + my ($name,$model,$fam,$id,$crc,$interval,$scale,$ret); + + #-- default + $name = $a[0]; + $interval = 300; + $scale = ""; + $ret = ""; + + #-- check syntax + return "OWCOUNT: Wrong syntax, must be define OWCOUNT [] [interval]" + if(int(@a) < 2 || int(@a) > 5); + + #-- check if this is an old style definition, e.g. is missing + my $a2 = $a[2]; + my $a3 = defined($a[3]) ? $a[3] : ""; + if( $a2 =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $model = "DS2423"; + $id = $a[2]; + if(int(@a)>=4) { $interval = $a[3]; } + } elsif( $a3 =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $model = $a[2]; + return "OWCOUNT: Wrong 1-Wire device model $model" + if( $model ne "DS2423"); + $id = $a[3]; + } else { + return "OWCOUNT: $a[0] ID $a[2] invalid, specify a 12 digit value"; + } + + #-- 1-Wire ROM identifier in the form "FF.XXXXXXXXXXXX.YY" + # determine CRC Code - only if this is a direct interface + $crc = defined($hash->{IODev}->{INTERFACE}) ? sprintf("%02x",OWX_CRC("1D.".$id."00")) : "00"; + + #-- Define device internals + $hash->{ROM_ID} = "1D.".$id.$crc; + $hash->{OW_ID} = $id; + $hash->{OW_FAMILY} = "1D"; + $hash->{PRESENT} = 0; + $hash->{INTERVAL} = $interval; + + #-- Couple to I/O device + AssignIoPort($hash); + Log 3, "OWCOUNT: Warning, no 1-Wire I/O device found for $name." + if(!defined($hash->{IODev}->{NAME})); + $modules{OWCOUNT}{defptr}{$id} = $hash; + $hash->{STATE} = "Defined"; + Log 3, "OWCOUNT: Device $name defined."; + + #-- Initialization reading according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- Start timer for initialization in a few seconds + InternalTimer(time()+1, "OWCOUNT_InitializeDevice", $hash, 0); + + #-- Start timer for updates + InternalTimer(time()+$hash->{INTERVAL}, "OWCOUNT_GetValues", $hash, 0); + + return undef; +} + +######################################################################################## +# +# OWCOUNT_InitializeDevice - delayed setting of initial readings and channel names +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWCOUNT_InitializeDevice($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + #-- Initial readings + @owg_val = (0.0,0.0,0.0,0.0); + + #-- Set channel names, channel units and alarm values + for( my $i=0;$i{READINGS}{"$owg_channel[$i]"}{TYPE} = defined($cnama[1]) ? $cnama[1] : "unknown"; + $hash->{READINGS}{"$owg_channel[$i]"}{UNIT} = $unarr[0]; + $hash->{READINGS}{"$owg_channel[$i]"}{UNITABBR} = defined($unarr[1]) ? $unarr[1] : ""; + $hash->{READINGS}{"$owg_channel[$i]"}{OFFSET} = $offset; + $hash->{READINGS}{"$owg_channel[$i]"}{FACTOR} = $factor; + } + + #-- set status according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- OWX interface + if( !defined($interface) ){ + return "OWCOUNT: Interface missing"; + } elsif( $interface eq "OWX" ){ + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetPage($hash,"reading"); + #-- Unknown interface + }else{ + return "OWCOUNT: InitializeDevice with wrong IODev type $interface"; + } + + #-- Initialize all the display stuff + OWCOUNT_FormatValues($hash); +} + +######################################################################################## +# +# OWCOUNT_FormatValues - put together various format strings +# +# Parameter hash = hash of device addressed, fs = format string +# +######################################################################################## + +sub OWCOUNT_FormatValues($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + my ($offset,$factor,$vval); + my ($value1,$value2,$value3) = ("","",""); + my $galarm = 0; + + my $tn = TimeNow(); + + #-- formats for output + for (my $i=0;$i{READINGS}{"$owg_channel[$i]"}{OFFSET}; + $factor = $hash->{READINGS}{"$owg_channel[$i]"}{FACTOR}; + #-- correct values for proper offset, factor + if( $factor == 1.0 ){ + $vval = ($owg_val[$i] + $offset)*$factor; + } else { + $vval = int(($owg_val[$i] + $offset)*$factor*1000)/1000; + } + #-- put into READINGS + $hash->{READINGS}{"$owg_channel[$i]"}{VAL} = $vval; + $hash->{READINGS}{"$owg_channel[$i]"}{TIME} = $tn; + + #-- string buildup for return value and STATE + $value1 .= sprintf( "%s: %5.3f %s", $owg_channel[$i], $vval,$hash->{READINGS}{"$owg_channel[$i]"}{UNITABBR}); + $value2 .= sprintf( "%s: %5.2f %s ", $owg_channel[$i], $vval,$hash->{READINGS}{"$owg_channel[$i]"}{UNITABBR}); + $value3 .= sprintf( "%s: " , $owg_channel[$i]); + + #-- insert comma + if( $i<3 ){ + $value1 .= " "; + $value2 .= ", "; + $value3 .= ", "; + } + } + #-- STATE + $hash->{STATE} = $value2; + + return $value1; +} + +######################################################################################## +# +# OWCOUNT_Get - Implements GetFn function +# +# Parameter hash = hash of device addressed, a = argument array +# +######################################################################################## + +sub OWCOUNT_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my ($value,$value2,$value3) = (undef,undef,undef); + my $ret = ""; + my $offset; + my $factor; + + #-- check syntax + return "OWCOUNT: Get argument is missing @a" + if(int(@a) < 2); + + #-- check argument + return "OWCOUNT: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + #-- get id + if($a[1] eq "id") { + $value = $hash->{ROM_ID}; + return "$a[0] $reading => $value"; + } + + #-- get present + if($a[1] eq "present") { + #-- hash of the busmaster + my $master = $hash->{IODev}; + $value = OWX_Verify($master,$hash->{ROM_ID}); + $hash->{PRESENT} = $value; + return "$a[0] $reading => $value"; + } + + #-- get interval + if($a[1] eq "interval") { + $value = $hash->{INTERVAL}; + return "$a[0] $reading => $value"; + } + + #-- reset presence + $hash->{PRESENT} = 0; + + #-- get memory page/counter according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- check syntax for getting counter + if( $reading eq "counter" ){ + return "OWCOUNT: get needs parameter when reading counter: " + if( int(@a)<2 ); + #-- channle may be addressed by bare channel name (A..D) or by defined channel name + return "OWCOUNT: invalid counter address, must be A or B" + if( !($a[2] =~ m/[AB]/) ); + my $page = ($a[2] eq "A") ? 14 : 15; + + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXCOUNT_GetPage($hash,$page); + #-- OWFS interface + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetPage($hash,"reading"); + #-- Unknown interface + }else{ + return "OWCOUNT: Get with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWCOUNT: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + return "OWCOUNT: $name.$reading => ".OWCOUNT_FormatValues($hash); + } + + +} + +####################################################################################### +# +# OWCOUNT_GetValues - Updates the reading from one device +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWCOUNT_GetValues($) { + my $hash = shift; + + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my $value = ""; + my $ret = ""; + my $offset; + my $factor; + + #-- define warnings + my $warn = "none"; + $hash->{ALARM} = "0"; + + #-- restart timer for updates + RemoveInternalTimer($hash); + InternalTimer(time()+$hash->{INTERVAL}, "OWCOUNT_GetValues", $hash, 1); + + #-- reset presence + $hash->{PRESENT} = 0; + + #-- Get readings, alarms and stati according to interface type + my $interface= $hash->{IODev}->{TYPE}; + if( $interface eq "OWX" ){ + $ret = OWXCOUNT_GetPage($hash,14); + $ret = OWXCOUNT_GetPage($hash,15); + #}elsif( $interface eq "OWFS" ){ + # $ret = OWFSAD_GetValues($hash); + }else{ + return "OWCOUNT: GetValues with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWCOUNT: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + $value=OWCOUNT_FormatValues($hash); + #--logging + Log 5, $value; + $hash->{CHANGED}[0] = $value; + + DoTrigger($name, undef); + + return undef; +} + +####################################################################################### +# +# OWCOUNT_Set - Set one value for device +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## + +sub OWCOUNT_Set($@) { + my ($hash, @a) = @_; + + my $key = $a[1]; + my $value = $a[2]; + + #-- for the selector: which values are possible + if (@a == 2){ + my $newkeys = join(" ", sort keys %sets); + return $newkeys ; + } + + #-- check syntax + return "OWCOUNT: Set needs one parameter when setting this value" + if( int(@a)!=3 ); + + #-- check argument + if( !defined($sets{$a[1]}) ){ + return "OWCOUNT: Set with unknown argument $a[1]"; + } + + #-- define vars + my $ret = undef; + my $channel = undef; + my $channo = undef; + my $factor; + my $offset; + my $condx; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + + #-- set new timer interval + if($key eq "interval") { + # check value + return "OWCOUNT: Set with short interval, must be > 1" + if(int($value) < 1); + # update timer + $hash->{INTERVAL} = $value; + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "OWCOUNT_GetValues", $hash, 1); + return undef; + } + +} + +######################################################################################## +# +# OWCOUNT_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWCOUNT_Undef ($) { + my ($hash) = @_; + delete($modules{OWCOUNT}{defptr}{$hash->{OW_ID}}); + RemoveInternalTimer($hash); + return undef; +} + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a 1-Wire bus connected +# via OWFS +# +# Prefix = OWFSCOUNT +# +######################################################################################## + + + + + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a 1-Wire bus connected +# directly to the FHEM server +# +# Prefix = OWXAD +# +######################################################################################## +# +# OWXAD_GetPage - Get one memory page + counter from device +# +# Parameter hash = hash of device addressed +# page = "reading", "alarm" or "status" +# +######################################################################################## + +sub OWXCOUNT_GetPage($$) { + my ($hash,$page) = @_; + + #-- For now, switch on conversion command + my $con=1; + + my ($select, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #=============== wrong value requested =============================== + if( ($page<0) || ($page>15) ){ + return "OWXCOUNT: Wrong memory page requested"; + } + #=============== get memory + counter =============================== + #-- issue the match ROM command \x55 and the read memory + counter command + # \xA5 TA1 TA2 reading 40 data bytes and 2 CRC bytes + my $ta2 = ($page*32) >> 8; + my $ta1 = ($page*32) & 255; + print "getting page Nr. $ta2 $ ta1\n"; + $select=sprintf("\x55%c%c%c%c%c%c%c%c\xA5%c%c", + @owx_ROM_ID,$ta1,$ta2); + #-- reset the bus + OWX_Reset($master); + #-- read the data + $res=OWX_Block($master,$select); + if( $res eq 0 ){ + return "OWX: Device $owx_dev not accessible in reading $page page"; + } + + #-- process results + #print "Have received ".length($res)." bytes\n"; + + #-- get 32 bytes + $select=""; + for( $i=0;$i<42;$i++){ + $select .= "\xFF"; + } + #-- read the data + $res=OWX_Block($master,$select); + + #-- process results + #print "Have received ".length($res)." bytes\n"; + + #-- get 10 bytes + $select=""; + for( $i=0;$i<10;$i++){ + $select .= "\xFF"; + } + #-- read the data + $res=OWX_Block($master,$select); + + #-- reset the bus + OWX_Reset($master); + + #-- process results + #print "Have received ".length($res)." bytes\n"; + @data=split(//,$res); + if ( ($data[4] | $data[5] | $data[6] | $data[7]) ne "\x00" ){ + return "OWXCOUNT: Device $owx_dev returns invalid data"; + } + + #-- for now ignore memory and only use counter + + my $value = ord($data[3])*4096 + ord($data[2])*256 +ord($data[1])*16 + ord($data[0]); + #print "Value received = $value\n"; + if( $page == 14) { + $owg_val[0] = $value; + }elsif( $page == 15) { + $owg_val[1] = $value; + } + return undef +} + +######################################################################################## +# +# OWXCOUNT_SetPage - Set one memory page of device +# +# Parameter hash = hash of device addressed +# page = "alarm" or "status" +# +######################################################################################## + +sub OWXCOUNT_SetPage($$) { + + my ($hash,$page) = @_; + + my ($select, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #=============== set the alarm values =============================== + #if ( $page eq "test" ) { + #-- issue the match ROM command \x55 and the set alarm page command + # \x55\x10\x00 reading 8 data bytes and 2 CRC bytes + # $select=sprintf("\x55%c%c%c%c%c%c%c%c\x55\x10\x00", + # @owx_ROM_ID); + # + #=============== wrong page write attempt =============================== + #} else { + return "OWXCOUNT: Wrong memory page write attempt"; + #} + + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- process results + if( $res eq 0 ){ + return "OWXCOUNT: Device $owx_dev not accessible for writing"; + } + + return undef; +} + +1; diff --git a/contrib/contrib/1-Wire/21_OWID.pm b/contrib/contrib/1-Wire/21_OWID.pm new file mode 100644 index 000000000..e71d1d23a --- /dev/null +++ b/contrib/contrib/1-Wire/21_OWID.pm @@ -0,0 +1,212 @@ +######################################################################################## +# +# OWID.pm +# +# FHEM module to commmunicate with general 1-Wire ID-ROMS +# +# Attention: This module may communicate with the OWX module, +# but currently not with the 1-Wire File System OWFS +# +# Prefixes for subroutines of this module: +# OW = General 1-Wire routines Peter Henning) +# +# Prof. Dr. Peter A. Henning, 2012 +# +# Version 1.11 - March, 2012 +# +# Setup bus device in fhem.cfg as +# +# define OWID +# +# where may be replaced by any name string +# +# is a 2 character (1 byte) 1-Wire Family ID +# +# is a 12 character (6 byte) 1-Wire ROM ID +# without Family ID, e.g. A2D90D000800 +# +# get id => FAM_ID.ROM_ID.CRC +# get present => 1 if device present, 0 if not +# +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +#-- Prototypes to make komodo happy +use vars qw{%attr %defs}; +use strict; +use warnings; +sub Log($$); + +#-- declare variables +my %gets = ( + "present" => "", + "id" => "" +); +my %sets = (); +my %updates = (); + +######################################################################################## +# +# The following subroutines are independent of the bus interface +# +# Prefix = OWID +# +######################################################################################## +# +# OWID_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWID_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "OWID_Define"; + $hash->{UndefFn} = "OWID_Undef"; + $hash->{GetFn} = "OWID_Get"; + $hash->{SetFn} = undef; + my $attlist = "IODev do_not_notify:0,1 showtime:0,1 loglevel:0,1,2,3,4,5 "; + $hash->{AttrList} = $attlist; +} + +######################################################################################### +# +# OWID_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################### + +sub OWID_Define ($$) { + my ($hash, $def) = @_; + + #-- define OWID + my @a = split("[ \t][ \t]*", $def); + + my ($name,$fam,$id,$crc,$ret); + + #-- default + $name = $a[0]; + $ret = ""; + + #-- check syntax + return "OWID: Wrong syntax, must be define OWID " + if(int(@a) !=4 ); + + #-- check id + if( $a[2] =~ m/^[0-9|a-f|A-F]{2}$/ ) { + $fam = $a[2]; + } else { + return "OWID: $a[0] family id $a[2] invalid, specify a 2 digit value"; + } + if( $a[3] =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $id = $a[3]; + } else { + return "OWID: $a[0] ID $a[3] invalid, specify a 12 digit value"; + } + + #-- 1-Wire ROM identifier in the form "FF.XXXXXXXXXXXX.YY" + # determine CRC Code YY - only if this is a direct interface + $crc = defined($hash->{IODev}->{INTERFACE}) ? sprintf("%02x",OWX_CRC($fam.".".$id."00")) : "00"; + + #-- Define device internals + $hash->{ROM_ID} = $fam.".".$id.$crc; + $hash->{OW_ID} = $id; + $hash->{OW_FAMILY} = $fam; + $hash->{PRESENT} = 0; + + #-- Couple to I/O device + AssignIoPort($hash); + Log 3, "OWID: Warning, no 1-Wire I/O device found for $name." + if(!defined($hash->{IODev}->{NAME})); + + $modules{OWID}{defptr}{$id} = $hash; + + $hash->{STATE} = "Defined"; + Log 3, "OWID: Device $name defined."; + + #-- Initialization reading according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + $hash->{STATE} = "Initialized"; + return undef; +} + +######################################################################################## +# +# OWID_Get - Implements GetFn function +# +# Parameter hash = hash of device addressed, a = argument array +# +######################################################################################## + +sub OWID_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my $value = undef; + my $ret = ""; + my $offset; + my $factor; + + #-- check syntax + return "OWID: Get argument is missing @a" + if(int(@a) != 2); + + #-- check argument + return "OWID: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + #-- get id + if($a[1] eq "id") { + $value = $hash->{ROM_ID}; + return "$a[0] $reading => $value"; + } + + #-- get present + if($a[1] eq "present") { + #-- hash of the busmaster + my $master = $hash->{IODev}; + $value = OWX_Verify($master,$hash->{ROM_ID}); + $hash->{PRESENT} = $value; + return "$a[0] $reading => $value"; + } +} + +######################################################################################## +# +# OWID_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWID_Undef ($) { + my ($hash) = @_; + delete($modules{OWID}{defptr}{$hash->{OW_ID}}); + RemoveInternalTimer($hash); + return undef; +} + +1; diff --git a/contrib/contrib/1-Wire/21_OWLCD.pm b/contrib/contrib/1-Wire/21_OWLCD.pm new file mode 100644 index 000000000..b41d67fd8 --- /dev/null +++ b/contrib/contrib/1-Wire/21_OWLCD.pm @@ -0,0 +1,970 @@ +######################################################################################## +# +# OWLCD.pm +# +# FHEM module to commmunicate with the 1-Wire LCD hardware +# +# Attention: This module may communicate with the OWX module, +# but currently not with the 1-Wire File System OWFS +# +# Prefixes for subroutines of this module: +# OW = General 1-Wire routines Peter Henning +# +# Prof. Dr. Peter A. Henning, 2012 +# +# Version 1.11 - March, 2012 +# +# Setup bus device in fhem.cfg as +# +# define OWLCD +# +# where may be replaced by any name string +# +# is a 12 character (6 byte) 1-Wire ROM ID +# without Family ID, e.g. A2D90D000800 +# +# get id => FAM_ID.ROM_ID.CRC +# get present => 1 if device present, 0 if not +# get gpio => current state of the gpio pins (15 = all off, 0 = all on) +# get counter => four values (16 Bit) of the gpio counter +# get version => firmware version of the LCD adapter +# +# Careful: Not ASCII ! strange Codepage +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +#-- Prototypes to make komodo happy +use vars qw{%attr %defs}; +use strict; +use warnings; +sub Log($$); + +#-- controller may be HD44780 or KS0073 +my $lcdcontroller = "KS0073"; +my $lcdlines = 4; +my $lcdchars = 20; + +#-- declare variables +my %gets = ( + "present" => "", + "id" => "", + "gpio" => "", + "counter" => "", + #"memory" => "", + "version" => "", + #"register" => "", + #"data" => "" +); +my %sets = ( + "icon" => "", + "line" => "", + "gpio" => "", + "backlight" => "", + "lcd" => "", + "reset" => "", + "test" => "" + +); +my %updates = (); + +######################################################################################## +# +# The following subroutines are independent of the bus interface +# +# Prefix = OWLCD +# +######################################################################################## +# +# OWLCD_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWLCD_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "OWLCD_Define"; + $hash->{UndefFn} = "OWLCD_Undef"; + $hash->{GetFn} = "OWLCD_Get"; + $hash->{SetFn} = "OWLCD_Set"; + my $attlist = "IODev do_not_notify:0,1 showtime:0,1 loglevel:0,1,2,3,4,5 ". + ""; + $hash->{AttrList} = $attlist; +} + +######################################################################################### +# +# OWLCD_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################### + +sub OWLCD_Define ($$) { + my ($hash, $def) = @_; + + #-- define OWLCD + my @a = split("[ \t][ \t]*", $def); + + my ($name,$fam,$id,$crc,$ret); + + #-- default + $name = $a[0]; + $ret = ""; + + #-- check syntax + return "OWLCD: Wrong syntax, must be define OWLCD " + if(int(@a) !=3 ); + + #-- check id + if( $a[2] =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $id = $a[2]; + } else { + return "OWLCD: $a[0] ID $a[2] invalid, specify a 12 digit value"; + } + + #-- 1-Wire ROM identifier in the form "FF.XXXXXXXXXXXX.YY" + # determine CRC Code - only if this is a direct interface + $crc = defined($hash->{IODev}->{INTERFACE}) ? sprintf("%02x",OWX_CRC("FF.".$id."00")) : "00"; + + #-- Define device internals + $hash->{ROM_ID} = "FF.".$id.$crc; + $hash->{OW_ID} = $id; + $hash->{OW_FAMILY} = "FF"; + $hash->{PRESENT} = 0; + + #-- Couple to I/O device + AssignIoPort($hash); + Log 3, "OWLCD: Warning, no 1-Wire I/O device found for $name." + if(!defined($hash->{IODev}->{NAME})); + + $modules{OWLCD}{defptr}{$id} = $hash; + + $hash->{STATE} = "Defined"; + Log 3, "OWLCD: Device $name defined."; + + #-- Initialization reading according to interface type + my $interface= $hash->{IODev}->{TYPE}; + #-- OWX interface + if( $interface eq "OWX" ){ + OWXLCD_InitializeDevice($hash); + #-- set backlight on + OWXLCD_SetFunction($hash,"bklon",0); + #-- erase all icons + OWXLCD_SetIcon($hash,0,0); + #-- Unknown interface + }else{ + return "OWLCD: Wrong IODev type $interface"; + } + $hash->{STATE} = "Initialized"; + return undef; +} + +######################################################################################## +# +# OWLCD_Get - Implements GetFn function +# +# Parameter hash = hash of device addressed, a = argument array +# +######################################################################################## + +sub OWLCD_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my $value = undef; + my $ret = ""; + my $offset; + my $factor; + + #-- check syntax + return "OWLCD: Get argument is missing @a" + if(int(@a) != 2); + + #-- check argument + return "OWLCD: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + #-- get id + if($a[1] eq "id") { + $value = $hash->{ROM_ID}; + return "$a[0] $reading => $value"; + } + + #-- get present + if($a[1] eq "present") { + #-- hash of the busmaster + my $master = $hash->{IODev}; + $value = OWX_Verify($master,$hash->{ROM_ID}); + $hash->{PRESENT} = $value; + return "$a[0] $reading => $value"; + } + + #-- get gpio states + if($a[1] eq "gpio") { + $value = OWXLCD_Get($hash,"gpio",0); + return "$a[0] $reading => $value"; + } + + #-- get gpio counters + if($a[1] eq "counter") { + $value = OWXLCD_Get($hash,"counter",0); + return "$a[0] $reading => $value"; + } + + #-- get EEPROM counters + if($a[1] eq "memory") { + $value = OWXLCD_Get($hash,"memory",0); + return "$a[0] $reading => $value"; + } + + #-- get version + if($a[1] eq "version") { + $value = OWXLCD_Get($hash,"version",0); + return "$a[0] $reading => $value"; + } + + #-- get register + if($a[1] eq "register") { + $value = OWXLCD_Get($hash,"register",0); + return "$a[0] $reading => $value"; + } + + #-- get data + if($a[1] eq "data") { + $value = OWXLCD_Get($hash,"data",0); + return "$a[0] $reading => $value"; + } +} + +####################################################################################### +# +# OWLCD_Set - Set one value for device +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## + +sub OWLCD_Set($@) { + my ($hash, @a) = @_; + + my $key = $a[1]; + my $value = $a[2]; + my ($line,$icon,$i); + + #-- for the selector: which values are possible + return join(" ", keys %sets) + if ( (@a == 2) && !(($key eq "reset") || ($key eq "test")) ); + + #-- check argument + if( !defined($sets{$a[1]}) ){ + return "OWLCD: Set with unknown argument $a[1]"; + } + + #-- check syntax for setting line + if( $key eq "line" ){ + return "OWLCD: Set needs two parameters when setting line value: <#line> " + if( int(@a)<3 ); + $line = ($a[2] =~ m/\d/) ? $a[2] : 0; + $value = $a[3]; + for( $i=4; $i< int(@a); $i++){ + $value .= " ".$a[$i]; + } + #-- check syntax for setting icon + } elsif ( $key eq "icon" ){ + if( ($a[2] ne "0") && ($a[2] ne "none") ){ + return "OWLCD: Set needs two parameters when setting icon value: <#icon> on/off/blink (resp. 0..5/off/blink for #16)" + if( (int(@a)!=4) ); + $icon = ($a[2] =~ m/\d\d?/) ? $a[2] : 0; + $value = $a[3]; + } else { + return "OWLCD: Set needs only one parameter when resetting icons" + if( (int(@a)!=3) ); + $icon = 0; + $value = "OFF"; + } + #-- check syntax for reset and test + } elsif ( ($key eq "reset") || ($key eq "test") ){ + return "OWLCD: Set needs no parameters when setting $key value" + if( int(@a)!=2 ); + #-- other syntax + } else { + return "OWLCD: Set needs one parameter when setting $key value" + if( int(@a)!=3 ); + } + + #-- define vars + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + + #-- set gpio ports from all off = to all on = 7 + if($key eq "gpio") { + #-- check value and write to device + return "OWLCD: Set with wrong value for gpio port, must be 0 <= gpio <= 7" + if( ! ((int($value) > 0) && (int($value) < 7)) ); + OWXLCD_SetFunction($hash, "gpio", int($value)); + return undef; + } + + #-- set LCD ON or OFF + if($key eq "lcd") { + #-- check value and write to device + if( uc($value) eq "ON"){ + OWXLCD_SetFunction($hash, "lcdon", 0); + }elsif( uc($value) eq "OFF" ){ + OWXLCD_SetFunction($hash, "lcdoff", 0); + } else { + return "OWLCD: Set with wrong value for lcd, must be on/off" + } + return undef; + } + + #-- set LCD Backlight ON or OFF + if($key eq "backlight") { + #-- check value and write to device + if( uc($value) eq "ON"){ + OWXLCD_SetFunction($hash, "bklon", 0); + }elsif( uc($value) eq "OFF" ){ + OWXLCD_SetFunction($hash, "bkloff", 0); + } else { + return "OWLCD: Set with wrong value for backlight, must be on/off" + } + return undef; + } + + #-- reset + if($key eq "reset") { + OWXLCD_SetFunction($hash,"reset",0); + OWXLCD_SetIcon($hash,0,0); + return undef; + } + + #-- set icon + if($key eq "icon") { + return "OWLCD: Wrong icon type, choose 0..16" + if( ( 0 > $icon ) || ($icon > 16) ); + #-- check value and write to device + if( $icon == 16 ){ + if( uc($value) eq "OFF" ){ + OWXLCD_SetIcon($hash, 16, 0); + }elsif( uc($value) eq "BLINK" ){ + OWXLCD_SetIcon($hash, 16, 6); + }elsif( ((int($value) > 0) && (int($value) < 6)) ){ + OWXLCD_SetIcon($hash, 16, int($value)); + } else { + return "OWLCD: Set with wrong value for icon #16, must be 0..5/off/blink" + } + }else{ + if( uc($value) eq "OFF"){ + OWXLCD_SetIcon($hash, $icon, 0); + }elsif( uc($value) eq "ON" ){ + OWXLCD_SetIcon($hash, $icon, 1); + }elsif( uc($value) eq "BLINK" ){ + OWXLCD_SetIcon($hash, $icon, 2); + } else { + return "OWLCD: Set with wrong value for icon $icon, must be on/off/blink" + } + } + return undef; + } + + #-- set a single LCD line + if($key eq "line") { + return "OWLCD: Wrong line number, choose 0..".$lcdlines + if( ( 0 > $line ) || ($line > ($lcdlines-1)) ); + return "OWLCD: Wrong line length, must be < ".$lcdchars + if( length($value) > $lcdchars ); + #-- check value and write to device + OWXLCD_SetLine($hash,$line,$value); + return undef; + } + + #-- start test + if($key eq "test") { + OWXLCD_SetLine($hash,0,"Hallo Welt"); + OWXLCD_SetLine($hash,1,"Mary had a big lamb"); + OWXLCD_SetLine($hash,2,"Solar 4.322 kW "); + OWXLCD_SetLine($hash,3,"\x5B\x5C\x5E\x7B\x7C\x7E\xBE"); + return undef; + } +} + +######################################################################################## +# +# OWLCD_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWLCD_Undef ($) { + my ($hash) = @_; + delete($modules{OWLCD}{defptr}{$hash->{OW_ID}}); + RemoveInternalTimer($hash); + return undef; +} + +######################################################################################## +# +# OWXLCD_Byte - write a single byte to the LCD device +# +# Parameter hash = hash of device addressed +# cmd = register or data +# byte = byte +# +######################################################################################## + +sub OWXLCD_Byte($$$) { + + my ($hash,$cmd,$byte) = @_; + + my ($select, $select2, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- issue the match ROM command \x55 + $select = sprintf("\x55%c%c%c%c%c%c%c%c", + @owx_ROM_ID); + #=============== write to LCD register =============================== + if ( $cmd eq "register" ) { + #-- issue the read LCD register command \x10 + $select .= sprintf("\x10%c",$byte); + #=============== write to LCD data =============================== + }elsif ( $cmd eq "data" ) { + #-- issue the read LCD data command \x12 + $select .= sprintf("\x12%c",$byte); + #=============== wrong value requested =============================== + } else { + return "OWXLCD: Wrong byte write attempt"; + } + + #-- write to device + OWX_Reset($master); + $res=OWX_Block($master,$select); + #-- process results + if( $res eq 0 ){ + return "OWLCD: Device $owx_dev not accessible for writing a byte"; + } + + return undef; +} + +######################################################################################## +# +# OWXLCD_Get - get values from the LCD device +# +# Parameter hash = hash of device addressed +# cmd = command string +# page = memory page address +# +######################################################################################## + +sub OWXLCD_Get($$$) { + + my ($hash,$cmd,$value) = @_; + + my ($select, $select2, $len, $addr, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- issue the match ROM command \x55 + $select = sprintf("\x55%c%c%c%c%c%c%c%c", + @owx_ROM_ID); + #-- issue the match ROM command \x55 and the read scratchpad command \xBE + $select2 = $select."\xBE"; + #=============== fill scratch with gpio ports =============================== + if ( $cmd eq "gpio" ) { + #-- issue the read GPIO command \x22 (1 byte) + $select .= "\x22"; + $len = 1; + #=============== fill scratch with gpio counters =============================== + }elsif ( $cmd eq "counter" ) { + #-- issue the read counter command \x23 (8 bytes) + $select .= "\x23"; + $len = 8; + #=============== fill scratch with EEPROM =============================== + #}elsif ( $cmd eq "memory" ) { + # #-- issue the read EEPROM command \x37 + # $len=16; + # $select .= "\x37"; + #=============== fill scratch with version =============================== + }elsif ( $cmd eq "version" ) { + #-- issue the read version command \x41 + $select .= "\x41"; + $len = 16; + #=============== fill scratch with LCD register =============================== + #}elsif ( $cmd eq "register" ) { + # #-- issue the read LCD register command \x11 + # $select .= "\x11"; + # $len = 16; + #=============== fill scratch with LCD data =============================== + #}elsif ( $cmd eq "data" ) { + # #-- issue the read LCD data command \x13 + # $addr = 0; + # $len = 16; + # #$select .= sprintf("\x13%c",$addr); + # $select .= "\x13\x00\x10"; + #=============== wrong value requested =============================== + } else { + return "OWXLCD: Wrong get attempt"; + } + #-- write to device + OWX_Reset($master); + $res=OWX_Block($master,$select); + #-- process results + if( $res eq 0 ){ + return "OWLCD: Device $owx_dev not accessible for reading"; + } + + #-- sleeping for some time + #select(undef,undef,undef,0.5); + + #-- fill according to expected length + for($i=0;$i<$len;$i++){ + $select2 .= "\xFF"; + } + #-- write to device + OWX_Reset($master); + $res=OWX_Block($master,$select2); + #-- process results + if( $res eq 0 ){ + return "OWLCD: Device $owx_dev not accessible for reading in 2nd step"; + } + + #-- process results (10 byes or more have been sent) + $res = substr($res,10); + #my $ress = "OWXLCD: Answer was "; + # for($i=0;$i{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- supposedly we do not need to do anything with a HD44780 + if( $lcdcontroller eq "HD44780"){ + return undef; + #-- need some additional sequence for KS0073 + }elsif ( $lcdcontroller eq "KS0073"){ + + #-- Function Set: 4 bit data size, RE => 0 = \x20 + #OWXLCD_Byte($hash,"register",32); + + #-- Entry Mode Set: cursor auto increment = \x06 + #OWXLCD_Byte($hash,"register",6); + + #-- Function Set: 4 bit data size, RE => 1, blink Enable = \x26 + OWXLCD_Byte($hash,"register",38); + + #-- Ext. Function Set: 4 line mode = \x09 + OWXLCD_Byte($hash,"register",9); + + #-- Function Set: 4 bit data size, RE => 0 = \x20 + OWXLCD_Byte($hash,"register",32); + + #-- Display ON/OFF: display on, cursor off, blink off = \x0C + OWXLCD_Byte($hash,"register",12); + + #-- Clear Display + OWXLCD_Byte($hash,"register",1); + + return undef; + #-- or else + } else { + return "OWXLCD: Wrong LCD controller type"; + } + +} + +######################################################################################## +# +# OWXLCD_SetFunction - write state and values of the LCD device +# +# Parameter hash = hash of device addressed +# cmd = command string +# value = data value +# +######################################################################################## + +sub OWXLCD_SetFunction($$$) { + + my ($hash,$cmd,$value) = @_; + + my ($select, $res, $res2, $res3, @data); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + #-- issue the match ROM command \x55 + $select=sprintf("\x55%c%c%c%c%c%c%c%c",@owx_ROM_ID); + + #=============== set gpio ports =============================== + if ( $cmd eq "gpio" ) { + #-- issue the write GPIO command + # \x21 followed by the data value (= integer 0 - 7) + $select .= sprintf("\x21%c",$value); + #=============== switch LCD on =============================== + }elsif ( $cmd eq "lcdon" ) { + #-- issue the lcd on cmd + $select .= "\x03"; + #=============== switch LCD off =============================== + }elsif ( $cmd eq "lcdoff" ) { + #-- issue the lcd off cmd + $select .= "\x05"; + #=============== switch LCD backlight on =============================== + }elsif ( $cmd eq "bklon" ) { + #-- issue the backlight on cmd + $select .= "\x08"; + #=============== switch LCD backlight off =============================== + }elsif ( $cmd eq "bkloff" ) { + #-- issue the backlight off cmd + $select .= "\x07"; + #=============== switch LCD backlight off =============================== + }elsif ( $cmd eq "reset" ) { + #-- issue the clear LCD command + $select .= "\x49"; + #=============== wrong write attempt =============================== + } else { + return "OWXLCD: Wrong function selected"; + } + + #-- write to device + OWX_Reset($master); + $res=OWX_Block($master,$select); + #-- process results + if( $res eq 0 ){ + return "OWLCD: Device $owx_dev not accessible for writing"; + } + + return undef; +} + +######################################################################################## +# +# OWXLCD_SetIcon - set one of the icons +# +# Parameter hash = hash of device addressed +# icon = address of the icon used = 0,1 .. 16 (0 = all off) +# value = data value: 0 = off, 1 = on, 2 = blink +# for battery icon 16: 0 = off, 1 = empty ... 5 = full, 6 = empty blink +# +######################################################################################## + +sub OWXLCD_SetIcon($$$) { + my ($hash,$icon,$value) = @_; + + my ($i,$data,$select, $res); + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- only for KS0073 + if ( $lcdcontroller eq "KS0073"){ + + #-- write 16 zeros to erase all icons + if( $icon == 0){ + #-- 4 bit data size, RE => 1, blink Enable = \x26 + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x10\x26",@owx_ROM_ID); + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- SEGRAM addres to 0 = \x40, + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x10\x40",@owx_ROM_ID); + #-- write 16 zeros to scratchpad + $select .= "\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- issue the copy scratchpad to LCD command \x48 + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x48",@owx_ROM_ID); + OWX_Reset($master); + $res=OWX_Block($master,$select); + } else { + #-- determine data value + if( int($icon) != 16 ){ + if( $value == 0 ){ + $data = 0; + } elsif ( $value == 1) { + $data = 16; + } elsif ( $value == 2) { + $data = 80; + } else { + return "OWXLCD: Wrong data value $value for icon $icon"; + } + } else { + if( $value == 0 ){ + $data = 0; + } elsif ( $value == 1) { + $data = 16; + } elsif ( $value == 2) { + $data = 24; + } elsif ( $value == 3) { + $data = 28; + } elsif ( $value == 4) { + $data = 30; + } elsif ( $value == 5) { + $data = 31; + } elsif ( $value == 6) { + $data = 80; + } else { + return "OWXLCD: Wrong data value $value for icon $icon"; + } + } + #-- 4 bit data size, RE => 1, blink Enable = \x26 + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x10\x26",@owx_ROM_ID); + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- SEGRAM addres to 0 = \x40 + icon address + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x10%c",@owx_ROM_ID,63+$icon); + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- data + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x12%c",@owx_ROM_ID,$data); + OWX_Reset($master); + $res=OWX_Block($master,$select); + } + #-- return to normal state + $select = sprintf("\x55%c%c%c%c%c%c%c%c\x10\x20",@owx_ROM_ID); + OWX_Reset($master); + $res=OWX_Block($master,$select); + #-- or else + } else { + return "OWXLCD: Wrong LCD controller type"; + } +} + +######################################################################################## +# +# OWXLCD_SetLine - set one of the display lines +# +# Parameter hash = hash of device addressed +# line = line number (0..3) +# msg = data string to be written +# +######################################################################################## + +sub OWXLCD_SetLine($$$) { + + my ($hash,$line,$msg) = @_; + + my ($select, $res, $res2, $res3, $i, $msgA, $msgB); + $res2 = ""; + $line = int($line); + $msg = defined($msg) ? $msg : ""; + #-- replace umlaut chars for special codepage + $msg =~ s/ä/\x7B/g; + $msg =~ s/ö/\x7C/g; + $msg =~ s/ü/\x7E/g; + $msg =~ s/Ä/\x5B/g; + $msg =~ s/Ö/\x5C/g; + $msg =~ s/Ü/\x5E/g; + $msg =~ s/ß/\xBE/g; + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- split if longer than 16 bytes, fill each with blanks + if( length($msg) > 16 ) { + $msgA = substr($msg,0,16); + $msgB = substr($msg,16,length($msg)-16); + for($i = 0;$i<32-length($msg);$i++){ + $msgB .= "\x20"; + } + } else { + $msgA = $msg; + for($i = 0;$i<16-length($msg);$i++){ + $msgA .= "\x20"; + } + $msgB = undef; + } + + #-- issue the match ROM command \x55 and the write scratchpad command \x4E + # followed by LCD page address and the text + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x4E\%c",@owx_ROM_ID,$line*32).$msgA; + OWX_Reset($master); + $res=OWX_Block($master,$select); + + #-- issue the copy scratchpad to LCD command \x48 + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x48",@owx_ROM_ID); + OWX_Reset($master); + $res3=OWX_Block($master,$select); + + #-- if second string available: + if( defined($msgB) ){ + #-- issue the match ROM command \x55 and the write scratchpad command \x4E + # followed by LCD page address and the text + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x4E\%c",@owx_ROM_ID,$line*32+16).$msgB; + OWX_Reset($master); + $res2=OWX_Block($master,$select); + + #-- issue the copy scratchpad to LCD command \x48 + $select=sprintf("\x55%c%c%c%c%c%c%c%c\x48",@owx_ROM_ID); + OWX_Reset($master); + $res3=OWX_Block($master,$select); + } + + #-- process results + if( ($res eq 0) || ($res2 eq 0) ){ + return "OWLCD: Device $owx_dev not accessible for writing"; + } + + return undef; + +} + +1; diff --git a/contrib/contrib/1-Wire/21_OWTEMP.pm b/contrib/contrib/1-Wire/21_OWTEMP.pm new file mode 100644 index 000000000..70bf0aae9 --- /dev/null +++ b/contrib/contrib/1-Wire/21_OWTEMP.pm @@ -0,0 +1,821 @@ +######################################################################################## +# +# OWTEMP.pm +# +# FHEM module to commmunicate with 1-Wire temperature sensors DS1820, DS18S20, DS18B20, DS1822 +# +# Attention: This module works as a replacement for the standard 21_OWTEMP.pm, +# therefore may communicate with the 1-Wire File System OWFS, +# but also with the newer and more direct OWX module +# +# Prefixes for subroutines of this module: +# OW = General 1-Wire routines (Martin Fischer, Peter Henning) +# OWFS = 1-Wire file system (Martin Fischer) +# OWX = 1-Wire bus master interface (Peter Henning) +# +# Prof. Dr. Peter A. Henning, 2012 +# Martin Fischer, 2011 +# +# Version 1.11 - March, 2012 +# +# Setup bus device in fhem.cfg as +# +# define OWTEMP [] [interval] +# +# where may be replaced by any name string +# +# is a 1-Wire device type. If omitted, we assume this to be an +# DS1820 temperature sensor +# Currently allowed values are DS1820, DS1822 +# is a 12 character (6 byte) 1-Wire ROM ID +# without Family ID, e.g. A2D90D000800 +# [interval] is an optional query interval in seconds +# +# get id => OW_FAMILY.ROM_ID.CRC +# get present => 1 if device present, 0 if not +# get interval => query interval +# get temperature => temperature measurement +# get alarm => alarm temperature settings +# +# set interval => set period for measurement +# set tempLow => lower alarm temperature setting +# set tempHigh => higher alarm temperature setting +# +# Additional attributes are defined in fhem.cfg +# Note: attributes "tempXXXX" are read during every update operation. +# +# attr stateAL "" = character string for denoting low alarm condition, default is red down triangle +# attr stateAH "" = character string for denoting high alarm condition, default is red up triangle +# attr tempOffset = temperature offset in degree Celsius added to the raw temperature reading +# attr tempUnit = unit of measurement, e.g. Celsius/Kelvin/Fahrenheit or C/K/F, default is Celsius +# attr tempLow = measurement value for low alarm +# attr tempHigh = measurement for high alarm +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +#-- Prototypes to make komodo happy +use vars qw{%attr %defs}; +use strict; +use warnings; +sub Log($$); + +#-- temperature globals - always the raw values from the device +my $owg_temp = 0; +my $owg_th = 0; +my $owg_tl = 0; + +#-- variables for display strings +my $stateal; +my $stateah; + +my %gets = ( + "id" => "", + "present" => "", + "interval" => "", + "temperature" => "", + "alarm" => "" +); + +my %sets = ( + "interval" => "", + "tempHigh" => "", + "tempLow" => "" +); + +my %updates = ( + "present" => "", + "temperature" => "", + "alarm" => "" +); + +######################################################################################## +# +# The following subroutines are independent of the bus interface +# +# Prefix = OWTEMP +# +######################################################################################## +# +# OWTEMP_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWTEMP_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "OWTEMP_Define"; + $hash->{UndefFn} = "OWTEMP_Undef"; + $hash->{GetFn} = "OWTEMP_Get"; + $hash->{SetFn} = "OWTEMP_Set"; + #tempOffset = a temperature offset added to the temperature reading for correction + #tempUnit = a unit of measure: C/F/K + $hash->{AttrList}= "IODev do_not_notify:0,1 showtime:0,1 loglevel:0,1,2,3,4,5 ". + "stateAL stateAH ". + "tempOffset tempUnit:C,Celsius,F,Fahrenheit,K,Kelvin ". + "tempLow tempHigh"; + } + +######################################################################################## +# +# OWTEMP_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################## + +sub OWTEMP_Define ($$) { + my ($hash, $def) = @_; + + # define OWTEMP [] [interval] + # e.g.: define flow OWTEMP 525715020000 300 + my @a = split("[ \t][ \t]*", $def); + + my ($name,$model,$fam,$id,$crc,$interval,$ret); + my $tn = TimeNow(); + + #-- default + $name = $a[0]; + $interval = 300; + $ret = ""; + + #-- check syntax + return "OWTEMP: Wrong syntax, must be define OWTEMP [] [interval]" + if(int(@a) < 2 || int(@a) > 6); + + #-- check if this is an old style definition, e.g. is missing + my $a2 = $a[2]; + my $a3 = defined($a[3]) ? $a[3] : ""; + if( ($a2 eq "none") || ($a3 eq "none") ) { + return "OWTEMP: ID = none is obsolete now, please redefine"; + } elsif( $a2 =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $model = "DS1820"; + $id = $a[2]; + if(int(@a)>=4) { $interval = $a[3]; } + Log 1, "OWTEMP: Parameter [alarminterval] is obsolete now - must be set with I/O-Device" + if(int(@a) == 5); + } elsif( $a3 =~ m/^[0-9|a-f|A-F]{12}$/ ) { + $model = $a[2]; + $id = $a[3]; + if(int(@a)>=5) { $interval = $a[4]; } + Log 1, "OWTEMP: Parameter [alarminterval] is obsolete now - must be set with I/O-Device" + if(int(@a) == 6); + } else { + return "OWTEMP: $a[0] ID $a[2] invalid, specify a 12 digit value"; + } + + #-- 1-Wire ROM identifier in the form "FF.XXXXXXXXXXXX.YY" + # FF = family id follows from the model + # YY must be determined from id + if( $model eq "DS1820" ){ + $fam = "10"; + }elsif( $model eq "DS1822" ){ + $fam = "22"; + }elsif( $model eq "DS18B20" ){ + $fam = "28"; + }else{ + return "OWTEMP: Wrong 1-Wire device model $model"; + } + # determine CRC Code - only if this is a direct interface + $crc = defined($hash->{IODev}->{INTERFACE}) ? sprintf("%02x",OWX_CRC($fam.".".$id."00")) : "00"; + + #-- define device internals + $hash->{ALARM} = 0; + $hash->{OW_ID} = $id; + $hash->{OW_FAMILY} = $fam; + $hash->{PRESENT} = 0; + $hash->{ROM_ID} = $fam.".".$id.$crc; + $hash->{INTERVAL} = $interval; + + #-- Couple to I/O device + AssignIoPort($hash); + Log 3, "OWTEMP: Warning, no 1-Wire I/O device found for $name." + if(!defined($hash->{IODev}->{NAME})); + $modules{OWTEMP}{defptr}{$id} = $hash; + $hash->{STATE} = "Defined"; + Log 3, "OWTEMP: Device $name defined."; + + #-- Start timer for initialization in a few seconds + InternalTimer(time()+1, "OWTEMP_InitializeDevice", $hash, 0); + + #-- Start timer for updates + InternalTimer(time()+$hash->{INTERVAL}, "OWTEMP_GetValues", $hash, 0); + + return undef; +} + +######################################################################################## +# +# OWTEMP_InitializeDevice - delayed setting of initial readings and channel names +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWTEMP_InitializeDevice($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + $stateal = defined($attr{$name}{stateAL}) ? $attr{$name}{stateAL} : ""; + $stateah = defined($attr{$name}{stateAH}) ? $attr{$name}{stateAH} : ""; + + #-- unit attribute defined ? + $hash->{READINGS}{"temperature"}{UNIT} = defined($attr{$name}{"tempUnit"}) ? $attr{$name}{"tempUnit"} : "Celsius"; + $hash->{READINGS}{"temperature"}{TYPE} = "temperature"; + + #-- Initial readings temperature sensor + $owg_temp = 0.0; + $owg_tl = -15.0; + $owg_th = 70.0; + + #-- Initialize all the display stuff + OWTEMP_FormatValues($hash); +} + +######################################################################################## +# +# OWTEMP_FormatValues - put together various format strings +# +# Parameter hash = hash of device addressed, fs = format string +# +######################################################################################## + +sub OWTEMP_FormatValues($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + my ($unit,$offset,$factor,$abbr,$vval,$vlow,$vhigh,$statef); + my ($value1,$value2,$value3) = ("","",""); + + my $tn = TimeNow(); + + #-- attributes defined ? + $unit = defined($attr{$name}{"tempUnit"}) ? $attr{$name}{"tempUnit"} : $hash->{READINGS}{"temperature"}{UNIT}; + $offset = defined($attr{$name}{"tempoffset"}) ? $attr{$name}{"tempOffset"} : 0.0 ; + $factor = 1.0; + + if( $unit eq "Celsius" ){ + $abbr = "°C"; + } elsif ($unit eq "Kelvin" ){ + $abbr = "K"; + $offset += "273.16" + } elsif ($unit eq "Fahrenheit" ){ + $abbr = "°F"; + $offset = ($offset+32)/1.8; + $factor = 1.8; + } else { + $abbr="?"; + Log 1, "OWTEMP_FormatValues: unknown unit $unit"; + } + #-- these values are rather coplex to obtain, therefore save them in the hash + $hash->{READINGS}{"temperature"}{UNIT} = $unit; + $hash->{READINGS}{"temperature"}{UNITABBR} = $abbr; + $hash->{tempf}{offset} = $offset; + $hash->{tempf}{factor} = $factor; + + #-- correct values for proper offset, factor + $vval = ($owg_temp + $offset)*$factor; + + #-- put into READINGS + $hash->{READINGS}{"temperature"}{VAL} = $vval; + $hash->{READINGS}{"temperature"}{TIME} = $tn; + + #-- correct alarm values for proper offset, factor + $vlow = ($owg_tl + $offset)*$factor; + $vhigh = ($owg_th + $offset)*$factor; + + #-- put into READINGS + $hash->{READINGS}{"tempLow"}{VAL} = $vlow; + $hash->{READINGS}{"tempLow"}{TIME} = $tn; + $hash->{READINGS}{"tempHigh"}{VAL} = $vhigh; + $hash->{READINGS}{"tempHigh"}{TIME} = $tn; + + #-- formats for output + $statef = "%5.2f ".$abbr; + $value1 = "temperature: ".sprintf($statef,$vval); + $value2 = sprintf($statef,$vval); + $hash->{ALARM} = 1; + + #-- Test for alarm condition + if( ($vval <= $vlow) && ( $vval >= $vhigh ) ){ + $value2 .= " ".$stateal.$stateah; + $value3 .= " ".$stateal.$stateah; + }elsif( $vval <= $vlow ){ + $value2 .= " ".$stateal; + $value3 .= " ".$stateal; + }elsif( $vval >= $vhigh ){ + $value2 .= " ".$stateah; + $value3 .= " ".$stateah; + } else { + $hash->{ALARM} = 0; + } + + #-- STATE + $hash->{STATE} = $value2; + #-- alarm + #$hash->{READINGS}{alarms}{VAL} = $value3; + #$hash->{READINGS}{alarms}{TIME} = $tn; + return $value1; +} + +######################################################################################## +# +# OWTEMP_Get - Implements GetFn function +# +# Parameter hash = hash of device addressed, a = argument array +# +######################################################################################## + +sub OWTEMP_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + my $value = undef; + my $ret = ""; + + #-- check syntax + return "OWTEMP: Get argument is missing @a" + if(int(@a) != 2); + + #-- check argument + return "OWTEMP: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + #-- get id + if($a[1] eq "id") { + $value = $hash->{ROM_ID}; + return "$a[0] $reading => $value"; + } + + #-- Get other values according to interface type + my $interface= $hash->{IODev}->{TYPE}; + + #-- get present + if($a[1] eq "present" ) { + #-- OWX interface + if( $interface eq "OWX" ){ + #-- hash of the busmaster + my $master = $hash->{IODev}; + $value = OWX_Verify($master,$hash->{ROM_ID}); + $hash->{PRESENT} = $value; + return "$a[0] $reading => $value"; + } else { + return "OWTEMP: Verification not yet implemented for interface $interface"; + } + } + + #-- get interval + if($reading eq "interval") { + $value = $hash->{INTERVAL}; + return "$a[0] $reading => $value"; + } + + #-- reset presence + $hash->{PRESENT} = 0; + + #-- OWX interface + if( $interface eq "OWX" ){ + #-- not different from getting all values .. + $ret = OWXTEMP_GetValues($hash); + #-- OWFS interface + }elsif( $interface eq "OWFS" ){ + $ret = OWFSTEMP_GetValues($hash); + #-- Unknown interface + }else{ + return "OWTEMP: Get with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWTEMP: Could not get values from device $name, return was $ret"; + } + $hash->{PRESENT} = 1; + OWTEMP_FormatValues($hash); + + #-- return the special reading + if ($reading eq "temperature") { + return "OWTEMP: $name.temperature => ". + $hash->{READINGS}{"temperature"}{VAL}; + } elsif ($reading eq "alarm") { + return "OWTEMP: $name.alarm => L ".$hash->{READINGS}{"tempLow"}{VAL}. + " H ".$hash->{READINGS}{"tempHigh"}{VAL}; + } + return undef; +} + +####################################################################################### +# +# OWTEMP_GetValues - Updates the readings from device +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWTEMP_GetValues($@) { + my $hash = shift; + + my $name = $hash->{NAME}; + my $value = ""; + my $ret = ""; + + #-- restart timer for updates + RemoveInternalTimer($hash); + InternalTimer(time()+$hash->{INTERVAL}, "OWTEMP_GetValues", $hash, 1); + + #-- reset presence + $hash->{PRESENT} = 0; + + #-- Get values according to interface type + my $interface= $hash->{IODev}->{TYPE}; + if( $interface eq "OWX" ){ + $ret = OWXTEMP_GetValues($hash); + }elsif( $interface eq "OWFS" ){ + $ret = OWFSTEMP_GetValues($hash); + }else{ + return "OWTEMP: GetValues with wrong IODev type $interface"; + } + + #-- process results + if( defined($ret) ){ + return "OWTEMP: Could not get values from device $name"; + } + $hash->{PRESENT} = 1; + $value=OWTEMP_FormatValues($hash); + #--logging + Log 5, $value; + $hash->{CHANGED}[0] = $value; + + DoTrigger($name, undef); + + return undef; +} + +####################################################################################### +# +# OWTEMP_Set - Set one value for device +# +# Parameter hash = hash of device addressed +# a = argument string +# +######################################################################################## + +sub OWTEMP_Set($@) { + my ($hash, @a) = @_; + + #-- for the selector: which values are possible + return join(" ", sort keys %sets) if(@a == 2); + #-- check syntax + return "OWTEMP: Set needs one parameter" + if(int(@a) != 3); + #-- check argument + return "OWTEMP: Set with unknown argument $a[1], choose one of ".join(",", sort keys %sets) + if(!defined($sets{$a[1]})); + + #-- define vars + my $key = $a[1]; + my $value = $a[2]; + my $ret = undef; + my $name = $hash->{NAME}; + my $model = $hash->{OW_MODEL}; + + #-- set new timer interval + if($key eq "interval") { + # check value + return "OWTEMP: Set with short interval, must be > 1" + if(int($value) < 1); + # update timer + $hash->{INTERVAL} = $value; + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "OWTEMP_GetValues", $hash, 1); + return undef; + } + + #-- set other values depending on interface type + my $interface = $hash->{IODev}->{TYPE}; + my $offset = $hash->{tempf}{offset}; + my $factor = $hash->{tempf}{factor}; + + #-- find upper and lower boundaries for given offset/factor + my $mmin = (-55+$offset)*$factor; + my $mmax = (125+$offset)*$factor; + return sprintf("OWTEMP: Set with wrong value $value for $key, range is [%3.1f,%3.1f]",$mmin,$mmax) + if($value < $mmin || $value > $mmax); + + #-- seems to be ok, put into the device + $a[2] = int($value/$factor-$offset); + + #-- OWX interface + if( $interface eq "OWX" ){ + $ret = OWXTEMP_SetValues($hash,@a); + return $ret + if(defined($ret)); + #-- OWFS interface + }elsif( $interface eq "OWFS" ){ + $ret = OWFSTEMP_SetValues($hash,@a); + return $ret + if(defined($ret)); + } else { + return "OWTEMP: Set with wrong IODev type $interface"; + } + OWTEMP_FormatValues($hash); + + Log 4, "OWTEMP: Set $hash->{NAME} $key $value"; + + return undef; +} + +######################################################################################## +# +# OWTEMP_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWTEMP_Undef ($) { + my ($hash) = @_; + + delete($modules{OWTEMP}{defptr}{$hash->{OW_ID}}); + RemoveInternalTimer($hash); + return undef; +} + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a 1-Wire bus connected +# via OWFS +# +# Prefix = OWFSTEMP +# +######################################################################################## +# +# OWFSTEMP_GetValues - Get reading from one device +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWFSTEMP_GetValues($) +{ + my ($hash) = @_; + + my $ret = OW::get("/uncached/".$hash->{OW_FAMILY}.".".$hash->{OW_ID}."/temperature"); + if( defined($ret) ) { + $hash->{PRESENT} = 1; + $owg_temp = $ret; + $owg_th = OW::get("/uncached/".$hash->{OW_FAMILY}.".".$hash->{OW_ID}."/temphigh"); + $owg_tl = OW::get("/uncached/".$hash->{OW_FAMILY}.".".$hash->{OW_ID}."/templow"); + } else { + $hash->{PRESENT} = 0; + $owg_temp = 0.0; + $owg_th = 0.0; + $owg_tl = 0.0; + } + + return undef; +} + +####################################################################################### +# +# OWFSTEMP_SetValues - Implements SetFn function +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## + +sub OWFSTEMP_SetValues($@) { + my ($hash, @a) = @_; + + #-- define vars + my $key = lc($a[1]); + my $value = $a[2]; + + return OW::put($hash->{OW_FAMILY}.".".$hash->{OW_ID}."/$key",$value); +} + +######################################################################################## +# +# The following subroutines in alphabetical order are only for a 1-Wire bus connected +# directly to the FHEM server +# +# Prefix = OWXTEMP +# +######################################################################################## +# +# OWXTEMP_GetValues - Get reading from one device +# +# Parameter hash = hash of device addressed +# +######################################################################################## + +sub OWXTEMP_GetValues($) { + + my ($hash) = @_; + + #-- For default, perform the conversion NOT now + my $con=1; + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- check, if the conversion has been called before - only on devices with real power + if( defined($attr{$hash->{IODev}->{NAME}}{buspower}) && ( $attr{$hash->{IODev}->{NAME}}{buspower} eq "real") ){ + $con=0; + } + + #-- if the conversion has not been called before + if( $con==1 ){ + OWX_Reset($master); + #-- issue the match ROM command \x55 and the start conversion command + my $select=sprintf("\x55%c%c%c%c%c%c%c%c\x44",@owx_ROM_ID); + if( OWX_Block($master,$select) eq 0 ){ + return "OWXTEMP: Device $owx_dev not accessible"; + } + #-- conversion needs some 950 ms - but we may also do it in shorter time ! + select(undef,undef,undef,1.0); + } + + #-- NOW ask the specific device + OWX_Reset($master); + #-- issue the match ROM command \x55 and the read scratchpad command \xBE + my $select=sprintf("\x55%c%c%c%c%c%c%c%c\xBE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + @owx_ROM_ID); + + my $res=OWX_Block($master,$select); + #-- process results + if( $res eq 0 ){ + return "OWXTEMP: Device $owx_dev not accessible in 2nd step"; + } + #my $res2 = "====> OWXTEMP Received "; + #for(my $i=0;$i<19;$i++){ + # my $j=int(ord(substr($res,$i,1))/16); + # my $k=ord(substr($res,$i,1))%16; + # $res2.=sprintf "0x%1x%1x ",$j,$k; + #} + #Log 1, $res2; + + #-- process results + my @data=split(//,$res); + #-- this must be different for the different device types + # family = 10 => DS1820, DS18S20 + if( $hash->{OW_FAMILY} eq "10" ) { + if ( (@data == 19) && (ord($data[17])>0) ){ + my $count_remain = ord($data[16]); + my $count_perc = ord($data[17]); + my $delta = -0.25 + ($count_perc - $count_remain)/$count_perc; + + my $lsb = ord($data[10]); + my $msb = 0; + my $sign = ord($data[11]) & 255; + + #-- 2's complement form = signed bytes + if( $sign == 0 ){ + $owg_temp = int($lsb/2) + $delta; + } else { + $owg_temp = 128-(int($lsb/2) + $delta); + } + $owg_th = ord($data[12]) > 127 ? 128-ord($data[12]) : ord($data[12]); + $owg_tl = ord($data[13]) > 127 ? 128-ord($data[13]) : ord($data[13]); + return undef; + } else { + return "OWXTEMP: Device $owx_dev returns invalid data"; + } + } elsif ( ($hash->{OW_FAMILY} eq "22") || ($hash->{OW_FAMILY} eq "28") ) { + if ( (@data == 19) && (ord($data[17])>0) ){ + + my $lsb = ord($data[10]); + my $msb = ord($data[11]) & 7; + my $sign = ord($data[11]) & 248; + + #-- 2's complement form = signed bytes + $owg_temp = $msb*16+ $lsb/16; + if( $sign !=0 ){ + $owg_temp = 128-$owg_temp; + } + $owg_th = ord($data[12]) > 127 ? 128-ord($data[12]) : ord($data[12]); + $owg_tl = ord($data[13]) > 127 ? 128-ord($data[13]) : ord($data[13]); + return undef; + } else { + return "OWXTEMP: Device $owx_dev returns invalid data"; + } + } else { + return "OWXTEMP: Unknown device family $hash->{OW_FAMILY}\n"; + } +} + +####################################################################################### +# +# OWXTEMP_SetValues - Implements SetFn function +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## + +sub OWXTEMP_SetValues($@) { + my ($hash, @a) = @_; + + my $name = $hash->{NAME}; + + #-- ID of the device + my $owx_dev = $hash->{ROM_ID}; + my $owx_rnf = substr($owx_dev,3,12); + my $owx_f = substr($owx_dev,0,2); + + #-- hash of the busmaster + my $master = $hash->{IODev}; + + my ($i,$j,$k); + + #-- 8 byte 1-Wire device address + my @owx_ROM_ID =(0,0,0,0 ,0,0,0,0); + #-- from search string to byte id + my $devs=$owx_dev; + $devs=~s/\.//g; + for($i=0;$i<8;$i++){ + $owx_ROM_ID[$i]=hex(substr($devs,2*$i,2)); + } + + #-- define vars + my $key = $a[1]; + my $value = $a[2]; + $owg_tl = $value if( $key eq "tempLow" ); + $owg_th = $value if( $key eq "tempHigh" ); + + #-- put into 2's complement formed (signed byte) + my $tlp = $owg_tl < 0 ? 128 - $owg_tl : $owg_tl; + my $thp = $owg_th < 0 ? 128 - $owg_th : $owg_th; + + OWX_Reset($master); + + #-- issue the match ROM command \x55 and the write scratchpad command \x4E, + # followed by the write EEPROM command \x48 + # + # so far writing the EEPROM does not work properly. + # 1. \x48 directly appended to the write scratchpad command => command ok, no effect on EEPROM + # 2. \x48 appended to match ROM => command not ok. + # 3. \x48 sent by WriteBytePower after match ROM => command ok, no effect on EEPROM + + my $select=sprintf("\x55%c%c%c%c%c%c%c%c\x4E%c%c\x48",@owx_ROM_ID,$thp,$tlp); + my $res=OWX_Block($master,$select); + + if( $res eq 0 ){ + return "OWXTEMP: Device $owx_dev not accessible"; + } + + #-- issue the match ROM command \x55 and the copy scratchpad command \x48 + #$select=sprintf("\x55%c%c%c%c%c%c%c%c",@owx_ROM_ID); + #$res=OWX_Block($hash,$select); + #$res=OWX_WriteBytePower($hash,"\x48"); + + #if( $res eq 0 ){ + # Log 3, "OWXTEMP_SetTemp: Device $romid not accessible in the second step"; + # return 0; + #} + + DoTrigger($name, undef) if($init_done); + return undef; +} + + + +1; diff --git a/contrib/contrib/1-Wire/Schaltplan_Aktives_1-Wire_Interface.png b/contrib/contrib/1-Wire/Schaltplan_Aktives_1-Wire_Interface.png new file mode 100644 index 0000000000000000000000000000000000000000..5b2bf4613eead669695a9dd7f0d65f6a91865cc3 GIT binary patch literal 19686 zcmeIacT|&W*FG4qfFi}Ch=PEqfFeb@bOiwg=^dm?kuJT10-_+G(z}942@nL4PC)5J zq_@y}C;;dxP*W&;S}f45{e3eP0RdkcdJt(e3qu#g!;>VTlkMJyW|VS z%AoqmU?@#fM$5+|TOucdr}ue?FCh^A&rY(#OU!-|9{Jq zMJ)B%xNGI{a^H8n&LZz~!cD2qwL{cI2%GanWLd9vuHZLIHoa0u61hdEy7J`lyt8^0 z*Af04*NuqKswUC=I@T@rMN1WC_Iv1Mow4rr!Q}&gf<*h_cUP+uh#roU?Bi~s6kIv0 z4XGu_lJW_iG5khWcd;rB;!`*iTtZ~~=L4w^OxJFY+tlh<_Y)(sevxX|;G7l9Bldhm zKX(WED-cnSa-Kwt8<1+>e{RaikhnY7(}IClK0Q(F<3tolSFco)7~b1%7!@S`Nc=J7 z2~S4_@wGr@lW5wl0&e%IPnrQ=E}j@*A-<$6kQ`}OpQ@QFTFqmmR`aTn=G{8I*>VVi zE{!o0wN*CUVar%#`sC?RX^Tbf?-*CBpHOaq7H zCs7!~7rGuRWq3cgNP~?;N!7_hDy|Yqvk~EZZA6KT6FiOE6w2Z*e_{GQ}1pU{JY(0n{4l z+rkIFi}_a1avubo&iL|$lu1=hjfCRr_H3d(r*?tS!S3cA&0J-{uS%(Q%^zRpbk1+~ zGS24xc=P7X-+R=>SS7@y(DlrWrFKJgeTG|l_hW$T2pR%7!bs~#P-ekGv>N{+J!m=~21 zy`BYWRu>I3$51oT2aJmCtYu_OuKEMT`vdDPC;BxCs}d83r6|R|+YA;4Gk7+Qj8thL zHAUQ3oSmHRu(PWRHbgKUEtib-Ro=aSHu1oTbRZ)>;OqF!8{^>XHf@HA)-A|s`7C$VXHJ|x zMT1IhN&*>&7>@YmP7xtq^RzK(nuiG=6>6IwOyN|;s`r8_mO zzH6i5bx|xY*+L&jD#VXB{-^fk-fEcenzq{G*S>)Ax6`tCljdviV|B_%?9cfMrI_fFuS z#Z6ydUpIam?J4<@qF_~i@2x=#adC0mq3>_Q@~$Nk*Z2g%cXKW@f1&X)kbD!*YcV@JtNZQs^X{*<0|iV%LOXdSrFv%(Xh1CJZlQ$+ zaZ!QIj_l|wEjd0Su$Vk%a#fz2DSn4L^dhdLOfl>_CJUhk6|T!agUARuOuW2B5LSYM zf({Q4WAaZTuvEwA09Apqr7#n^x4+A1qoUHeHq{tJC7k@*F?RTe?eTwIU0p0nDM_iR zekj`CsS*-yD;Op5ojS*8#QkKcn!<3^3p$)nOIN2Ol>}^uoag_1_VDnqqLWsEkAAho z929=>dQ$gb^T7DlqLbd07SGAItT7yVu8Vzn0|Ntsqwc@DV%086t3=6!(N?WB(6K~j zx6%G};lh|Vz8&7cqL83n&Vb00BGv9wvAQnr?(Uu{?&Ij_$l$Zl`Q{C&QxVMg@P6>} zO00RY#Q8b#wMAvk3L4L&iV}`hiO=BbN1k+ZBk)+idG`+U z!&VRGX65$*!a>5R(3Oag4b#SMSp77)X?-8xA!t7;Dj@K~_h8r9*qBmczo1O#!Gqd{ zFgo03mkpd6>;C+_uyULPR$0@isHQ;7IMGo+r{pph7mBNTJFQ2t%D}#!7KUnt$0C%q zs0wn^rys`T=0ZL+aj|T9#zRd*lbF~Z$8CDJzhhR-M|pXE(=$}D?t`LA%jLkL%EA

}(q?AtN))To7c`cKpd+K=ec#Hj~8Iw1W8+(ODln4jE8%8?8!XM1mr<;*lC`CCr z>=^o}(5&_Allb+CV8$)n*cirUdv!uSmXiZ-#u1^{l+oXRShWy>ewq=fcILID)O2;z zuUJYlrj4n?f!PXg$oD=L79Twsg-8F=H}*%p)6DN01`?~LAfgSBJMAHe0$x4 zB<4zs;B<8VimtzX~*Ao;}<`LX!&)O{nK})A_*UCXy)Mxfk1tr ziJ4nC8~yB19aUaib4;MavP|mfiXEh>(zwpFe-H4T=j14c6CZc;II>s@?|#NU5lN*j~Q!(`9iG zjbMKXApmR8Y1~mSmap5dQk@>ds5gq1KH?eHmL@M!90`%@EwCRGx3|Z!am+gHZ?An- zNK6s)`k{p~&?wjJ{;EJ&9%c<(-Pp+0jYce8EFoP?$+gz0IvT0)@W#)swFi5PPv;%j z&x|w`xvFfjv)naz`fZLriH^eG=6=cD$4L@h3~&mEKJq23jL4*a`PR*9*GGDBCgFA8U92k#2tTV0e8_`dpOB#6 zhUI0T^Qt4Mr?ld48ag$^YOjUMlPb}zIIBLmySa$Rtk_Q0D1 z=t4T@oj=a_+-cJcDFG(j!3n}ZqEk)ItRFZRF78iR< zP+DU+VLzG#TDHJzrXyN8CEg0nA_1=@e|6rG<;{QQ?KiUr(VA+U7Y?egwN z{f}O#zy_++uk!NR+HxWId|A8UvhtbQ&dx46)t@==vV`w`o?Z#I7im%U5rJVP)fTXw zY7R3@*;@}SG8FCKDpMly8Ol(J#!bW@We|z@Atu(x~lojU4MW7wzjsc ztgQKYOB$Nkegm((($ay-yog&~@4l>4o(jXzcr4ACMtF$!Rp{W37PtMZ($;fv*>!Bk zRe>L?p`TuAa;nev`b4`>qKM$i)lK&vi&P&0kM-%aw6ulpOhMbBo=P#hq~u&rg>vF+ zr-)iFnd!I62}geCv`&V3>RC~f*zK03_6^LFPv`6?_Tm)ab(Jf*U95JKy?viOgPcxa z2~&oL5QeJW#U_imwGBg>W?*1Q^*i*?uy~fpv0Sua5G=arm2)EFFy%1aTQ;7clqg{P z>ovJ>vR#co5nO@KK7vm0_x`(fEcFS*R^a0mfjVW&N21Y}6KIDloKh?4JdPOlaqlLs zaBWvAHSzIGkTx94@x*PfAMqI4nt;-gytQ#!xQXKhqO) ziFFYKBA<6ggC|{@*be>H!t`l_UC5Db?|Y9|Fv3v8?(Mn}HFIjlGoJWBDySy@o@$1i zec9<}LVkz)uwf;3rXy8{V>AbaEJBguB6+l#>Iowf6esS-LpFJKi_fNIFv^=nXiBbu z*EV$r*L}3b6DfBczgiH(aZg-i`u3vyXB=8~jkv95e(l`xGad+ab`;2v>|%QO8} z&!hQ0!=!`jHN4}4t{p5VS+(aScXoDIBAZ7>MkEi$ST{V6^P8n*R#6ciG9`n@^oOXZ zs8~baveMGh>6OUB+YeD-X*L*fXNuP?&%5EiU` z*t5zPH(EtRM5LL07GXn9gw)KI((IH?3!>`S6GiV0j__Y8%=rT)1_Ud3@tbL5&v89c{O9IPm!I3YIz{H~31*`@jul%LQL%WV4XEy( zR(qETgr1jtc9^u#|3``R#pKW#lCWR#ixvz$;`3uYW6ia<^-}fMXQ~vNn>(`S)H~4G zM`eb;<4bHE%x)rF$RaV;kNb*q|MV!Q7K`7WQ@dUvZGNsJuFoeyx;vUG&7jINT{hy! zJoAd+hYufKym&EE?jryAu?qS}vCx?_ZhAf@E-EinR9J0?5#si#vN`FekJSMa!hqj%PEQ7PT?6+8jn|2 zR|(8e;m?RWyCT)znVmP4Aa`5^4DiF()n{(d2r)U z#KfVs*2(ktm7#J?339cob@&oYfYVgU8|1_~zXjRv>h<;D1n4`00D)_XMuLuKr-Px!+3kSc#=R`AygSl!H4$7Tul;Bx5Cc_SgssK?4xm@= zC&OS|#1gkK?v7LcRf?tlkyvP6zw|5HA$rVvaHclAtgP%{+1@Yo&6~UNPr5UeP%}u} za7oAT@NjD@G6rR>0-GMr3gEwujg9ZF90>N%h`7P>xSBW!#Rq@wtNzZcJK3OJaTOzb z{rdIF%F0o{qXVe9dL6H6e=`Z9GJykE?9r)PU^B=A_;Lq7r&mFRxYS4*lZS_(vh$Gh z6K(GHyNI^7Cjqsq;{lYT$Ok{fE2gC$L>zc8eK~xFo;~0kshmb`C(u}Z&)OH+felj; zT0u2gSA2nfM8sUb)oS;?TD^30dCbh>iWlKFR0t*ZWBJMJ2yGf7vH}T-;ro>QCOq+@ z?o(kxPP3S5pW*uYnU&LU8py5q=M_7X%Sh(7HnCnjN7FSiCoFKOK8?T8GxqxVv;6g@)sQ^tHx}#mN(20)V%-i~b(NSGW3_i_o4*SrYqwDk3#dcH*l=DBWSd!ayZ#eUDyr*RD%;aGc61ZMyq}HUX#qdC?pvr&|9<%^-2B2PXa9Gtc z^vPJ!VtipSdduTtmXO`~p`x>_g$M!pOyu~>)Y;%y;dnYh+p+Hgi}W~pgnrv6QKglZZvRmVzCa*{t`_X5WObrU3H4hbAw{x`|AUoYtdUC=?ut3|bpky&fkuGORMW4v!!ij=r^5_Oh@*olpA-0uxR z#@CLbxgtMxTDpoCsgZV~JkY-Fd(k#uM}2eN^s-d7vuyY=c@?Ywx7r=cg)I(lKKxM8tgU*IIYgK1GSvFu}#E^U*4BWFlLN)}+}`sgCa$ zbuaFgO}VG?M8xXvZ#fM&E?w?^u%I0I7F)r&r0Z+5Q~q)>=SBXmMl!#3-)wuL*Cv)Z zaFsw%l9O8}5YQFh%2oFua|=Ptf@RxeJlbxAvRFuB=kR-60c zWqqUp)v!E&3LvnrohN~JCNAtCjVKh6C8i@y8CJ3qRkUK5x03NsE# zM6p!6_AfN8L6}pCL{-&mwNeC~Y)xT8jrq$?*>svsgXzifzkK*S_hYBh86t9wQ*)(B zKM6p@%Gw_js__Zc3)H*-FgQ6)WJJ)JAivrzg#Aj$TCR5{Lih*WePzZk%&PCLTv?0+ zy=C)I&$D~ELeK@~NHJNsIENM6xWMY1@Mk2w zf=Y7n6UU|chU=3TtvU`5_QpKs=)+`8?_O@PDd64vAqWLUe+t2YJ zT2HIb4CUnF%2BFz*XgKDH~$Jk?z+(++>Aq>W`*abRb;dJj8jDrSDUu`L_<)}6*Bad zErp&E6)ih`ZbpSeCY{Ak(f@Tf`4E0B`LNFAUd2KV3->(@%C&sBj=moDpX*n|DjjC; zqy|!r(77=#iETr!@=DfrWHA2O9QgZ8vY4jlNt?5eTIKha+APGay@&RZ3O44sRFh9@ zh45#)L(%$}UBkR#cy;5$eAX`&g{g!ex+7Md6Vg_}E8kZ9cCcI@(Pt4@p4WeDN1(1m zdE;B4Osvwcy71}()Dn06>}+goicyOzlMFrA=Ox)754d~~-*S9?!rk}*YT zOD}$!VKOL40yIo}?zLlAG}Uzge}2FB=Kz>p4mqbqu0<~WIx$GFT{hIukS6Ec?u*@N z#*c8elj)O6M2lxW$V7bEk6T~w=c1vZu_qn<k|_y+Quk-f1HVQtcm{HcA8jF(*K!2@yFj z_?+;CL+Xe2Zpds9SK5j6^b_g5%f`0B|Ics#BGgvsn0SZQLS(9D&&4r21se#aA@DISz9?KOt;A31; z=A(|waWbKNoUvu+3QBe6Aa1m6vDF!gaMy|056+;sPI&em;-( zeRWfiujC+M3v@O8USO*45Xa;#4t}3&h@_L<35O`|aTIt6_%*t_^k;nvhn&h2PoMgt zUADnRDJxSP$r{N|0_p#D~>wtk#UkiUp-UKbTXmeNX4Vcb2HNul{61 zCE#*0O0EqJZwV~nLvbt7(n}A9&y#5nm%>OD$Tz)foSvs z;J)EmMK$@Yk-^E?Id@tIt8U`*x~1tToZaJ5^2h6e4n6m(dnPg{cYUu_OgoaPU1SPO zPoI+Ks@kX}Vf+^>64%8v1%df*F90wkj*7Q?XY1Zx0Ir(NqbKdu3B-yX6#BgBo!hNk zXz$er(WdV`ge+WAP}{aYQ9D`X7r#$C-cgG)NR5w=kBhq%822}1vfEjfFCGN06Dm>| zB5mFe`?14V^y-E!!r%KOyH_8Cai_W8z_6yT|I#v_VV}#bW1~cQxkur$Xlb;~LefO{ zSAV6;g#9JSu2cBs=T^oL$X?7$OrHVRTleLjmwb{cE+#69k<1`QtXN>Ke|G}K=_;@J zJB&DhPTp>dUH$vtq>{|W%`rs$YyjTV*)L zqvm*fehB%vYQvs!H#9t)vpAE-VsoLJ_2%*8%k&|TjRwA-v%dXK7GV?x4QL^tf@`VT zxRNRG*4;~@{VPKMHnADnA#&f2JeFS}Z|zU?iL- zrpT;|bP>0L03W?obT5fD}HOh)HX#5h-s~B+Nc)z(a%}IA|fh|dV zpiE^>Ge57K58KsJ+*>Jy&mS!pPCIymazd|+mR9_JTyn6#rF(}O(NcGn9r@N%6bspG zg|2*$d3)iiOSjjP5?5*eysZ5V(CR00Cb0F1H1?N9`Ecx)>Mz|PN3aE*XGeAlEwsc- zhnFwz-fj#y|22FnG?7HFB5JSZ)w@&1`oGR91mUiFH`aHWHGg21iTqX}>jnbbxUX!@ zt1#7zkyL(_kH2f%9l4D$NAdF8rf9uzvQSumc0S(vW0x`iEja})eLO?XsD^aJ#hoe2 znmfq`=Ox>gg|vLi#@9}1A84UOBQV>ew;;gaTqJ_)?JX%DvG>Nv$mZw7893+K~g42W2ld z=M|2WcMj3R1mc3l(QZGQR>0;S0OOWaRmZj3$Qw$WP1^5WIIKrMd&m>V#L;E7Uex_! zQt_uI({f8%?F3`W`!fgv7jcXj9Y|fkB4+iD;$JS!;Irn=4~nh7344cjukUvHHT^_S?CQ<~!wkii_eL@B~YQ45E_DO;dD48Kfc*7|P@H&BxDI zpfe8j1JM45eTOAIQEDEbYh$%Lb3SQcTmO~3pWJ*?bM|(&sSUY zE%Q2CT%bB@-}R{NcgzTc4HZ%A*DGhqb=y%z$Xej=Gp<#V6ZleI&YCwXHK zhysQE_YjNXKopZ`$#+}yp5%WQe9gYWh(?%@}0VDCD0=w)WiiR-Wd^bAQN-$-}y=c|ES)e{`KzD|yfbuxb0&OnlwdD+ig-W^%b;;=!Nhr=-t-yRRBsi^@dFD)&t zQ*o(A^Sh&M@%N4{!*^)WEFa~O?)BOjk=M&ql5Gn@jS1021vlWvEI>F_c=E*A@w&{3 zjQDP9gf_9C=knmcPAG zKKTDW5*l;|-&QXjaPlSR@^7)ab@KcV#g>i^!T2Y@mG*Id5m=i#+tkP*`F|1bY7SOF zfaXH)J8Eji6q6At&vE~=a9f2+0vpC4HZ-JxC?J9LtwCW%M(D3@=aJqg_KgNS@(5f) zA{KnKe*0^UATp~TUfeVu)FrX?R^I;?4v|nqF}VCM98#iNgFQ{jV6*%eV&H^K!4gK6 z(dRoP^_-Lp#>@!H5K0B|-iU4_qdcE|)^I$R8B2;{)Ki$JtyfS#0g5;l-9VkNN}}r$ z1_m_7KP5Ok@j{D zcJ|yHi`(DiEUhesA{HFoUQk+{|9;rBo3mvj7;Yf6Yc&T2?I}c!%<<=cZUvHC3S9#O zLg|GU02hXMzO?Z6A+amb#Q@xx$e};;U6Q{swaxFrrP%RUM{#DB zyA4A#pNPSZCUMldQ+oIUzyn;RwBn_OPsf>LmM=+4*#|@ZswPPQK0$3A6t`g;!b$pz14jpIETN)8N&y;zoN7ZSDv0I1R?qnezXL)X+MgFmzV-! zBs+(A!@(U83zQ2L?yLI$RuQafbiUI|O3D=Y`OhjgU(1x(6hyhRO#5O7dwe)vaI_RJ z5-*VeH*qDb0U+Ki$g6M;SB{y@i&|fq?+CyxiSSo;>*_BcnY@s0cz#o^G*j z=_;ktm4g+m8&WYh1beX8mNT&&>}EA^+tKh@vw5a$vcZtW?WvPlZ$C<;wlJPy0&Ja{ zYM8Air%;pNQ z>A{(qbJnP;KA=-U)F)*ENo}aa-tg&D_8kRcmn$%HBc(6W0bxrV?K?eq@UqxCJ5o|o z5?~Kp$JE3`Gymye>5D1T#_%&t0(O{ViP9p)qXFYMvld;VP=Yf^?Q9?9V7WV6Jc61T zYew~Uy{KDwmXBe-J(mkiK5E5#v&x^a!XV?5zwE?G!D(IC7e|0n z!>lh?$I4#~j=I2dq>^}ZYwOJWNb$3`c+N4!;Y%xVQV-WIb?@FbFz6OJzB^5UT6^{4V1)FL}oPQYM@SV%5qD z8$c<3%vW1jSXlR~vNE%>S{Lg|NT5N~8TqGk!;VW3VUaE?)d@KUNJ^N;nOhxG{8t zJ!!!m(XX{u=Yn20`Fv=WxM|IokXe};;wdV(`?DEVz6#JU)Tq<0Ua89e-q@C`NOSpe zR`p_<_9Y~_q}iX9okQD++x>m8|{@X1F?+-S{>##F)>1cB3|>KgCSMUR0-8E08TLXZSX#@Js7nh^mX^q9kp+R<#=q%Pj6S_kyfu7 z7NxGMtJ@q44&2%0)v5``t(}6ON~gALhMd5SV{O6zc%TYSyctUlkDKBE{{$}ia0Rzu z=sO3u0>*;5t8v}^NZhMaWRR_AmX`LRx+i72D819+cgPE8S+B${o44vTIqk&@7kFPx z*3nim)-arhLBU!9`JllliTAD9BYI2}L#kbnbt;(VyLW+Ee7DZZr9S4RY`jvvc|+kt zKC7{H(^fogT5Fxi2K)o>9e9j)zRg5Ru>=PO#u1QZ%dncM9vvWiLn+Q)rCUP*_dWY# z;Ya8%oKpv?+E>Fv|EDK%hHJ~_@S@9M74F8T^)#NehtxDJ?c28l{DX!Bea4(s>Qj5^ zC061v(W@tq8D1*`tD`PDuQ7LtaUZ6KV2FBasF1rusu>h2hR;Hjt!(z1_u395IQm}O zd2x#UrP6ub_6>&ak))B7-N|hKFNk{SiNk9Mge$1Z$HklY3gO%To{(-$(MS7=!S;j$ z8ZE)R6Z|Y9nn5nN>t=qlVA&Wc(2HingSKgP8`{j8DkjwMOW2NjpR6iYzZR1r*diFU zYKTd-x7cCr1O*8Y*Wn^iK#1Q0F6ihnTB)tIy4q*eR_$B%Gv0%HKHkdSmR4D9jzow8 z8i1FsU>+i);f(TpWWMu`#q`l46;oZok6&A2Xr&PyP=DNWHh8$0@}%Cer01%_XIP&& zmRLlxD6Frpx)Vm+>@lS(KxeZhBS)#!B`U?HIwo?BjS*^Lox1eA@$T&+qin+~de9K_MD!e7!F`+1D2O}2je>lwxqty=x!2}vb=6^)M9a4(icK}NMg^9+&Q-4DwJn6fas)iB@YtjMp!WmJO%b1B4-6|BmB7fk9 zHNw@-`V|1RE-!HZHn^*QeXEpIyeq8Ne`Z|iqx4InJ zc3;?PZ#@rXe(I@=d{D%YSS)Pq!fbyLqKC`Of-y+d45Hd2I?BogFQ)VvYtNkFrv0x; zcrYSXj_Vl4!MTrA{P!qL(}Y?Kc~5V6Q!PB$;*x@Q|BJ5?z4@a@`i*P7Ri5o$1rN1i z)L28lCx!Sv#gz@2w{s=?u^s`roit=>rJGF0{k07t|`0ojx90nv$ z-{sk4D6(Jtak*GGPq@fxd8k--8I@XaMcjLPgmLn*qN0ExA{TfSGFoUcsEPLnr)I|w zFIUUO&rk~U3$M9ujtV;?H-->3A=e4gvt!K>o}xMHnOVHZcQWb6E}*038}}$EiYz)9 zxJ?4f;?r(1(;@mKYYKA-Cq^Sg&eI&Bw;`T#xw@}GExqfxS?sEUFj4?)G_?DxPpo4( zyGsXbHL+ZVoMp0A^7q_Vjqp9BvyWR;-cvu+OLU*V*MYV#-@>g-sG^zebisp@xw-(9#mW|^oAp^O^jn9biWYWkJ1VU7 zkA@~tT*ZFP;FT*zp73YOYF3kZxpxxmHHoMS97HYjUaK4U|5!I!PY9Aw?07c~EfvET zS`Rq2dqJtC3ZR$%UlakbAGBx5j}F$*k252`epOiR&Q((Q`irs3@jHmz3qXqc=a`&q zZGjGx&w0TOB=xDZ9y!2?tR02oR-Xq$P6d%P$4cwi?ev8_LX!khvwOL=keWY<;s_js zA3JVlilu6j9bX5OJ2R)Ah1KUBnapDG>PmVzxEsR2z>VEU&O(&zBdH}4H-&Dp+GzCy!_JI%kc{hu&urM224tQ#hX$zR1p|=$k zdxnSWu`Px)Sqjj|TM!OR>nRcvzs+7PZ0quH$#+txPu14y>h_}Hk#1qhzj|w8FjET)@v*UBS*r$I1Qx70_?)L_LPXB(M$Fs@xWu}; zyW8tABihU02J11p>%27Z4F&|&A+_WZ7>a;FfU$z@XuE2+e{pkdZSC{t&uU+vybTR) zZEsKJHf;b&lRa<%ob+Xb!HmA!&^^Z>>}(0b1RYQyK!kv8DDvaSo^&W^_-MnTx8+V~ z-mf9{ZxIIj?J?$Ml<=Lbd;9aJB)%2COuyVY8^j9IwQePec(<5t4P)a8s0Y&1(yF0x z4&KF7x3SRu>({SM5N#z7{Fv%G(`8|b1F3+I7=*eLyqK(kanRCtBSLf?jKncZ4$#Yz z!D&pPZC|<8FhV46wt}|cPRhQ@IE3zAEtRl^t-SVR2^S7?9=y~MG**2w7N z;{$y+36YWD7+70d=*jLDtFG~nhGy(fh2Xw-!}i>cY<7l@mRGNc@$%rLhwc+3(IS-g zoSdA{oZ;r`%9)7-qvjp0eCOpMFa;v9_f_odR-iFdj$Z_r5GY;(sc5(i&QVYtLKjRR z6^yi_wN0TVuSj1C~RxJXEX*Y{{7|BnrGlMy$xj~Om_BXlR08PyUq1vWoZZPko`R<1*Q z>JfXUQ%wjQDPbLPft6zF!YK@ zlKS}_K~bi9LmS#}`3X8q$%<}nru>WVfC}l!QUPsR!|jJvuN53VxHYiH&TK$iVQ#P0 z!T!Fgs;cM4JomGv_bBwCF6oQIeUG(apU2=E;)w@>9GJn95?GBbSoxm#XOg4=wMOC5 z{&~=j?EegW=PzHr%y=Ctr^0G;bYDUH*_%LkYvDQ{= zxN$g4)aa29>PLPJ@2)`ao@I&M$R*CF+Z)|VgoW<8x2!4d6G1dRitweMe(w_7yl?Q% zL<>kie3->}f(W7KO5j3*wzR#oV`gp+Es?N@NG`DOOYAN2CM#xkTAn?7mcf0TNLE%B z2-$r73SFclf(`n2Gl$SO5k2}aH?v1vWoz(tdU-5}AOEK1z2Pc5;+E3Ghk?gwfj|Yv zL)3e^EPQebtE{s)_6TzwI=3}_{?S_)#!>tRT*xaQZQEF5Y@QmQQ0epU53{-aKb;^}@D+c53S6?8DOvgMc>u0q|u&i+26~0Z;rB z?1>dF{qirpRzMhzkB>V{)}4`+Z9HxpsBq1LW^8D7g~PfJ(dd|?gAOSPi~-8jIBhPNPG7FL#eQ)I5S+ReoI&t|^Po()D&Z?CA~M|F?F2oG@GH5;kIze96?Io8Rav$_c6!6X*=uG^ zeIvnXqVMP;?L>zs9?hVQU3zG1ls~sJs@w`qzW!|Y+*XnzBO{B7oU#vq#(`Ug?Of`6 z;0Y6X+;SAXt{e^}=2<3S%0og!+s9Dh^djaK7Gz8ezS|Q(-;gpv3S5Kq@dHvTux&6n zD#>RS7L;@XPe>uiM?jk+3_g2GO7cx84VUKBaPmJJQnEv}6Q8i)ulKo?aC2D^223O3 z>0)iJ`RY}Ddlr!3Q>?58`84ZQBZJqI%rY-Dy4UxBq2c(wKg{|3|geNx3?eQ{Eil9XITOr92{=3vpYLFmcN*K zt(b}b+ZC%C{HC(13aRq=pUo=&YU;dt^(yq4dZPqp{WFfYV1B`aVzl!6SAdN)KJ{A3 zQy4$K#vum-~o#3X&vPDq*ZTH(UbQH`i!yeAZWIF;3Dxsu{e5*1Je z_31yWEC<5@v>&=yzBB@ke~mD9SNMfXmG7eJ#SDNe&Qtg04}ezv!_;f2lIxfHMbq?D=u!Bf6u!Xar<7@eHR|AoP*FB#@s zV1iC&Sx?X1k>QN3o2;x5R(D}f>>b(T>?G4XX%G~t7nck}+0(A?zORmYddm3xnVGMH zm(`J<^b;@jrb#cSML$#t&C*EJd>Q&lTJnJrOQ2=~b(l(w!lfq5&PQJcpLoy&i<}!u z7I7Ratn4UCa1x!{ERNhV6-{>Q3y!TJzSRrk1qB!UOof9s7p0+Lcz1utY5%jc@Btif zrBw0y#*MA5uiXoi=@1fDVmnHo!E`~ozCuq=&%h8qC&9y`x*sKBb$A4q`h%bA*xTFl z+uuHKm=0m8E~2c(uyOliM)$(EZ{N11g_FZkE{9fdxzW*4bvA10&OHKdDtYaKT;`qF zRs~P?Wl)NtL)rmY*e5KJ?dC__H*#s|=%#d}ReIp>TRfWvWdrEpgZ&+RCz|n1bn~;b zZkH%Nw$7}KR(Z{~#lsM%9m`UEvHKxnY9!6(MIbq+?0R;kz_!${_7?KxIb32XK*;s> z_VR(baUg`}lGf7d2SE%EAS)&&=BLY<^jC|QB|f2{6%82o1?Sz73ODGBWq;~%SKJ-^ z(10@`<-wOe&QQ%(XXDIU$d@mLoeAAJvx2QHEv_q=!9RcQ-o2~5dySu;Us{IY>eVMV z|9U|&t1xnfvTJZ~XW~~dz!IPR?H65pN*?h~SSX59!;?CyAEU?!=gyt0Gc<2DoE(!g zB6=bvkVwzSh|0>kOi!PemDM!k2(d_>C6b(+940UiiM+$bh5P#vF-E_0r_y`J5ilr# zhhadyt+8Arr%$V9_l)%_WoVZ7}8D31tyeuR@`?qUAUYDod-Y55aB^&bVQa#V$(2wt)kY(ZL$o{It@Y8?B?E z(c9PO3K|^v$7v+KWn?fPQT6PfnD-N2@5+#mY@P-@k7tDKi0?^Wy9Tky#>xs>!4(t~ z;Fz$92WJOFSptE%yu1tp>=s)?-6%kO%`7Z*1P2qa->_|3&5J-tn3t$(`jhWWidlyPP(00;Kee>V^$$%Xns0B! zu7GSqLdj6+x;&I?SnMR=cZ4`u0!U=LJphU(pS>+O5Ex9-qnmb^((@>dl4s2yf4Z!M zL~I4;UeGG-GjMU){IYo|bX#iHFs-kz4>UaJ>9KNmzlY{D*3mh(*QDRFva=fn=mDM` zu&-4^?>=OS?cATyB>1UkMT69LG#{#{gA0W+=y{&}y@d$Q(WS-3M9=#ZA0)((P2AAr z32;x@7r(VUymO!yfm>M#H_SCeSAz!E)U+1*^Fc@(lOIm2auB*a;W0Gi30-5Bnu)wO z#u&hudCzNW33pV9=;p~|)P$V@8}lFozvIHf!g98MDDl+36y8GEVf_C7*iG0&dtRFO zq-^w=(OP2CDO?`1N!M$i6Ciu?!tH&QqJ6IPWv=``;aRyld>Sm#p~3H|Ct5m@8OGK@t<45FG-6U`k7gsX!n|x(6Kh4-Mp_1mUcC|#J0i`_&o?8F--+tq zdEn!lqHWps%&vyLYwz+xs3j7bGQ_*pT)*78<#l%XOo|!@emRi-m2y4!vtP^WHuwwg zo+0AH|L(vphd(|0?Dr5nwXr^6g#Y=O;vE8bVts^w0iOKcqiVumee~x4e)a$J!}RS5 zg9hf~$0i#MV8(`Li*;%ht_h4^yn7O_&jeoiL2q@svy4vxpU?k0_n)6vz<0$D*0+=s z{p{HuxgOJQtSXQEY(&9I_mhb2kKwqShI#JzB>G4nNbi~!CnY5W(n4wD9YK?@F`jF`dt=FOhRzOqXWir!vV^(|%yY!io$Fdj$V3 zDZk79YG>$Fsa_*EJCjb$FjjkeJBQXQku@-(+rlA*CXElwjwp*Q8=et;L6nv%u<=ge z(AB(d;4U*=9tT(T(I3$jANux0f+Bqg3l-JvY+uX9=3sLm*>osP&}CoK$jFF_it6fT zj%>m+It)JwRL%VhXzkMGP`dB#cmX&sEiJ9f?s)Ig(ml8k1qFrR;9zxi^#JUU%(H_9 zew%rokW6Zv;8E4iDrssQQ`4>XcF}l!c?|OCj4J%3jUA>pE7|lv-*?P3Pcv_{f=gRK zDoGOVNyUy)i;q)^iW1<`8aOjCG4Z}W3y+LEoU1LXudfdeA5av&{a|8pwmZRa^>crw zD!M|%b-DI2|EEu%_My`CZ`E#u;)sU;kP<_hiKW+j{6POSY zRP!^vc9`oZQF19n8~*{LOBksjpOBCcE34hsV5+&f`N6@#(a}*TKGW%?OL&G@95a`geScQG(pY%@|qeUhxIS3_|a0?YMY+7MHhH#TTudr>La*x0P5K<*mWMb*sEVa+2DaTIVr&MqzvgVIAFOOy{5$Z>+} zbeZSWAV1!9N0Pk=c&nxc1>1FRvIHzDD~$)kh453*g{(T%i++!SibAwmFLxhf5HJ@z zj;KI}X&)5F=WR0iI+F za{?AsMkta`gLqu*Qf3`34Bux?hFH<&powdgXw#6zC&$LB8pvLj$|rN_H8}0uT%Hhd z+lCCAL_0p)YJv!KHM5B0dkhl*TRJJ>r6J9j$Q z4-G%=y`DNY-B^mezWX#3Xx^NA6gHkj_`5N|Rogx(H}pfuXh3FMTwFmx0c>r5fB#DR zx2w3MdcRc}4JU1@)`)5%pfhbP2rWi+0KLItQ0a= zUYqM`D2Y1>h7es8dy3B@ z)rQgW){JEW4Td9KpRiS~2O^(6`97_AM5eGP;iJu2R_f1+Ec*ga@TE*+ENv6*zS$_- zJEgYGTJECfg?KYbvv zB{BLwPP}VVVYa?^E?eJHVT&lff>C4+#p~09nW=%kc#~0tIYoVi#xQ@+xby9zqD#VV z7ZRN}76-=jMJG44>5KZ&t)eZ)W!;OekuAyRthQo#zO4+}rfSH)M0VmPFA+C1%Dizd z8B{!Uw=gukCPKuuTTI+HMxOR@#mgL052RIUSQTGjNpiEUJ}dBHw=~d0p?FszeI?_u ze10Bu()@$*z2k=n>5RHX=T#o+|Qt?tgNi3XKiD%81}0AXLq-xPu1bw?KQj( zuo#*mUz#YatRku)r?~z)O%8-KWoNoNZ z2!`(Jt~YnCJf^5zBi&})Us{)aRZDV^kK_7nP!L`)P%x?*fSo*LZ6$AgzBIcuR=hRj zd$T{@+gll)<5pQGJI5S?Lip?%wkUB7Nv5QP=d9cDBX=G2njV&Hx1M4$Q)`lDwPVp& z=Xv#~6Z0L#lImLOYVvAOJ?A@};c(Ayr}-y0>9-e&tMS2^!RlpN15)YsbLTZTkI1rp z|G<}`nzKhq>?lLY$;01erGv6r_Pq16?y^T}4EW|ht9V=vzo`*1h)O}Zm)hDyDk@fm zi5D{Rv=no=t9};kk@XdRtpQ99b)ZQ_{SDaflQ-ZSKE_AHW{S_h)9+UY(T$vzCVNsa3W^Ou)`;FJ)pOJR)fq?;DV!i&; z*@~4>acrMv$#?`0Y?C)Y5R9 z01b#L5X>vJ)BbtkEs{SrAzXS;*Rr;S)_c{T~(>LFv9b@95S~%wuDDV|!R!zw7MRw6|`54$lw&dA|~JZeKjBUE-wuJPNnXP%Xn7Cs>RQ z5B&jRuka^-|2J>mOciT69xgTmkYGJinbTra^}8}+QIMXjM%1#NEE~*YcXzk2@0|b} zoBc|ApjCn@%ZOgIT1*^YtUH$*eJ@7vn`2+(MCXMOwJVv1wX2ATIesuYZ?tKk4OvBe zrdc>IPsC2n6`RT?Eg>Rpqxm2y#Y()mO~1aEuPpMUf8vb#3o&7k85DX7@d^g1w;?}| zVbf;@HLdwRD@#KL=GWt-s`5OvjiD}8Do@Kh`}$;QudlBqEhmfFI5@H!i&a!rU&trH z7CjFq+~0Qgh13fVEG##DKeH>szj_SF58zLXW7d7Zee$#C_Fs;jp&`edS`d{7C=d!=aYia z_D}l{arbG*FVpQXZppQDbjWxe)>k@$+}+*1uVCM~cV|&udl`d^wbidl@daPfQLGnL z8PG&1q|AH2D&X`th6b}4Np#VrN|Ki2Ki2-Hk*0BXBa9H*qLf+{gCptz-I-|h_RR^G zQT0RYdH*P0A+{yP0rT$8kbdaPIpcDon%}W#fJ}a8Az#DBScTg5w4xev;QD8;(Jfq0 z$7{ZGJ{P;2?4cJ7#~N18)egfchS@2jzw~XC>ee4uPwDG4T^trBydHEf5 ze2Gp5S9Ub^6OL#@?OvKzl5}1=UmjKo)@_yj<;(_3DAwqEGq=1I^V+e`u7su2{cUX- zEn_kU(i_3@`uhL4=LLc-9D*p^<)~HG@->l(9duNR*LnV2^)@QEp3$biFi40SR_D>V zEo*5Rr5`Z$_|jgLO;~zfyDYRm+4?YW*>u0vQSy1JUPIxUjQXjdg}QagIZfBv- zGj@)aQ*o}|dh4V$^}cSXJ0L1rol4avdtAS_v}NpUag5(iDMQ5_q(1SKNGamf>7FT+ zD=ge_i>R}_7|WeKBaX4zP|Fjt&Af08%g()*-MMv6F46aO4>D-*sEO%>$bB@5O+583 z$P4btTHexES60SIQOC!}W046JmzEyP*Pqz7-sbuvVEDcKW`>thpUEN?+r`+Od}&FG zh);*ck1Ih~pYMoqMgCI3OHEHt9?zy30aArs4x3M8H?^|1E-5LQVHP9Wy{2l3bKS3Y zm?|?!P<$wPKCIj9p+=7VH9p?M(=(mNo>EY-+4KCM)^_O(25Yik`y3J(BIF$fj$k`q z2P(jYYD*1K)P%mcp$`aN&mu=#6lGwe zFG1J?wq3jAleMJ_U1&T%Y`q_JF^70OM37Ba)6ihitt1jp}%uA6NrE=-s=v4TpjL{;M-65gFN1t*wEh z;|cO$P8At-D$}Ekryb<3lW&`fHNQ2pUcYvcmk&=!ohn=@dY10u4)_}>jp5%VAV>fD z@}#S)Ysu>blOiB9pUmgt{qcJ5P`Yqra494V2(Ln*yveU4+hxr42eS3qc{Nm!`q@<()*ui4QXLGg&jV{oGg~r9j z#ek@`wzli@!@h)J^Lmqkq*K_As%Xz=Ork$=B9O`N;MmX4*<-qC$C}lByl+0clC{L{ zTiuyg;8Td~jaL|TPZViLii!QXycx?=j3nd?frdb|IUg`if)yX!jMHed&W9Tzy&pF{ z(`_=P&r@7c4!&4q4A{OANwSA1C>k0XL4ko4dXGgcgLDFlgMApTj3@ejUL*5o^Za?_ z{{GGM%||I36~N!F-1B)Qo^EtMCnIa}j>2~Mv7Rr&7Z-1Y(yA!dIUp||+l!KBtwyXE+p5m_Jh!xAOzH;$ns5rsdO-)|I z8LzfT8{9xC!{U+;8P=L#_a}@WV%99wi+TrM`$e9LKapl{q)05Lvnt*4tdE3%A$(sao<9VLVErby?}K7l z$Gs$mdWJ@NhCAJ{o^omPMwP4jW_Z2AvEiLvUG&zDG5pi{!cB8I;acO*&vm~o= zPxn!T91ooar?4iOP?JG@A}q_DJ9-HJ9^DnRtR2>H88L%*($Lp?o^DOq#pz8Nm9 zCVHDN_vp8|@D4W!G#k^U87ct|j_>WLLbP#WS1{LS;j*!PwImT!4Al~}=d!f1BdIE} zFBF9wHy<@uR93#?%z(aqIGV10mc_Q&s7g#?~= zl10h!QJIBAHV>-BG^d={xUe6_M5!!zBUP(@$Q;06?q2S4{l%$LyNHfSFxhf2u=kv& zIw)LnIioFAoS%sQS!fHE)Do_4gHyqk!B=F!!!qh^jrYDhH7P7>wiSK-$uq=r_JZSS zIFasty>n*1Meu{vYWX8r{C7K*xaIPdeSND06=5izR=jyi^Pil(sVLNyl@8lHjR+UN6dfief>f__%*(%e>o5vY{v*B^tA!3xw1hHobeSo*r zOOc%?$ZPr1@XT0o#+I}?5$cfPY(7;)fAjhjkwig5Oh;V1u{FiJOnYkd>Bvg^+dlff z<35-yY4ZNK^g1Eyo|Tmq5d0Fjv*=_h5Fk3>@TnyQ&=G0LnNpxK zvB^tm1YonUlr8z*4;h}o^xEy$x?T>DqGVvewb_i3B=-c?+fdB}jHaWZF>doi0RJ8w90YO- zJi@OoFSogbLm-SISn)!!y&rt2U8d>Y z>PkxGRx_2OqbfBR4@Wi5r+ zZ1Td_d}dk}UOHg&&pn!4f2?ldATmiyzs0(ioKl#7TBvK+1>wKtRCw3Wzg86{;f96hprQMg55nZz)eF z%F8xk=^gAUoHJ^hWr;w9bC(ck74jL?P$`L<`cQb^yE|7D$otN@h82>J3l<$%uDraw zwk?+?B|k?_uijiqjtAze#wvPK-}Ma4mKY^Rz+6W3o+NZkXy#l=ANtmA2=}|TtPpSE zY?(lFGoPon0gT)B`r|A;^pNGj@QLJGb#(~|iTH$s^MeIXu#X~11<@Zp0ty^D21YWc zW&G$zq7-iGB{}?72K5tB)d+d}o&9#ZQ|I^Dtev&SfGPsfOO(N!oSZ<*Tsv|DbaX)~wb1_UN^}gBB}=K^D>rHX1gX{pAAlxj z(IVuY()#MGFC~5n3j@(cHl9)Ws0Jcp^ggOl=;ZwT{&dQqUbosKZTldeog=!+%ZL}}DHWB` zWZRa+=;))PZZcfagQeD1hmGF=Moi2s7M7^3|LI}zxe`6isK89%s&lp1@b>(w6-H3w zacW^?v=$YGeO0hq`w&71FY|BzbYSV!+HkV6DplcPWc2P2K*zfB2EfRoyb#iiY8H{~ zs+y{oS_LFF0gHahw{K?V=2}Z1g={ZiwoAf5^zz0my|Z`jJ|!8UO6GUfGh$PDc3V@d zTOZTC28z@%q6v2$h=}(y|C~J;df679)6O_a4M10`3aAot_HR@T3B}6f2&5IVxFfIz zn*KUD+!7vfSq(X1NEu6cv-s%9y(6g8V^@cojgD^nXsuh&^NgB)%O>14)Kz#r4b&JN zU0G*~f74@OWn_gBCiq~+w_hJTdGZ8y25bmLCp8eiBzWFp&wC=;FdC6p`-rL-Q$}&z z>F7fvUX50D_3ZIkJea0S)2qA%X``Y?PeZ9&&KEs*Q-1bOGY--mjFAZsq{o+i)u_3z zycIvixc9jW`#P&R@KWx``gI86FdazBt_Lf;6b4d?RCt>>64A)Cx=PLXE5h0fgf4h$ zB_k)2|10+o=u3spPF2W5IS*6#csCUL(yzyKGdaY^R<9!7-Lt>+rokM-Qe>8Cx$pJ% z2u-e@b4xy=sHf18u#V>bn{(O#l0)TN6Do8|J18S$nD{@{1a4}2h!^hKFG33Ar8xNK z|Libzg!UEAPdytv$Qju)Q$-@9o;cBue;fZ`{Q8fUs}Mex>3vj#^H_7&Pi}{y&w^1jbA&&@AK<}24*lefaZAo@-e_UtGcTI6YNd` ztt`JeTd$1124e&bB?Rk_?{cG)i?Xk}oL-a@(@Strz?Xy&xl1(JF0^3s6G^aSe*+c= zt)%BL6KvUk9GsL<&(zYw(`i(>rU;@W3w95XHJX;~UxQkw`*DO-LdT;#1%sgg%T7GC-SzSqIQ;p+Tw>H8ZLP+c^O5A`h1&+wXBD1^3%ce1ZqlTtn*#hKKUZT7tOwmL6dCE+W!V)8MtP*k4L{vG;+1u@ zof<42d*H{-AJ2bIuC(Zlp#dLeU}4D{d)jOaKo+Qe1O!t4ooPS1Wqn*00A%HdB(`iJ+-iq+R29iL%QOEX@OkivOi4`+gtwb zF{?snMtTYgZ?5iGt7$j1P_ng1}k)3I;ufbvmkxYENlg{(E z&q@A(RRWT)4#;lny|L-SzNEOgZ&1I}$(VmM1s0{cy1L7gtt!2jenKK`hVnp?GJ3Oy zAw(d@L%VXh0!VU;tUZaYvQJJAXM<+!oyX>ETfL#PoR@$H6GTA<4~r&-#~E?(?}C|F zujq9ui!&f=U0Db^q~=fd)t(3yoVq#5$ebKf2hNjs@Jj7}UooXT&WqqJ1pX60b5za0 zDxuKymS;`2tQ5cc1{tiRo`rS}JBm2+>QWf+oUF@r>cwc`c$9trxDvL=-eQ0KITZ(d zSNky-pg&=5qAgKS`a7CLnkNfS(S9K|V96D*%l(T8_{%*>7~uEt{uqwZ`*!C6fF(wX z@D=uDN_dbVOIoET;_Xz!>Nkmj8~#a~x{BKIYUsHY+c#i70q1}`U=(~2+Y1=IEEA)@ zs+JO4+u`Kb$^@C(6OR_aN96tea<%}saYC_qvReh1KVW@IC@n1oo2IR7WWyqmMh7uE+wWHsEZElF0!~m70~}zp%&` z>`Jid04zRSVp*ce`$3R#3C~&ot>Tn6!s2FU$3R6L7JCmFP8!*v2dGW;MP3)_-ut!a z^|bMTub#zOGhD=T-qt0Ky z=!D`>iUTnSCJD+>DfIRAUnTAXhD-Psrt0Jbixj@Q04B8@uEKLf4ur0EI}IS?qFsst zi%(gk!8h4w;%`efr0f*!6^YVwGwuCD1#1=X)R=%fPu*4! z@B=AtX?U!78WT*)9PG8?t_cV9|GJh1Va3tI9H8)?=6Z0OW_SSoT=cc*@$`D`SDI*S z(RdaEpFhh#fhh`G^fMM&T6{d0*TtXNaB<2f34L%|At;6C>m6Hf&*s*;BN@c$COi?6 zAg#`Sf$X1iC?qPxuMSLq)&Bx4pWoo$Aln(3^paYB=QoS}V*-xJ>FYO_K2QfZ9n4<> zOOS2zDL%!A^`7X<%gbuda=H*4)Ca)RMQSLHdIWZ4u!Sqi{eaDAA?xI_9o2U(Fo?gbmngMl}?|t@mpm z^&S2AzN_k`0}oEE^&A&HeF#)kB)xa*dCUzJ*f1O%U&oWGQ6U+paF!bwFddzoylyV+ zjg4sl3Kz=SdY(yErd8E7JRJ2{Q>F?Z0rGlx2#|MV$n{kcS2BbS zv|>&gAKtxV9dR)PrMLjq=ll8d^K;+38~0=`>u-Wgev~1ZcqsLN#vPv1N<>74h)BW# z@Udw2df+zH+&wMlvsi93bE#2>XVgNHll95QU;6N&lQRges4(Kf?@kE=+MFG#8_ zEX7eTXD|1J^sauzUTmhTpK1qi-V5Znl?j3n75*B0GIW=KHpRC)O~=R5eXWCNK)}?W zyZOamaBW3t%w-4FePskG?I;^f6V1 zlQ6eZ^HR86toIn=R|;PZ82F4A;G_3|i3Tg90-T&F?Fq|O$#(2vid=nk`~64X)5s#= zXoR$ZSmq6+p&H}9_}06tA^?x*lYk|Su|`KJ_^WZu#$VJ{i!=@QI8cBp4U`eA z39KRcH>1}cqRP8o40jcysumSZRlmPQS%{J@y;ye?MLoJSom$hz(*FFvg7Iw3?@0vc z-gJ%rf~@DjpR<)*o5tk&up$OWnku&6HmSv8uq4Y2djO@ae%h z-uaIsb9zG6;ICq%+tMUVH9Dwpz6Neb5MjJnxOb(G_PE(G+CmzfO4H5S;>b*hj^{SJ z+8b#A@QsDJ$icvfr;%VT=LajMSJYcYJ2e$8oG4tSqsmWo@*I4j!J`m}6RFJ;3#IMl zVMX7YA7D#3D&TD4xc)hN9fTJYcKzI**8)#-E!%S+0>bc8kk!%Pct0{u`|L`01os0Rur!0SUA!$*DQV8t$JhERSm-{IJChE?RI9L>&&vV#sl9x0=B6?ZKb5e_~N z>z{esEft&WI%20C>q*bgw_)I>`@;s398Io>bRgPP-ZUVAerY7GUGV15d=d`(;2!yo3ippgH-RiIcLm~3c@b?W)K@RY;Hqtpn|j7xDj;U zEPV3oe3kaZHfA9koCd%S>8aR&sQ`fWgtl*gip>RsV}|oh0MbW)p_$|D3lVGfebfFO z;k=(FA?zkAd-;C{pVEw=4zg4y4o?}EJg!tx$2TM~BrSZ`6TtOT=y(DCA9C;okVoHh z_a0Kg7hvM~GBaRe=GRn1sa9(Q(ObEgV!4QYO;O$1K{#Rc^;SxbO)P z1VviJs@(jTkytYYR;Fu=wHO;hUSAbe}8p zkxX$fAp=HyTmY5W(ucTNS>IKIKB5vplIfjop2;G!)L-3A@Fdg$>oPNn9tbt%jb`V- z-)0(TtkzVV@1|uBf}BChmEq`!^is<$lf-lXvz**qg(Qxh=JUl^+ZBg;p!?PXT^d;S zB_xn3#2E1-N%-TNfiR@$$EMj(dS-V)DLvA;#0(4@f!Kz&Ho}u8`+)o$aeY|)=jiA; zkKHnmZUIk?N&!SP;3#r-bYy2^TRFKzKtKQ~xo=AD^qZ#p`-^RtT7c&s9FUWn+qCvA1i}P*r~GI` zK;wtKJ-BWFbg2WB@HY1L8h!8YY!(`5XlR~@3J3^*YXR*RLoPWG$Sk}kOP<5$<^n#p z=`z$fqCMP8UQ|new-5A)8bcsfKrRO9?Eeo9TYt5h{1@*c@G^&!3AX_GF_u=I9>DrJ zB|K+PAzi(O=7s#N-?|t8s0JCwKnNvy{rdIU*%??6Xh4ksTqQcXgR?W-B{uy1jlaMD z#(;W>HZVizEveRnTp1T1Z;@pKzN>_Uh|doUe*{e0r5yDSu^veaI1TW~s^+(D7)UI(EE*dP>zIOEEEjRd9J_2Z( zcqZuK~-ZmzA30hy04UFPmPaOH#22i}YG zf7qQ-i#qe(+S+@)=Se3MWwcZQ<^n$_XwkX)I6B5h1aC@U7Qy;I?*Z%(&}8ZbL8VJC z!g9#cck^dr%x|zT+ai#Hv>&IA`9`SeF#MzEf#O{?1URArKZaU{2J8o0uypp^y>G6K zt$QX=T5lS`^SSy%8!v8Z=Pyw#Frc+If*wi;IVhC60ay`qIh+QePt{ZIc5sa!5MnTw zIhUFM?El^`@hJ>Q)o`usuV#ZE{N}ylyZ$QuuIwTd+c7RFsMq@2rEMMC%%_YwjxfdIubc1X|i%-k!;TSCu z`T&d$gmfl=(#zq zG?N~tdTnKw6j;X&nOIs}_r)4KW5fbyPeKMuR$MWprRDe(+~uJ9psI(0^pTwT9q%5o zx7xo9);A~cnF7@AkC21PkY-|8)T4uqR6&|XUOjb8U>*iA89bh`83t?uF%bfh0iVp? zmR26`%#S@+jDT`+tq)+Qb9?=;8{x8^^}0Fg)~>?_pQ8g&m~n@}eDpLM+G_P8Kn}|5 z95{v^?xOXQjhlPRuAC4DoVSMJow<5XVpM_}iv~~0w-4OG@X`PxB8y$lL3gX~0Tdg+ z+|Q&0BFg@E+ItRx%Vxfg)&Ra$l910MIm|lipMnmRsea>cNDhcyPDHBvIs#Qg{SRj_ zz7=AEPjR3KY7)Z02zex*+H6V>3JSbf$M{P3^r;c25*@?1FbawrpZDegkYP3H~y{VH9n2saZ^r9>p5sI0&*5J9EyR2 z7Z3#^OobjQxR||9uMe(@%gdGM<63}+1XTEdnQ4G~DE_<42)rHw=M0szB&-*kwCLlK zzI`j#13*QD;S&HV1;BL4>iPc6e?KXS5a@eq9U1`Buz3cXkL6P6f%`Ti19BX$JH_bq z@CE1uX~#kbOmo?wPfl?X8qW6VDWz}U~ zr{NHIzxYj?VYsYm!Zbt0ol>6+XELg2kN=WM_c&Q1zp#y$hD{|2Cr?A#r3|> zd#d9*Q|ykftBxdOk_q}!FEKPQtUA;&#!rbpeE1M}wWM=KX^Hv&-C>3!s$Q%aotiq} z^zmrETmtB}g4rmtUW;^~O%=%eA&y6)2##8k^&YXBs~;T>`hf;cHaYNj^i&8U1! zP6j+QVEwqxL_#trOZE5-e|`CnmK)?^FP4`)JZ=jO&aI3U*_zB=s!hVjLg47H+$2PGnNE@TaZu}kps58h)xQ`XL zDfeF;Uq^#K!uhyBUVruK6(D6gehPCywxh}^-V-A!WKF(%3MZD7D5I_BYM98${TAyi zCg^j7$!Y#>RAgm%RsN{e)mabtXTRAPg?NXWo+Z|kdm_wji0mB$i;7#w>m9V3Jtb*= z@!fr$?^S^X5<>vztoN)m22BX4`hHOTrx!qsHWsjVPef@I)4VqO0rUk-frUk(iYyBY zOX8!k3xqz<*))~v>88*_a2)0a+;;7{?R2AU(<*=+zh7gcs{XhbT3)(iXhn`rO_9^! znV|LeIak_er<&abk@C3Mxvoc#`kJ}V6)Ka*^O1IlKEX7W6PWd6${ZmXmLP?u0-hCs z^p>+0Ab}jbJTPAyd&@C-_X|5?DQ)o;onu`WnZC5uoPOlGJ8En2E;zST}D+n2JF{qj% zjibST*?_Pl(G_iMN!s>p1EQHdob-Wr(6f_l2Gh;&hNVb2DI&#}C!cI<%8bj1WqRT27Mvtyv{ZD2Zxban8y~fsH zNZ@0}(?=4NU2BB?;4H+%#F<%Hf`WqX0*moXI@8Pm9?Js>cz_5<`B_I{MDSY!aQUSP+uX{`3-qM|V~c?B4ekJw21G1@$uKASP{q9q)X5_x+%nWyJzznLs2qWfLuEwR*91 z;aal&-y2l_)pa@|-1y1SzY)4U`LZC^H@9`w{VVC1@R5aq0^o9M`wmW2(*f@UdR7e^ zJA-z}a72p(CpUcKbTCRR6F^6?YNUJ|w~+mz*r8xEw6%)ho?g8v{^o~;e(K0M5eF(l zC?{afqF5Nun3&I)7PlLskDeUtf}cYe_9X|LpUq%X{aO1g=zDsyMP-;`w2m3zMUh$y zArvRa2R4?MXG|`YPFKJ>_16s-Fso;{(kZCdY-k+(_a zYOvDvx)NRzj7e7?D0(9csNp|k_|}Y~$U$L393FeyC7_36eXe=G>BGb;d<=hl`mNUr z!082u8ijjaLD%!IEiuPoti1T+MW4stPQAqyEwn7Ow74VXEQECg`CKiRp`_iPaBy6> z;%@+()F}PVhGYV}&%njmmMt2EQDa02Xb*jL@I*8sj(AS%N7bLmc0!eIbF~|#}+ z-P0ggB=8*P(WYNX-9#r>>vVIG>_9UcDVc+cDr8PoL$4PmH7~JFQf+y<>wkg1mM{GI3{f2)qZpa1UkSecSKzn17S7 zt_*xehM$723?b0t3A{qZ21|dY>UNBTpZFg8l%03aMF=}x4+CY*HDX5k-nf6mF3{_H zLMoR}^baMt-1j3h-5Z8Ndun0<%P|9;Y~W`>tmUouEOU z_^~NSUWH5X_>#Ek&HwD4>|!ZSQGcT`A38IN#DT=3~Db zdUXZm2smn2FVnX+G{hk$mJ=)&~loltzX* zU`jy|X41sA=8sDT^dxd`8skPvQ9SkUgFQXZUVZ2ZK*RL`upV3kT-Rq~8yOv~Y(WBk zLk+%?6i`EV_BEJp0oK}0`PLA`0>UCa|G*L3zTa^S?gD87dIcvnW(nz_mQ<60VKW@bvt zPgg-nwxPMoaAQQ2x^!?mIlV`hLc>o`T{j-?3SE!i*Sa3sD^FbCb+;lt3XA5;ry~y` z{F2@MHe&T*8C?pFpuWrX7Tdz@ABXuw6BP<~#>Ow`?!22q>K--IS;t-Y=Pr>6=!{Y8 z^!hmRcn>fUq=^i(sQm5jG#lOJI>g%^Vtk%Di6ph0yed8P`kc57)n2;vN%%sD>zOn} zK(7_skehMCy`<;t-k!6quk4_F9M;0F5~33FJ5)E#j?MRMrrUHXrTwjg^P<|A#*G~N zrzAx_^lwz}s$Cdj)fEmnc};x=n-EUaQ?y5>$2bW$i7n7tb*sYds+cZRcKEar2qY6C zyM1p)E+Q9)+WpL~H*RNjS_Y8ndqJLmu;=b@sfl^XD z?Imv&gJnYJU3@FA+4tVS#CuIx>%<>2*{V^NNur`K&cg%ou&^kuMFnrqsb6~RSA*D` zv}eV<2HUwK$&6QQ4~sQ|nvzt$syN+qBglGWCY=Pz2ne4Oj~hpIDx645A~=Tqw1AF= zjAbHaa=IivM8YpVGbni)*$|<3XgJhr82v6$yiV=>hsKU*XW=LM9Ls3g1no0UH|3Ql zf)d%n>CEyxu;gSG+i~a5$LYPJA)^|K&4jM(2b=e;FQEm_{jnmvT982( zMQ;*1@fbQ3JW1V*jSxBr4R8jOr3;lu7?0cOve)4yKl8!2AGRv{RTZZkHMvW9)_p8f zR_s#(DL)2Xg7ZdmdN!YsWnXm{uP1Dj$!Wg~|9_hM@^C2IxBsDyNXk}-&|{Cvke%-+ z*+tf|w#gc@jGfAokeB{xx(zCr)Tx9OPTFU#Zaq zGyKMoTrGlEV&{2J1Pa#9BQx;HLJnVVce*mAI^n{)XPZ9H5aeI4%C~uGvn29-l)Dg= zKIUOQTOE!eE-$9KE8LCm5$1bmpES7Cxx{W-xx7?&u&~duD_$$?`uWR!(+yK)?!DU` zPO1uTuI2V=tVXIop1(zmz}Dt$#LkdcXQ!fU4hQk+jtqn=$BF4-FDW9g#k?pD=cp|i7G{%acWB41S>SZU#=V2KRo8Fl7$96MX0h**SgbSVltq-zw_Ge= zmC8gh7Ke8HX*rgwO(sA0Nwbt&Z^xT-VwQ z7oHlqN=z31JMz!>dh6Yx`pPN4HLXr8H=e&&g#6La@a^Yty-GUS!Gqmwp*O{{m=?|V z-qxH7QU(G(pjK`yp=|qL5r=m%N(SCcMJg2kEGX^Dt{M_QSBs={ZJS(aakP@KZ1_SX z5@%TFcsUhxuN#NqjTPSlZt}Iq!l#^^9O$#9ZF0-H zsTK|a50KiPP*%34JH^Y(%gk)MJHiuEzz4$*+TR7N(%5Hrfirz~)@B5|rlOtdIm&hN zvd#`jWhKNa_Vo6WzdSt&^bE`jWx;60+}s?zFEm_&f`T4De(dh|1yJb#!|Fo3;()RK8Fk_eAgpK>_01+}s8IK~)9ycEjjeiLT;C$ieBvMig*?NUCstdDrC68>V!0b!zer7GL^3@yY2+pkQ7Zzy$?k9)P$i=0 zt3|s&)#TT7%ii!UeR{qPzd&2tFC$nB8{hA2`C>=PXlS%A6Z^~R>v1aUds0j?Oin|i zt?9d)byXAhg4ywG=!~e`p4O~DEmIn8z(lnL>{Kt^?lUid5}2A=+P)$8fF5%tD+@;I z;Yxf9-nbN;=QfG7XY}Dxq~3m%Qd+BzRmnR>3e=%l4vTkgH|@I0wf*7f8UHk|Y#mIDTn(;S?@emPF}P~dA_oyZsWz*&5k)f8l5)|QF!!jr^XLvM_7t=S z9*dbh9~^T)Pi)V8QGWAY^e#4{wy9P|G%ZYD()sIOP^pe;coKGWP;x}_T&-vF`lWmZ z8dgu!CE8`YBg3I_N!r8&Db5)D?wu3aJwwGV!(QhCT^60V`kiZcH3eu8J^_ct#kPP5 zx=*SIcG1xODdD0KprM4hn9lu*cJk1s&4i?l7odBDs3to%cMpK5larG->dJPe)Yg>> zeqN+lfPfwX;dH&d+76(>TuA$yc6RgC_j`1r?jk}gPrLr1ry}1%FT}$rKlW*qE`lHc zUj&&{Qeq-ZZkp#2Rqx39(;TlXe-kz;)Hluy7O}K7t`eTaF?j2b>%M=&c9+q_$rNoM zPuwoc7*tvakQQnaZ>uVVE~GDHrF{lxLN;}@O#KUgxHvbSMjhT{npASwf{(X0t{Ug+ z4HHR+jxiyXJTaK7XBhoAe^6=7Krd!I`V@59eEqQ|r3A znNrNSkMQu)l;Rj7Pp)D}{VADJIeX9QZ_U6`%_8i4Pjw-S8^1~Fqllk+Sl72NuA8NC zJ2tNTf^Lr)@G(S$g)b?2y1R=&Zl zB+xk}+sKCFlGyk5SLGKTw;Y<`q>L1rD!XjQyajU#r0gsd zL==SJ0J-eDK2PAR3}pspmU=Fgf(T_S$n(yh&#JD*fBJPbn6j*L97&UzZPIts;btjC zE1t~3EEQ&kGoIyl0a1^uWZ{9sll;yExHn`Tbv9!U|MBD{UBiBOwgF|Y zwSb1Y%96N+j`^TO`n~HOD%vcyLTp|Vuv&}0zP;IX830xFzCvfHdfRJ z?5uS}oQ&m`Go<^vu)x8^WkF;UPuD9nDFsamo>Ql?M~MRb{J^2PJy<}=Rb^qWL^pxc zlG46%>=#$EQS!#j-2;oIN1zPIXR=pbXYd*6Yr9x7XAF<|XD&Z);Zb-JV9r7FiJnhA zmM1+U!+QOAqDd+2j3QsWNDMcl=Yvwaqk7S9EUj+VUq`oam2edm6(t5wvyZG@{n2p! z*`{VG7Ejz+xT7tG6;9t6)m(}8d4SGzh>>5t`6(NNpVmxG-;l+ed3@AR^wFiG42S{N z^q&ehj$Mn5IyV~X!SdjbzshZ=AguJ-BZIc4Fwi_84j0GaaqaS&cVh^SUVx@;_FeBi zshNB4ee~J5goON?UiRKko0#THoI_W%-TN>rVQ9%0PoGT_}7<}=`5jb`|^Cl^L|1w`aC|aMdLJ7p!J-{@X5upeTG)KX)Ohh zg9a)J+_ZdpIk=si926v8>0nj^eAzb&d15|Bqum{^I~iX`= zK*s)hcKEnj{=b%&;=eLXWB*Hk-&!5q|mvEU2! zPTIUip}#GIGH=o@oyzy3)YM4mkCMZ3b&vlzw>?$1!egX#hDAXTE2~R2K+VHtBNA=m z2d~>yMx`1T7TcmfN%pu7zXpn)?`kIp$ZpsK*Sy?*xfo)q$<(>Kiv=7c~bfUip2GmR(OPuAD9+MmKq# zgOuS}2q8LL?xglbEt1Eo|0_L+s_ws+-|S4UV=N~xJIv&bJ-ADBD4WVB5V@3`>ee#} zg9|T?Y|)Q=59B$`YnW3%`0F(0+kCWK?fzOns-pfB${s`CGOeCe$Pj6owD|h>XCB4b zr$z^iImOqDqgu`9@=2%JrCv)W<{}aHBs#M`;Q+Jk99Q;0rF|#ZJPTr~3Bg3|nbz9N z9=Y`+d%5(y-DVk)$m)+~_l+isbbAEGi=0Q8Y!nts@Lnry*@-W$UfmpjO~}c_WVfg2TIaT;10p(UCm~ReZkXJa4xPz#sl(AZT7_El|Loq z$Z1_|DdkF)uedp9`;#cmd>$P}vBjD-ajBb+k0FSv?T*%`f=<~SYk&9Vgjce-(aHBA0unv;<8iant%`-Hmz79W-L%TX@BM(xu@h^SER^5Jm z1$VrvO$ zem#A${Nk2U0p-&=20X7xqiUAO3)o7koKx-&#BUv}Ds3I6X%7t{;Ctgt#(&m!yp9OQ z%wi1j$A7UX=5#$1-0C%KvcFyPqW`00PtwsEus;A!K*DPO1Nm+8eT)ZJ{q4DMay5C3 zSXGhXhpWx;+`LsIw3Vs|eМ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 literal 0 HcmV?d00001 diff --git a/contrib/contrib/1-Wire/Schaltplan_USB_1-Wire_Interface.png b/contrib/contrib/1-Wire/Schaltplan_USB_1-Wire_Interface.png new file mode 100644 index 0000000000000000000000000000000000000000..4c5f08cefbf7a782fc917adb9d8f7a9091dc6478 GIT binary patch literal 41324 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 LaI!C@dHw4@*a%yN literal 0 HcmV?d00001 diff --git a/contrib/contrib/11_FHT8V.pm b/contrib/contrib/11_FHT8V.pm new file mode 100755 index 000000000..f5d1c7840 --- /dev/null +++ b/contrib/contrib/11_FHT8V.pm @@ -0,0 +1,247 @@ +############################################# +package main; + +use strict; +use warnings; + +use vars qw(%fht8v_c2b); # would Peter like to access it from outside too? ;-) + +# defptr{XMIT BTN}{DEVNAME} -> Ptr to global defs entry for this device +my %defptr; + +# my %follow; + +sub +FHT8V_Initialize($) +{ + my ($hash) = @_; + +# $hash->{Match} = "^([0-9]{2}:2[0-9A-F]{3} )*([0-9]{2}:2[0-9A-F]{3})\$"; + $hash->{SetFn} = "FHT8V_Set"; + $hash->{DefFn} = "FHT8V_Define"; + $hash->{UndefFn} = "FHT8V_Undef"; + $hash->{AttrList} = "IODev do_not_notify:1,0 dummy:1,0 showtime:1,0 loglevel:0,1,2,3,4,5,6"; + +} + + +################################### +sub FHT8V_valve_position(@) +{ + my ($hash, @a) = @_; + my $na = int(@a); + my $v; + + my $arg2_percent=0; + if ( $na > 3 ) { + $arg2_percent=$a[3] eq "%"; + } + if ( $a[2] =~ m/^[0-9]{1,3}%$/ || $a[2] =~ m/^[0-9]{1,3}$/ && $arg2_percent ) { + my $num; + if ( $arg2_percent ) { + $num=$a[2]; + } else { + $num=substr($a[2],0,-1); + } + return "Out of range." if ( $num > 100 || $num < 0 ); + $num=255 if ( $num == 100 ); + $v=sprintf("%.0f",2.56*$num); + } else { + return "Argument hast invalid value \"$a[2]\"." if ( $a[2] !~ m/^[0-9]{1,3}$/ ); + return "Out of range. Range: 0..255." if ( $a[2] > 255 || $a[2] < 0 ); + $v = $a[2]; + } + + Log GetLogLevel($a[2],2), "FHT8V $a[0]: v: $v"; + + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X26%02X",$hash->{NO}, $v)) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + + $hash->{STATE}=sprintf("%d%%", $v*0.390625); + return undef; +} + +sub FHT8V_beep(@) +{ + my ($hash, @a) = @_; + + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X2E00",$hash->{NO})) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + + $hash->{STATE}="beep"; + return undef; +} + +sub FHT8V_open(@) +{ + my ($hash, @a) = @_; + + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X2100",$hash->{NO})) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + + $hash->{STATE}="open"; + return undef; +} + +sub FHT8V_off(@) +{ + my ($hash, @a) = @_; + + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X2000",$hash->{NO})) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + + $hash->{STATE}="off"; + return undef; +} + +sub FHT8V_close(@) +{ + my ($hash, @a) = @_; + + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X2200",$hash->{NO})) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + + $hash->{STATE}="close"; + return undef; +} + +sub +FHT8V_assign(@) +{ + my ($hash, @a) = @_; + my $na = int(@a); + my $v = 0; + + if ( $na > 2 ) { + return "Parameter \"".$a[3]."\" defining offset must be numerical." if ( $a[3] !~ /[0-9]+/ ); + $v=int($a[3]); + } + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X2F%02X",$hash->{NO},$v)) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + + # not sure if this is nessesary but I saw it in the documentation... + IOWrite($hash, "", sprintf("T".$hash->{XMIT}."%02X2600",$hash->{NO},$v)) # CUL hack + if($hash->{IODev} && $hash->{IODev}->{TYPE} eq "CUL"); + $hash->{STATE}="assigning"; + return undef; +} + +sub +FHT8V_Set($@) +{ + my ($hash, @a) = @_; + my $na = int(@a); + + return "Parameter missing" if ( $na < 2 ); + if ( $_[2] eq "valve" ) { + return FHT8V_valve_position(@_); + } + if ( $_[2] eq "open" ) { + return FHT8V_open(@_); + } + if ( $_[2] eq "close" ) { + return FHT8V_close(@_); + } + if ( $_[2] eq "beep" ) { + return FHT8V_beep(@_); + } + if ( $_[2] eq "assign" ) { + return FHT8V_assign(@_); + } + if ( $_[2] eq "off" ) { + return FHT8V_off(@_); + } + return "Could not set undefined parameter \"".$_[2]."\"."; +} + + +############################# +sub +FHT8V_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + my $na = int(@a); + + my $u = "wrong syntax: define FHT8V housecode " . + "addr"; + + return $u if( $na < 3 ); + return "Define $a[0]: wrong housecode format: specify a 4 digit hex value ". + "or an 8 digit quad value" + if( ($a[2] !~ m/^[a-f0-9]{4}$/i) && ($a[2] !~ m/^[1-4]{8}$/i) ); + + if ( $na > 3 ) { + return "Define $a[0]: wrong valve address format: specify a 2 digit hex value " . + "or a 4 digit quad value" + if( ($a[3] !~ m/^[a-f0-9]{2}$/i) && ($a[3] !~ m/^[1-4]{4}$/i) ); + } + + my $housecode = $a[2]; + $housecode = four2hex($housecode,4) if (length($housecode) == 8); + + my $valve_number = 1; + if ( $na > 3 ) { + my $valve_number = $a[3]; + $valve_number = four2hex($valve_number,2) if (length($valve_number) == 4); + } + + $hash->{XMIT} = lc($housecode); + $hash->{NO} = lc($valve_number); + + my $code = "$housecode $valve_number"; + my $ncode = 1; + my $name = $a[0]; + + $hash->{CODE}{$ncode++} = $code; + $defptr{$code}{$name} = $hash; + + for(my $i = 4; $i < int(@a); $i += 2) { + + return "No address specified for $a[$i]" if($i == int(@a)-1); + + $a[$i] = lc($a[$i]); + if($a[$i] eq "fg") { + return "Bad fg address for $name, see the doc" + if( ($a[$i+1] !~ m/^f[a-f0-9]$/) && ($a[$i+1] !~ m/^44[1-4][1-4]$/)); + } elsif($a[$i] eq "lm") { + return "Bad lm address for $name, see the doc" + if( ($a[$i+1] !~ m/^[a-f0-9]f$/) && ($a[$i+1] !~ m/^[1-4][1-4]44$/)); + } elsif($a[$i] eq "gm") { + return "Bad gm address for $name, must be ff" + if( ($a[$i+1] ne "ff") && ($a[$i+1] ne "4444")); + } else { + return $u; + } + + my $grpcode = $a[$i+1]; + if (length($grpcode) == 4) { + $grpcode = four2hex($grpcode,2); + } + + $code = "$housecode $grpcode"; + $hash->{CODE}{$ncode++} = $code; + $defptr{$code}{$name} = $hash; + } + $hash->{TYPE}="FHT8V"; + AssignIoPort($hash); +} + +############################# +sub +FHT8V_Undef($$) +{ + my ($hash, $name) = @_; + foreach my $c (keys %{ $hash->{CODE} } ) { + $c = $hash->{CODE}{$c}; + + # As after a rename the $name my be different from the $defptr{$c}{$n} + # we look for the hash. + foreach my $dname (keys %{ $defptr{$c} }) { + delete($defptr{$c}{$dname}) if($defptr{$c}{$dname} == $hash); + } + } + return undef; +} + +1; diff --git a/contrib/contrib/21_OWTEMP.pm.fork b/contrib/contrib/21_OWTEMP.pm.fork new file mode 100644 index 000000000..08818e2a4 --- /dev/null +++ b/contrib/contrib/21_OWTEMP.pm.fork @@ -0,0 +1,531 @@ +################################################################ +# +# Copyright notice +# +# (c) 2009 Copyright: Martin Fischer (m_fischer at gmx dot de) +# All rights reserved +# +# This script free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +################################################################ +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); +use OW; + +my %gets = ( + "address" => "", + "alias" => "", + "crc8" => "", + "family" => "10", + "id" => "", + "locator" => "", + "power" => "", + "present" => "", +# "r_address" => "", +# "r_id" => "", +# "r_locator" => "", + "temperature" => "", + "temphigh" => "", + "templow" => "", + "type" => "", +); + +my %sets = ( + "alias" => "", + "temphigh" => "", + "templow" => "", + "interval" => "", + "alarminterval" => "", +); + +my %updates = ( + "present" => "", + "temperature" => "", + "templow" => "", + "temphigh" => "", +); + +my %dummy = ( + "crc8" => "4D", + "alias" => "dummy", + "locator" => "FFFFFFFFFFFFFFFF", + "power" => "0", + "present" => "1", + "temphigh" => "75", + "templow" => "10", + "type" => "DS18S20", + "warnings" => "none", +); + +##################################### +sub +OWTEMP_Initialize($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "OWTEMP_Define"; + $hash->{UndefFn} = "OWTEMP_Undef"; + $hash->{GetFn} = "OWTEMP_Get"; + $hash->{SetFn} = "OWTEMP_Set"; + $hash->{AttrList}= "IODev do_not_notify:0,1 showtime:0,1 model:DS18S20 loglevel:0,1,2,3,4,5"; +} + +##################################### +sub +OWTEMP_UpdateReading($$$$) +{ + my ($hash,$reading,$now,$value) = @_; + + # define vars + my $temp; + + # exit if empty value + return 0 + if(!defined($value) || $value eq ""); + + # trim value + $value =~ s/\s//g + if($reading ne "warnings"); + if($reading eq "temperature") { + $value = sprintf("%.4f",$value); + $temp = $value; + $value = $value . " (".$hash->{OW_SCALE}.")"; + } + + # update readings + $hash->{READINGS}{$reading}{TIME} = $now; + $hash->{READINGS}{$reading}{VAL} = $value; + Log 4, "OWTEMP $hash->{NAME} $reading: $value"; + + return $value; +} + +##################################### +sub +OWTEMP_GetUpdate($$) +{ + my ($hash, $a) = @_; + + # define vars + my $name = $hash->{NAME}; + my $now = TimeNow(); + my $value = ""; + my $temp = ""; + my $ret = ""; + my $count = 0; + + # define warnings + my $warn = "none"; + $hash->{ALARM} = "0"; + + # check for real sensor + if($hash->{OW_ID} ne "none") { + # real sensor + + if(!$hash->{LOCAL} || $a eq "") { + + ##################### + # OW::Get is too slow: do it in the background by fork. After receiving + # the data from the OW module, the child contacts the parent, and calls + # "set childupdate ", which in turn will call this function + # again with a filled CHILDDATA + + if(!$hash->{CHILDDATA}) { + if($hash->{CHILDPID}) { + Log 2, "OWTEMP: Child already forked: timeout too short?"; + return; + } + + return if(($hash->{CHILDPID} = fork)); + + my @ret; + foreach my $r (sort keys %updates) { + my $ret = OW::get("/uncached/".$hash->{OW_PATH}."/".$r); + $ret = "" if(!defined($ret)); + push(@ret, $ret); + last if($ret eq ""); + } + my @port = split(" ", $attr{global}{port}); + my $server = IO::Socket::INET->new(PeerAddr => "localhost:$port[0]"); + Log 0, "OWTEMP: Can't connect to parent\n" if(!$server); + syswrite($server, "set $hash->{NAME} childupdate ".join(":",@ret)."\n"); + exit(0); + + } else { + + ##################### + # Digest the data sent by the CHILD. + my @ret = split(":", $hash->{CHILDDATA}); + delete($hash->{CHILDPID}); + delete($hash->{CHILDDATA}); + foreach my $r (sort keys %updates) { + $ret = shift(@ret); + if($ret eq "") { + # + $hash->{PRESENT} = "0"; + $r = "present"; + $value = "0"; + $ret = OWTEMP_UpdateReading($hash,$r,$now,$value); + $hash->{CHANGED}[$count] = "present: ".$value + } else { + $hash->{PRESENT} = "1"; + $value = $ret; + if($r eq "temperature") { + $temp = sprintf("%.4f",$value); + $temp =~ s/\s//g; + } + $ret = OWTEMP_UpdateReading($hash,$r,$now,$value); + } + last if($hash->{PRESENT} eq "0"); + } + } + } else { + $ret = ""; + $ret = OW::get("/uncached/".$hash->{OW_PATH}."/".$a); + if(!defined($ret)) { + $hash->{PRESENT} = "0"; + $a = "present"; + $value = "0"; + $ret = OWTEMP_UpdateReading($hash,$a,$now,$value); + } else { + $hash->{PRESENT} = "1"; + $value = $ret; + if($a eq "temperature") { + $temp = sprintf("%.4f",$value); + $temp =~ s/\s//g; + $value = $temp; + } + $ret = OWTEMP_UpdateReading($hash,$a,$now,$value); + } + } + } else { + # dummy sensor + $temp = sprintf("%.4f",rand(85)); + $dummy{temperature} = $temp; + $dummy{present} = "1"; + $hash->{PRESENT} = $dummy{present}; + + if(!$hash->{LOCAL} || $a eq "") { + foreach my $r (sort keys %updates) { + $ret = OWTEMP_UpdateReading($hash,$r,$now,$dummy{$r}); + } + } else { + $ret = ""; + $ret = $dummy{$a}; + if($ret ne "") { + $value = $ret; + if($a eq "temperature") { + $temp = sprintf("%.4f",$value); + $temp =~ s/\s//g; + } + $ret = OWTEMP_UpdateReading($hash,$a,$now,$value); + } + } + } + + return 1 + if($hash->{LOCAL} && $a eq "" && $hash->{PRESENT} eq "0"); + + # check for warnings + my $templow = $hash->{READINGS}{templow}{VAL}; + my $temphigh = $hash->{READINGS}{temphigh}{VAL}; + + if($hash->{PRESENT} eq "1") { + if($temp <= $templow) { + # low temperature + $hash->{ALARM} = "1"; + $warn = "templow"; + } elsif($temp >= $temphigh) { + # high temperature + $hash->{ALARM} = "1"; + $warn = "temphigh"; + } + } else { + # set old state + $temp = $hash->{READINGS}{temperature}{VAL}; + ($temp,undef) = split(" ",$temp); + # sensor is missing + $hash->{ALARM} = "1"; + $warn = "not present"; + } + + if(!$hash->{LOCAL} || $a eq "") { + $ret = OWTEMP_UpdateReading($hash,"warnings",$now,$warn); + } + + $hash->{STATE} = "T: ".$temp." ". + "L: ".$templow." ". + "H: ".$temphigh." ". + "P: ".$hash->{PRESENT}." ". + "A: ".$hash->{ALARM}." ". + "W: ".$warn; + + # inform changes + # state + $hash->{CHANGED}[$count++] = $hash->{STATE}; + # present + $hash->{CHANGED}[$count++] = "present: ".$hash->{PRESENT} + if(defined($hash->{PRESENT}) && $hash->{PRESENT} ne ""); + # temperature + $hash->{CHANGED}[$count++] = "temperature: ".$temp." (".$hash->{OW_SCALE}.")" + if(defined($temp) && $temp ne ""); + # temperature raw + $hash->{CHANGED}[$count++] = "tempraw: ".$temp + if(defined($temp) && $temp ne ""); + # low temperature + $hash->{CHANGED}[$count++] = "templow: ".$templow + if(defined($templow) && $templow ne ""); + # high temperature + $hash->{CHANGED}[$count++] = "temphigh: ".$temphigh + if(defined($temphigh) && $temphigh ne ""); + # warnings + $hash->{CHANGED}[$count++] = "warnings: ".$warn + if(defined($warn) && $warn ne ""); + + + if(!$hash->{LOCAL}) { + # update timer + RemoveInternalTimer($hash); + # check alarm + if($hash->{ALARM} eq "0") { + $hash->{INTERVAL} = $hash->{INTV_CHECK}; + } else { + $hash->{INTERVAL} = $hash->{INTV_ALARM}; + } + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "OWTEMP_GetUpdate", $hash, 1); + } else { + return $value; + } + + if(!$hash->{LOCAL}) { + DoTrigger($name, undef) if($init_done); + } + + return $hash->{STATE}; +} + +##################################### +sub +OWTEMP_Get($@) +{ + my ($hash, @a) = @_; + + # check syntax + return "argument is missing @a" + if(int(@a) != 2); + # check argument + return "Unknown argument $a[1], choose one of ".join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + # define vars + my $value; + + # get value + $hash->{LOCAL} = 1; + $value = OWTEMP_GetUpdate($hash,$a[1]); + delete $hash->{LOCAL}; + + my $reading = $a[1]; + + if(defined($hash->{READINGS}{$reading})) { + $value = $hash->{READINGS}{$reading}{VAL}; + } + + return "$a[0] $reading => $value"; +} + +##################################### +sub +OWTEMP_Set($@) +{ + my ($hash, @a) = @_; + + # check syntax + return "set needs one parameter" + if(int(@a) != 3); + # check arguments + return "Unknown argument $a[1], choose one of ".join(",", sort keys %sets) + if(!defined($sets{$a[1]}) && $a[1] ne "childupdate"); + + # define vars + my $key = $a[1]; + my $value = $a[2]; + my $ret; + + if($key eq "childupdate") { + $hash->{CHILDDATA} = $value; + OWTEMP_GetUpdate($hash,undef); + return undef; + } + + # set new timer + if($key eq "interval" || $key eq "alarminterval") { + $key = "INTV_CHECK" + if($key eq "interval"); + $key = "INTV_ALARM" + if($key eq "alarminterval"); + # update timer + $hash->{$key} = $value; + RemoveInternalTimer($hash); + # check alarm + if($hash->{ALARM} eq "0") { + $hash->{INTERVAL} = $hash->{INTV_CHECK}; + } else { + $hash->{INTERVAL} = $hash->{INTV_ALARM}; + } + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "OWTEMP_GetUpdate", $hash, 1); + } + + # set warnings + if($key eq "templow" || $key eq "temphigh") { + # check range + return "wrong value: range -55°C - 125°C" + if(int($value) < -55 || int($value) > 125); + } + + # set value + Log 4, "OWTEMP set $hash->{NAME} $key $value"; + + # check for real sensor + if($hash->{OW_ID} ne "none") { + # real senson + $ret = OW::put($hash->{OW_PATH}."/$key",$value); + } else { + # dummy sensor + $dummy{$key} = $value; + } + + # update readings + if($key ne "interval" || $key ne "alarminterval") { + $hash->{LOCAL} = 1; + $ret = OWTEMP_GetUpdate($hash,$key); + delete $hash->{LOCAL}; + } + + return undef; +} + +##################################### +sub +OWTEMP_Define($$) +{ + my ($hash, $def) = @_; + + # define OWTEMP [interval] [alarminterval] + # e.g.: define flow OWTEMP 332670010800 300 + + my @a = split("[ \t][ \t]*", $def); + + # check syntax + return "wrong syntax: define OWTEMP [interval] [alarminterval]" + if(int(@a) < 2 && int(@a) > 5); + # check ID format + return "Define $a[0]: missing ID or wrong ID format: specify a 12 digit value or set it to none for demo mode" + if(lc($a[2]) ne "none" && lc($a[2]) !~ m/^[0-9|a-f]{12}$/); + + # define vars + my $name = $a[0]; + my $id = $a[2]; + my $interval = 300; + my $alarminterval = 300; + my $scale = ""; + my $ret = ""; + + # overwrite default intervals if set by define + if(int(@a)==4) { $interval = $a[3]; } + if(int(@a)==5) { $interval = $a[3]; $alarminterval = $a[4] } + + # define device internals + $hash->{ALARM} = 0; + $hash->{INTERVAL} = $interval; + $hash->{INTV_CHECK} = $interval; + $hash->{INTV_ALARM} = $alarminterval; + $hash->{OW_ID} = $id; + $hash->{OW_FAMILY} = $gets{family}; + $hash->{OW_PATH} = $hash->{OW_FAMILY}.".".$hash->{OW_ID}; + $hash->{PRESENT} = 0; + + $modules{OWTEMP}{defptr}{$a[2]} = $hash; + + # assign IO port + AssignIoPort($hash); + return "No I/O device found. Please define a OWFS device first." + if(!defined($hash->{IODev}->{NAME})); + + # get scale from I/O device + $scale = $attr{$hash->{IODev}->{NAME}}{"temp-scale"}; + # define scale for temperature values + $scale = "Celsius" if ($scale eq "C"); + $scale = "Fahrenheit" if ($scale eq "F"); + $scale = "Kelvin" if ($scale eq "K"); + $scale = "Rankine" if ($scale eq "R"); + $hash->{OW_SCALE} = $scale; + + $hash->{STATE} = "Defined"; + + # define dummy values for testing + if($hash->{OW_ID} eq "none") { + my $now = TimeNow(); + $dummy{address} = $hash->{OW_FAMILY}.$hash->{OW_ID}.$dummy{crc8}; + $dummy{family} = $hash->{OW_FAMILY}; + $dummy{id} = $hash->{OW_ID}; + $dummy{temperature} = "80.0000 (".$hash->{OW_SCALE}.")"; + foreach my $r (sort keys %gets) { + $hash->{READINGS}{$r}{TIME} = $now; + $hash->{READINGS}{$r}{VAL} = $dummy{$r}; + Log 4, "OWTEMP $hash->{NAME} $r: ".$dummy{$r}; + } + } + + $hash->{STATE} = "Initialized"; + + # initalize + $hash->{LOCAL} = 1; + $ret = OWTEMP_GetUpdate($hash,""); + delete $hash->{LOCAL}; + + # exit if sensor is not present + return "Define $hash->{NAME}: Sensor is not reachable. Check first your 1-wire connection." + if(defined($ret) && $ret eq 1); + + if(!$hash->{LOCAL}) { + if($hash->{ALARM} eq "0") { + $hash->{INTERVAL} = $hash->{INTV_CHECK}; + } else { + $hash->{INTERVAL} = $hash->{INTV_ALARM}; + } + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "OWTEMP_GetUpdate", $hash, 0); + } + + return undef; +} + +##################################### +sub +OWTEMP_Undef($$) +{ + my ($hash, $name) = @_; + + delete($modules{OWTEMP}{defptr}{$hash->{NAME}}); + RemoveInternalTimer($hash); + + return undef; +} + +1; diff --git a/contrib/contrib/64_ESA.pm b/contrib/contrib/64_ESA.pm new file mode 100644 index 000000000..fbbc57b4e --- /dev/null +++ b/contrib/contrib/64_ESA.pm @@ -0,0 +1,160 @@ +############################################## +# (c) by STefan Mayer (stefan(at)clumsy.ch) # +# # +# please feel free to contact me for any # +# changes, improvments, suggestions, etc # +# # +############################################## + +package main; + +use strict; +use warnings; + +my %codes = ( + "19fa" => "ESA2000_LED", +); + + +##################################### +sub +ESA_Initialize($) +{ + my ($hash) = @_; + +# S0119FA011E00007D6E003100000007C9 ESA2000_LED + + $hash->{Match} = "^S................................\$"; + $hash->{DefFn} = "ESA_Define"; + $hash->{UndefFn} = "ESA_Undef"; + $hash->{ParseFn} = "ESA_Parse"; + $hash->{AttrList} = "IODev do_not_notify:0,1 showtime:0,1 model:esa2000-led loglevel:0,1,2,3,4,5,6 ignore:0,1"; +} + +##################################### +sub +ESA_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + return "wrong syntax: define ESA CODE" if(int(@a) != 3); + $a[2] = lc($a[2]); + return "Define $a[0]: wrong CODE format: specify a 4 digit hex value" + if($a[2] !~ m/^[a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/); + + + $hash->{CODE} = $a[2]; + $modules{ESA}{defptr}{$a[2]} = $hash; + AssignIoPort($hash); + return undef; +} + +##################################### +sub +ESA_Undef($$) +{ + my ($hash, $name) = @_; + delete($modules{ESA}{defptr}{$hash->{CODE}}) + if(defined($hash->{CODE}) && + defined($modules{ESA}{defptr}{$hash->{CODE}})); + return undef; +} + +##################################### +sub +ESA_Parse($$) +{ + my ($hash, $msg) = @_; + +# 0123456789012345678901234567890123456789 +# S0119FA011E00007D6E003100000007C9F9 ESA2000_LED + $msg = lc($msg); + my $seq = substr($msg, 1, 2); + my $cde = substr($msg, 3, 4); + my $dev = substr($msg, 7, 4); + my $val = substr($msg, 11, 22); + + Log 5, "ESA msg $msg"; + Log 5, "ESA seq $seq"; + Log 5, "ESA device $dev"; + Log 5, "ESA code $cde"; + + my $type = ""; + foreach my $c (keys %codes) { + $c = lc($c); + if($cde =~ m/$c/) { + $type = $codes{$c}; + last; + } + } + + if(!defined($modules{ESA}{defptr}{$dev})) { + Log 3, "Unknown ESA device $dev, please define it"; + $type = "ESA" if(!$type); + return "UNDEFINED ${type}_$dev ESA $dev"; + } + + my $def = $modules{ESA}{defptr}{$dev}; + my $name = $def->{NAME}; + return "" if(IsIgnored($name)); + + my (@v, @txt); + + if($type eq "ESA2000_LED") { + + @txt = ( "repeat", "sequence", "total_ticks", "actual_ticks", "ticks_kwh", "raw", "total_kwh", "actual_kwh" ); + + # Codierung Hex + $v[0] = int(hex($seq) / 128) ? "+" : "-"; # repeated + $v[1] = hex($seq) % 128; + $v[2] = hex(substr($val,0,8)); + $v[3] = hex(substr($val,8,4)); + $v[4] = hex(substr($val,18,4)) ^ 25; # XOR 25, whyever bit 1,4,5 are swapped?!?! + + $v[5] = sprintf("CNT: %d%s CUM: %d CUR: %d TICKS: %d", + $v[1], $v[0], $v[2], $v[3], $v[4]); + $v[6] = $v[2]/$v[4]; # calculate kW + $v[7] = $v[3]/$v[4]; # calculate kW + $val = sprintf("CNT: %d%s CUM: %0.3f CUR: %0.3f TICKS: %d", + $v[1], $v[0], $v[6], $v[7], $v[4]); + + +# $v[0] = "$v[0] (Repeated)"; +# $v[1] = "$v[1] (Sequence)"; +# $v[2] = "$v[2] (Total)"; +# $v[3] = "$v[3] (Actual)"; +# $v[4] = "$v[4] (T/kWh)"; + + } else { + + Log 3, "ESA Device $dev (Unknown type: $type)"; + return ""; + + } + + my $now = TimeNow(); + + my $max = int(@txt); + + if ( $def->{READINGS}{"sequence"}{VAL} ne $v[1] ) { + Log GetLogLevel($name,4), "ESA $name: $val"; + for( my $i = 0; $i < $max; $i++) { + $def->{READINGS}{$txt[$i]}{TIME} = $now; + $def->{READINGS}{$txt[$i]}{VAL} = $v[$i]; + $def->{CHANGED}[$i] = "$txt[$i]: $v[$i]"; + } + $def->{READINGS}{type}{TIME} = $now; + $def->{READINGS}{type}{VAL} = $type; + + $def->{STATE} = $val; + $def->{CHANGED}[$max++] = $val; + } else { + Log GetLogLevel($name,4), "(ESA/DISCARDED $name: $val)"; + return "($name)"; + } + + return $name; +} + +1; diff --git a/contrib/contrib/70_NT5000.pm b/contrib/contrib/70_NT5000.pm new file mode 100755 index 000000000..30db53037 --- /dev/null +++ b/contrib/contrib/70_NT5000.pm @@ -0,0 +1,1098 @@ +######################################################################################## +# +# NT5000.pm +# +# FHEM module to read the data from a Sunways NT5000 solar inverter +# +# Prof. Dr. Peter A. Henning, 2011 +# +# Version 1.0 - February 21, 2012 +# +# Setup as: +# define nt5000 NT5000 +# +# where nt5000 may be replaced by any name string and +# is a serial (USB) device or the keyword "emulator". +# In the latter case, a 4.5 kWP solar installation is simulated +# +# Additional attributes are defined in fhem.cfg as +# attr nt5000 room Solaranlage +# Area of solar installation +# attr nt5000 Area 32.75 +# Peak Solar Power +# attr nt5000 PSP 4.5 +# Months with erroneous readings - see line 83 ff +# attr nt5000 MERR +# Expected yields per month / year +# attr nt5000 Wx_M1 150 +# attr nt5000 Wx_M2 250 +# attr nt5000 Wx_M3 350 +# attr nt5000 Wx_M4 450 +# attr nt5000 Wx_M5 600 +# attr nt5000 Wx_M6 600 +# attr nt5000 Wx_M7 600 +# attr nt5000 Wx_M8 600 +# attr nt5000 Wx_M9 450 +# attr nt5000 Wx_M10 350 +# attr nt5000 Wx_M11 250 +# attr nt5000 Wx_M12 150 +# attr nt5000 Wx_Y 4800 +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +package main; + +use strict; +use warnings; +use Device::SerialPort; + +#-- Prototypes to make komodo happy +use vars qw{%attr %defs}; +sub Log($$); + +#-- Line counter +my $cline=0; + +#-- These we may get on request +my %gets = ( + "reading" => "R", + "month" => "M", + "year" => "Y", + "serial" => "S", + "proto" => "P" +); + +#-- These occur in a pulldown menu as settable values +my %sets = ( + "time" => "T" +); + +#-- These we may get on request +my %attrs = ( + "Wyx" => "R", +); + + +######################################################################################## +# +# NT5000_Initialize +# +# Parameter hash +# +######################################################################################## + +sub NT5000_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "NT5000_Define"; + $hash->{GetFn} = "NT5000_Get"; + $hash->{SetFn} = "NT5000_Set"; + # Area = Area of solar panels, to calculate expected output from solar irradiation + # PSP = Peak Solar Power of installation + # MERR = List of month entries that failed. Reason: Defective loggers in the NT5000 itself + # sometimes "jump" ahead in the monthly setting. Maybe singular problem of author ? + # Every pseudo-month-entry in this list means that its yield is ADDED to the FOLLOWING month + # e.g. MERR = 4 => Month value for April is wrong and should be added to the value from May, + # which is the following one. + # WxM1 .. WxM12 = Expected yield from January .. December + # WxY = Expected yield per year + $hash->{AttrList}= "Area PSP MERR ". + "Wx_M1 Wx_M2 Wx_M3 Wx_M4 Wx_M5 Wx_M6 Wx_M7 Wx_M8 Wx_M9 Wx_M10 Wx_M11 Wx_M12 ". + "Wx_Y ". + "loglevel:0,1,2,3,4,5,6"; +} + +####################################################################################### +######################################################################################## +# +# NT5000_Define - Implements DefFn function +# +# Parameter hash, definition string +# +######################################################################################## + +sub NT5000_Define($$) { + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + return "Define the serial device as a parameter, use none or emulator for a fake device" + if(@a != 3); + $hash->{STATE} = "Initialized"; + + my $dev = $a[2]; + + Log 1, "NT5000 device is none, commands will be echoed only" + if($dev eq "none"); + + Log 1, "NT5000 with emulator mode" + if($dev eq "emulator"); + + if( ($dev ne "none") && ($dev ne "emulator")) { + Log 3, "NT5000 opening device $dev"; + my $nt5000_serport = new Device::SerialPort ($dev); + return "NT5000 Can't open $dev: $!" if(!$nt5000_serport); + Log 2, "NT5000 opened device $dev"; + $hash->{USBDev} = $nt5000_serport; + sleep(1); + $nt5000_serport->close(); + + } + + $hash->{DeviceName} = $dev; + $hash->{Timer} = 60; # call every 60 seconds + $hash->{Cmd} = "reading"; # get all data, min/max unchange + $hash->{SerialNumber} = ""; + $hash->{Protocol} = ""; + $hash->{Firmware} = ""; + $hash->{STATE} = "offline"; + my $tn = TimeNow(); + + #-- InternalTimer blocks if init_done is not true + my $oid = $init_done; + $init_done = 1; + NT5000_GetStatus($hash); + $init_done = $oid; + return undef; +} + +######################################################################################## +# +# NT5000_Get - Implements GetFn function +# +# Parameter hash, argument array +# +######################################################################################## + +sub NT5000_Get ($@) { +my ($hash, @a) = @_; + +return "NT5000_Get needs exactly one parameter" if(@a != 2); +my $name = $hash->{NAME}; +my $v; + +if($a[1] eq "reading") + { + $v = NT5000_GetLine($hash,"reading"); + if(!defined($v)) + { + Log GetLogLevel($name,2), "NT5000_Get $a[1] error"; + return "$a[0] $a[1] => Error"; + } + $v =~ s/[\r\n]//g; # Delete the NewLine + $hash->{READINGS}{$a[1]}{VAL} = $v; + $hash->{READINGS}{$a[1]}{TIME} = TimeNow(); + } +elsif($a[1] eq "month") + { + $v = NT5000_GetLine($hash,"month"); + if(!defined($v)) + { + Log GetLogLevel($name,2), "NT5000_Get $a[1] error"; + return "$a[0] $a[1] => Error"; + } + $v =~ s/[\r\n]//g; # Delete the NewLine + $hash->{READINGS}{$a[1]}{VAL} = $v; + $hash->{READINGS}{$a[1]}{TIME} = TimeNow(); + } + elsif($a[1] eq "year") + { + $v = NT5000_GetLine($hash,"year"); + if(!defined($v)) + { + Log GetLogLevel($name,2), "NT5000_Get $a[1] error"; + return "$a[0] $a[1] => Error"; + } + $v =~ s/[\r\n]//g; # Delete the NewLine + $hash->{READINGS}{$a[1]}{VAL} = $v; + $hash->{READINGS}{$a[1]}{TIME} = TimeNow(); + } +else + { + return "NT5000_Get with unknown argument $a[1], choose one of " . join(",", sort keys %gets); + } + +Log GetLogLevel($name,3), "NT5000_Get $a[1] $v"; +return "$a[0] $a[1] => $v"; +} + +######################################################################################## +# +# NT5000_Set - Implements SetFn function +# +# Parameter hash, a = argument array +# +######################################################################################## + +sub NT5000_Set ($@) { + my ($hash, @a) = @_; + my $name = shift @a; + my $res; + + #-- for the selector: which values are possible + return join(" ", sort keys %sets) if(@a != 2); + return "NT5000_Set: With unknown argument $a[0], choose one of " . join(" ", sort keys %sets) + if(!defined($sets{$a[0]})); + + #-- Set time value + if( $a[0] eq "time" ){ + #-- only values >= 15 secs allowed + if( $a[1] >= 15){ + $res = "not yet implemented"; + } else { + $res = "not yet implemented"; + } + Log GetLogLevel($name,3), "NT5000_Set $name ".join(" ",@a)." => $res"; + DoTrigger($name, undef) if($init_done); + return "NT5000_Set => $name ".join(" ",@a)." => $res"; + } +} + +######################################################################################## +# +# NT5000 - GetStatus - Called in regular intervals to obtain current reading +# +# Parameter hash +# +######################################################################################## + +sub NT5000_GetStatus ($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + # Call us in n minutes again. + InternalTimer(gettimeofday()+ $hash->{Timer}, "NT5000_GetStatus", $hash,1); + + # Obtain the current reading + my $result = NT5000_GetLine($hash, "reading"); + + # If one of these applies we must assume that the inverter is offline (no retry !) + # Logging only if this is a change from the previous state + if( !defined($result) ) { + Log GetLogLevel($name,1), "NT5000 cannot be read, inverter offline" if( $hash->{STATE} ne "offline" ); + #Log 3, "NT5000 cannot be read, inverter offline"; + $hash->{STATE} = "offline"; + return $hash->{STATE}; + } elsif( length($result) < 13 ){ + Log GetLogLevel($name,1), "NT5000 returns incomplete line, inverter offline" if( $hash->{STATE} ne "starting" ); + #Log 3, "NT5000 returns incomplete line"; + $hash->{STATE} = "starting"; + return $hash->{STATE}; + }else { + # we have obtained a reading: inverter is online + #Log 3, "NT5000 has answered 13 bytes"; + my $tn = TimeNow(); + my @names = ("Udc", "Idc", "Pdc", "Uac", "Iac", "Pac", "Temp", "S", "Wd", "Wtot", "Eta"); + + if( !($hash->{STATE} =~ m/.*kW/) ) { + # we have turned online recently + Log GetLogLevel($name,2), "NT5000 inverter is online"; + $hash->{STATE} = "starting"; + # Obtain the serial number and protocol + my $serial = NT5000_GetLine($hash, "serial"); + $serial =~ s/^.*S://; + $serial =~ s/[\r\n ]//g; + $hash->{SerialNumber} = "$serial"; + my $proto = NT5000_GetLine($hash, "proto"); + $proto =~ s/^.*P://; + $proto =~ s/[\r\n ]//g; + $hash->{Firmware} = substr($proto,0,1).".".substr($proto,1,1); + $hash->{Protocol} = substr($proto,2,1).".".substr($proto,4,2); + + # Obtain monthly readings in 70 seconds - only once + InternalTimer(gettimeofday()+ 20, "NT5000_GetMonth", $hash,1); + + # Obtain yearly readings in 10 seconds - only once + InternalTimer(gettimeofday()+ 40, "NT5000_GetYear", $hash,1); + + my $resmod ="header: "; + #Put a header line into the log file + for(my $i = 0; $i < int(@names); $i++) { + $resmod .= $names[$i]." "; + } + $hash->{CHANGED}[$main::cline++] = "$resmod"; + }; + + #-- Log level 5 + Log GetLogLevel($name,5), "NT5000 online result = $result"; + + # All data items in one line saves a lot of place + my $resmod = $result; + #$resmod =~ s/;/ /g; + $hash->{CHANGED}[$main::cline++] = "reading: $resmod"; + + #-- split result for writing into hash + my @data = split(' ',$result); + $hash->{STATE} = sprintf("%5.3f kW",$data[5]); + for(my $i = 0; $i < int(@names); $i++) { + # This puts individual pairs into the tabular view + $hash->{READINGS}{$names[$i]}{VAL} = $data[$i]; + $hash->{READINGS}{$names[$i]}{TIME} = $tn; + } + + DoTrigger($name, undef) if($init_done); + + $result =~ s/;/ /g; + } + + return $hash->{STATE}; +} + +######################################################################################## +# +# NT5000_GetMonth - Read monthly data from inverter +# +# Parameter hash +# +######################################################################################## + +sub NT5000_GetMonth ($) { +my ($hash) = @_; +my $name = $hash->{NAME}; + + #-- Obtain the monthly reading + my $result = NT5000_GetLine($hash, "month"); + $result =~ s/^.*M://; + $result =~ s/[\r\n ]//g; + Log GetLogLevel($name,3), "NT5000 monthly result = $result"; + $result=~ s/,/./g; + my @data = split(";", $result); + my $day = $data[0]; + + #-- Expected yield for month + my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time); + my $mex = "Wx_M".($month+1); + my $wex = $attr{$name}{$mex}; + my $wac = 0; + my $wre; + + my @names = ("W_D1","W_D2","W_D3","W_D4","W_D5","W_D6","W_D7","W_D8","W_D9","W_D10", + "W_D11","W_D12","W_D13","W_D14","W_D15","W_D16","W_D17","W_D18","W_D19","W_D20", + "W_D21","W_D22","W_D23","W_D24","W_D25","W_D26","W_D27","W_D28","W_D29","W_D30","W_D31"); + + my $yearn = $year+1900; + my $monn = $month+1; + + #-- we are not sure, if this is logged now or in the next minute, hence an "or" in the regexp + $hash->{LASTM}=sprintf("%4d-%02d-%02d_%02d:(%02d|%02d)",$yearn,$monn,$day,$hour,$min,$min+1); + + for(my $i = 0; $i < $day; $i++) + { + + my $dayn = $i+1; + my $daten = $yearn."-".$monn."-".$dayn."_23:59:59"; + $wac += $data[$day-$i]; + if( $wex ) + { + $wre = int(1000*$wac/$wex)/10 if ($wex>0 ); + }; + # Put one item per line into the log file + # +1 necessary - otherwise will be overridden by the changes in the daily readings + $main::cline++; + $hash->{CHANGED}[$main::cline++] = "$names[$i]: $daten $data[$day-$i] $wac $wre"; + # This puts individual pairs into the tabular view + #$hash->{READINGS}{$names[$i]}{TIME} = $tn; + #$hash->{READINGS}{$names[$i]}{VAL} = $data[$day-$i]; + }; +} + +######################################################################################## +# +# NT5000_GetYear - Read yearly data from inverter +# +# Parameter hash +# +######################################################################################## + +sub NT5000_GetYear ($) { +my ($hash) = @_; +my $name = $hash->{NAME}; + +#-- Obtain the yearly reading + my $result = NT5000_GetLine($hash, "year"); + $result =~ s/^.*Y://; + $result =~ s/[\r\n ]//g; + Log GetLogLevel($name,3), "NT5000 yearly result = $result"; + $result=~ s/,/./g; + my @data = split(";", $result); + + #-- Expected yield for year + my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time); + my $wex = $attr{$name}{Wx_Y}; + my $wac = 0; + my $wre; + + my @names = ("W_M01","W_M02","W_M03","W_M04","W_M05","W_M06","W_M07","W_M08","W_M09","W_M10", + "W_M11","W_M12"); + + my $yearn = $year+1900; + #-- we are not sure, if this is logged now or in the next minute, hence an "or" in the regexp + $hash->{LASTY}=sprintf("%4d-%02d-%02d_%02d:(%02d|%02d)",$yearn,$month+1,$dayn,$hour,$min,$min+1); + + for(my $i = 0; $i <= $month; $i++) { + my $monn = $i+1; + my $daten = $yearn."-".$monn."-28_23:59:59"; + my $mex = "Wx_M".($monn); + my $mmex = $attr{$name}{$mex}; + $wac += $data[$month+1-$i]; + if( $wex ) + { + $wre = int(1000.0*$wac/$wex)/10 if ($wex > 0); + }; + #-- Put one item per line into the log file + # +1 necessary - otherwise will be overridden by the changes in the daily readings + $hash->{CHANGED}[$main::cline++] = "$names[$i]: $daten $data[$month+1-$i] $mmex $wac $wre"; + #-- This puts individual pairs into the tabular view + # $hash->{READINGS}{$names[$i]}{TIME} = $tn; + # $hash->{READINGS}{$names[$i]}{VAL} = $data[$i]; + }; + } + +######################################################################################## +# +# NT5000_GetLine - Read data from inverter +# +# Parameter hash, a = string parameter to define what will be asked +# +######################################################################################## + +sub NT5000_GetLine ($$) { + my ($hash,$a) = @_; + my $name = $hash->{NAME}; + + return "NT5000_GetLine: Unknown argument $a, choose one of " . join(",", sort keys %gets) + if(!defined($gets{$a})); + + my $dev = $hash->{DeviceName}; + + #-- Inverter data + my $rError = "\x00\x01\x01\x01"; + my $rOnline1 = "\x00\x01\x02\x01"; + my $rMon1 = "\x00\x01\x03\x01"; + my $rYear1 = "\x00\x01\x04\x01"; + my $rTime = "\x00\x01\x06\x01"; + my $rSerial = "\x00\x01\x08\x01"; + my $rProFW = "\x00\x01\x09\x01"; + my $sYY = "\x00\x01\x50"; + my $sLL = "\x00\x01\x51"; + my $sDD = "\x00\x01\x52"; + my $sHH = "\x00\x01\x53"; + my $sMM = "\x00\x01\x54"; + + my @invBuffer; + + #------------------------ current readings ------------------------------------- + if( $a eq "reading" ) + { + #Log 3, "Asking for online data"; + my $invReturn = NT5000_5to13($hash,$dev,$rOnline1); + #-- test if this is an offline case + if( !($invReturn) ) + { + return undef; + } + @invBuffer=split(//,$invReturn); + #-- test again if this is an offline case + if( @invBuffer < 13 ) + { + return undef; + } + #-- Process data + my $udc = ord($invBuffer[0])*2.8+100; + my $idc = ord($invBuffer[1])*0.08; + my $uac = ord($invBuffer[2])+100.0; + my $iac = ord($invBuffer[3])*0.120; + my $t = ord($invBuffer[4])-40.0; + my $s = ord($invBuffer[5])*6.0; + my $pdc = int($udc*$idc)/1000; + my $pac = int($uac*$iac)/1000; + my $wd = (ord($invBuffer[6])* 256 + ord($invBuffer[7]))/1000; + my $wtot= ord($invBuffer[8])* 256 + ord($invBuffer[9]); + #-- Calculate eta + my $name= $hash->{NAME}; + my $a = $attr{$name}{Area}; + my $eta; + if ( $s && $a ) { + if( ($a>0)&&($s>0) ) { + $eta = int(100000*$pac/($s*$a))/100; + } else { + $eta = 0; + }; + } else { + $eta=0; + } + return sprintf "%3.1f %2.2f %1.3f %3.1f %2.2f %1.3f %2.0f %4.0f %2.3f %5.1f %1.2f", + $udc,$idc,$pdc,$uac,$iac,$pac,$t,$s,$wd,$wtot,$eta; +#------------------------ montly readings ------------------------------------- + } elsif( $a eq "month" ) { + my $i=1; + #-- Get the first block anyhow + my $ica=$i%256; + my $cmd2=sprintf("%s%c",substr($rMon1,0,3),$ica); + my $invReturn = NT5000_5to13($hash,$dev,$cmd2); + #-- test if this is an offline case + if( !($invReturn) ){ + return undef; + } + @invBuffer=split(//,$invReturn); + #-- test again if this is an offline case + if( @invBuffer < 13 ) { + return undef; + } + #-- Process data + my $day = ord($invBuffer[1]); + my $result="M:$day;"; + for( my $j=0; $j<3; $j++ ) { + $result .= ((ord($invBuffer[2+4*$j])* 256 + ord($invBuffer[3+4*$j]))/1000).";" if( ($day-$j) > 0); + } + #-- Get further blocks if necessary + for( $i=2; $i<=($day+2)/3; $i++) { + $ica=$i%256; + $cmd2=sprintf("%s%c",substr($rMon1,0,3),$ica); + $invReturn = NT5000_5to13($hash,$dev,$cmd2); + #-- test if this is an offline case + if( !($invReturn) ) { + return undef; + } + @invBuffer=split(//,$invReturn); + #-- test again if this is an offline case + if( @invBuffer < 13 ) { + return undef; + } + for( my $j=0; $j<3; $j++ ) { + $result .= ((ord($invBuffer[2+4*$j])* 256 + ord($invBuffer[3+4*$j]))/1000).";" if( ($day-($i-1)*3-$j) > 0); + } + }; + return "$result\n"; +#------------------------ yearly readings ------------------------------------- + } elsif( $a eq "year" ) { + my $i=1; + #-- We read the full data, e.g. current month (cm) .. cm-12 + my @pmval; + #-- Get the first block + my $ica=$i%256; + my $cmd2=sprintf("%s%c",substr($rYear1,0,3),$ica); + my $invReturn = NT5000_5to13($hash,$dev,$cmd2); + #-- test if this is an offline case + if( !($invReturn) ) { + return undef; + } + @invBuffer=split(//,$invReturn); + #-- test again if this is an offline case + if( @invBuffer < 13 ) { + return undef; + } + #-- Process data for current month (cm) .. cm-4 + my $month = ord($invBuffer[1]); + for( my $j=0; $j<5; $j++ ) { + #-- value for pseudo-month + push(@pmval,(ord($invBuffer[2+2*$j])* 256 + ord($invBuffer[3+2*$j]))/10); + } + #-- Get the second block + $i++; + $ica=$i%256; + $cmd2=sprintf("%s%c",substr($rYear1,0,3),$ica); + $invReturn = NT5000_5to13($hash,$dev,$cmd2); + #-- test if this is an offline case + if( !($invReturn) ) { + return undef; + } + @invBuffer=split(//,$invReturn); + #-- test again if this is an offline case + if( @invBuffer < 13 ) { + return undef; + } + #-- Process data for cm-5 .. cm-10 + for( my $j=0; $j<6; $j++ ) { + #-- value for pseudo-month + push(@pmval,(ord($invBuffer[2*$j])* 256 + ord($invBuffer[1+2*$j]))/10); + }; + #-- Get the third block + $i++; + $ica=$i%256; + $cmd2=sprintf("%s%c",substr($rYear1,0,3),$ica); + $invReturn = NT5000_5to13($hash,$dev,$cmd2); + #-- test if this is an offline case + if( !($invReturn) ) { + return undef; + } + @invBuffer=split(//,$invReturn); + #-- test again if this is an offline case + if( @invBuffer < 13 ) { + return undef; + } + #-- Process data for cm-11 .. cm-12 + for( my $j=0; $j<2; $j++ ) { + #-- value for pseudo-month + push(@pmval,(ord($invBuffer[2*$j])* 256 + ord($invBuffer[1+2*$j]))/10); + }; + #-- Now we have to correct for those erroneous jumps of the internal data logger + # of the NT5000 + # The first one is never wrong, belongs to the current month + my @val = ($pmval[0]); + my $result; + # which and how many pseudo-month-entries do we have ? + my $merr = $attr{$name}{MERR}; + # none, we my return HERE + if( !defined($merr) ) { + @val= ($pmval[0]); + for( my $j=1; $j<=$month; $j++ ){ + push(@val,$pmval[$j]); + } + $result = "Y:$month;".join(';',@val); + return $result; + }; + # oops, correction has to be done + my @merrs = split(',',$merr); + my $merrno = @merrs; + #-- For the year we therefore need the first $month+$merrno entries + + my $pm; + my $mlim = $month+$merrno; + for( my $j=1; $j<$mlim; $j++ ) { + $pm = $month-$j; + my $listed = 0; + #-- check if this is in the list + for( my $k=0; $k<$merrno; $k++ ) { + # yes, it is + if( $merrs[$k]==$pm ) { + $listed = 1; + } + } + # yes, it is indeed + if( $listed==1) { + # add data to the last entry in @val + $val[@val-1]+=$pmval[$j]; + # no, it is not + } else { + # append value to array + push(@val,$pmval[$j]); + } + }; + #-- Compare the results + #Log 3, "YEAR PSEUDO ".join(';',@pmval); + #Log 3, "YEAR CORR ".join(';',@val); + $result = "Y:$month;".join(';',@val); + return $result; + }elsif( $a eq "serial" ) { + my $r1 = NT5000_5to13($hash,$dev,$rSerial); + return "S:".substr($r1,0,12)."\n" + }elsif( $a eq "proto" ){ + my $r1 = NT5000_5to13($hash,$dev,$rProFW); + return "P:".substr($r1,0,6)."\n" + }else { + print "OHOH => NT5000_GetLine mit Argument $a\n"; + } +} + +######################################################################################## +# +# NT5000_5to13 - Read 13 bytes from the inverter or the emulator +# +# Parameter: hash,dev = none,emulator or a serial port definition +# cmd = 5 byte parameter to query the device properly +# +######################################################################################## +sub +NT5000_5to13($$$) +{ + +my $retry = 0; +my ($hash,$dev,$cmd) = @_; + +my $result; +my ($i,$j,$k); + +if( $dev eq "none" ) #no inverter attached + { + return "\x00\x01\x02\x03\x04\x05\x06\x07\x07\x09\x0a\x0b\x0c"; + + } elsif ( $dev eq "emulator" ) #emulator attached + { + #-- read from emulator + + #-- calculate checksum + my $CS = unpack("%32C*", $cmd); + $CS=$CS%256; + my $cmd2=sprintf("%s%c",$cmd,$CS); + #-- control + #print "Sending out:\n"; + #for(my $i=0;$i<5;$i++) + # { my $j=int(ord(substr($cmd2,$i,1))/16); + # my $k=ord(substr($cmd2,$i,1))%16; + # print "byte $i = 0x$j$k\n"; + # } + + my $result = NT5000_emu(5,$cmd); + #print "[I] Answer 13 bytes received\n"; + #for($i=0;$i<13;$i++) + # { $j=int(ord($invBuffer[$i])/16); + # $k=ord($invBuffer[$i])%16; + # print "byte $i = 0x$j$k\n"; + # } + return($result); + + } else # here we do the real thing + { + #Just opening the old device does not reaaly work. + #my $serport = $hash->{USBDev}; + my $serport = new Device::SerialPort ($dev); + if(!$serport) { + Log 1, "NT5000: Can't open $dev: $!"; + return undef; + } + #Log 3, "NT5000 opened"; + $serport->reset_error(); + $serport->baudrate(9600) || die "failed setting baudrate"; + $serport->databits(8) || die "failed setting databits"; + $serport->parity('none') || die "failed setting parity"; + $serport->stopbits(1) || die "failed setting stopbits"; + $serport->handshake('none') || die "failed setting handshake"; + $serport->write_settings || die "no settings"; + + #my $rm = "NT5000 timeout reading the answer"; + + #-- calculate checksum + my $CS = unpack("%32C*", $cmd); + $CS=$CS%256; + my $cmd2=sprintf("%s%c",$cmd,$CS); + #-- control + #print "Sending out:\n"; + #for(my $i=0;$i<5;$i++) + # { my $j=int(ord(substr($cmd2,$i,1))/16); + # my $k=ord(substr($cmd2,$i,1))%16; + # print "byte $i = 0x$j$k\n"; + # } + + my $count_out = $serport->write($cmd2); + Log 3, "NT5000 write failed\n" unless ($count_out); + Log 3, "NT5000 write incomplete $count_out ne ".(length($cmd2))."\n" if ( $count_out != 5 ); + #Log 3, "write complete $count_out \n" if ( $count_out == 5 ); + #-- sleeping 0.03 seconds + select(undef,undef,undef,0.05); + my ($count_in, $string_in) = $serport->read(13); + #Log 3, "NT5000 read unsuccessful, $count_in bytes \n" unless ($count_in == 13); + #Log 3, "read complete $count_in \n" if ( $count_in == 13 ); + #-- sleeping 0.03 seconds + select(undef,undef,undef,0.05); + $serport->close(); + return($string_in); + } + +} + +######################################################################################## +# +# NT5000_emu - Emulator section - to be used, if the real solar inverter is not attached. +# +######################################################################################## + +sub NT5000_emu { +#-- For configuration purpose: when does the sun come up, and when does it go down +my $start = 6 + 55.0/60; +my $stop = 21 + 31.0/60; + +#-- Inverter data + +my $rError = "\x00\x01\x01\x01"; +my $rOnline1 = "\x00\x01\x02\x01"; +my $rMon1 = "\x00\x01\x03\x01"; +my $rYear1 = "\x00\x01\x04\x01"; +my $rTime = "\x00\x01\x06\x01"; +my $rSerial = "\x00\x01\x08\x01"; +my $rProFW = "\x00\x01\x09\x01"; +my $sYY = "\x00\x01\x50"; +my $sLL = "\x00\x01\x51"; +my $sDD = "\x00\x01\x52"; +my $sHH = "\x00\x01\x53"; +my $sMM = "\x00\x01\x54"; + +#-- Timer data +my ($sec,$min,$hour,$day,$month,$year,$wday,$yday,$isdst) = localtime(time); + +#-- parse incoming parameters +my ($count,$buf1)=@_; + +#-- default: do not send data +my $senddata=0; +my ($i,$j,$k); +my (@buf3,@buf3a); + +#-- No bytes received +if( $count == 0) + { + Log 3, "[NT5000 emulator] Zero bytes received, count=0"; + return undef; + } +#-- no sun yet +if( (($hour+$min/60.0-$start)<0) || (($stop-$hour-$min/60.0)<0) ) + { + Log 3, "[NT5000 emulator] No Sun !"; + return undef; + } +#-- 5 bytes received +elsif( $count == 5) + { + my $buf2 = substr($buf1,0,4); + my $buf4 = substr($buf1,0,3); + + #---- Error + if( $buf2 eq $rError ) + { + Log 3, "[NT5000 emulator] Request for error list received"; + my @buf3=($year,$month,$day,$hour, + $min,0,0,0,0,0,0,0); + $senddata=1; + } + + #---- Online block 1 + elsif( $buf2 eq $rOnline1 ) + { + #Log 3 "[NT5000 mulator] Request for online data block 1 received"; + my ($wd,$wdl,$wdh,$wtot,$wtotl,$wtoth,$uac,$udc,$iac,$idc,$pac,$t,$s)=(0,0,0,0,0,0,0,0,0,0,0,0,0); + #-- shift into full day + my $q = ($hour+$min/60.0-$start)/($stop-$start); + if( ($q>0) && ($q<1.0) ) + { + #-- produce fake data + $wd= int( 4500/3.14*($stop-$start)*(1-cos($q*3.14)) ); + $wdl=$wd%256; + $wdh=int(($wd-$wdl)/256); + $wtot=1000+int($wd/1000); + $wtotl=$wtot%256; + $wtoth=int(($wtot-$wtotl)/256); + + $uac=int( 230-100 + 0.5 ); + $udc=int( (600-100)/2.8 + 0.5 ); + $iac=int( 4500/230*sin($q * 3.14)/0.12 + 0.5 ); + $idc=int( 4550/600*sin($q * 3.14)/0.08 + 0.5 ); + + $pac=int(4500*sin($q*3.14))/1000.0; + + $t=10+40; + $s=int(100/6); + } + @buf3=($udc,$idc,$uac,$iac,$t,$s,$wdh,$wdl,$wtoth,$wtotl,0,0); + $senddata=1; + } + + #---- Monthly data block + elsif( substr($buf2,0,3) eq substr($rMon1,0,3) ) + { + my $ica = substr($buf2,3,1); + my $blocknr = ord($ica)%16; + # Log 3, "[NT5000 emulator] Request for monthly data block $blocknr received"; + my ($mon1,$day1,$wdh1,$wdl1,$mon2,$day2,$wdh2,$wdl2,$mon3,$day3,$wdh3,$wdl3)=(0,0,0,0,0,0,0,0,0,0,0,0,0); + + #-- produce fake data + $mon1 = $month; + $day1 = $day+3-$blocknr*3; + my $wd = (19.001 + 2*($day1 % 2))*1000.0; + $wdl1 = $wd%256; + $wdh1 = int(($wd-$wdl1)/256); + $mon2 = $mon1; + $day2 = $day1-1; + if( $day2 < 1 ) + { + $mon2-- if($mon2 > 0); + $day2 = 31; + } + $wd = (19.001 + 2*($day2 % 2))*1000.0; + $wdl2 = ($wd)%256; + $wdh2 = int(($wd-$wdl2)/256); + $mon3 = $mon2; + $day3 = $day2-1; + if( $day3 < 1 ) + { + $mon3-- if($mon3 > 0); + $day3 = 31; + } + $wd = (19.001 + 2*($day3 % 2))*1000.0; + $wdl3=$wd%256; + $wdh3=int(($wd-$wdl3)/256); + + @buf3=($mon1,$day1,$wdh1,$wdl1,$mon2,$day2,$wdh2,$wdl2,$mon3,$day3,$wdh3,$wdl3); + $senddata=1; + } + + #---- Yearly data block + elsif( substr($buf2,0,3) eq substr($rYear1,0,3) ) + { + my $ica = substr($buf2,3,1); + my $blocknr = ord($ica)%16; + Log 3, "[NT5000 emulator] Request for yearly data block $blocknr received"; + my ($wmh1,$wml1,$wmh2,$wml2,$wmh3,$wml3,$wmh4,$wml4,$wmh5,$wml5)=(0,0,0,0,0,0,0,0,0,0); + my @pwm=(1500,2500,3500,4500,6000,6000,6000,6000,4500,3500,2500,1500); + + #-- produce fake data + my $ip; + @buf3=(0,0,0,0,0,0,0,0,0,0,0,0); + if( $blocknr==1){ + $buf3[1]=$month; + for( my $i=0;$i<5;$i++) + { + my $ip=$month-$i; + if( $ip<0 ) + { + $ip += 12; + }; + my $wd=$pwm[$ip]; + my $wdl = ($wd)%256; + my $wdh = int(($wd-$wdl)/256); + $buf3[2*$i+2]=$wdh; + $buf3[2*$i+3]=$wdl; + } + }elsif( $blocknr==2){ + for( my $i=0;$i<6;$i++) + { + my $ip=$month-5-$i; + if( $ip<0 ) + { + $ip += 12; + }; + my $wd=$pwm[$ip]; + my $wdl = ($wd)%256; + my $wdh = int(($wd-$wdl)/256); + $buf3[2*$i]=$wdh; + $buf3[2*$i+1]=$wdl; + } + }else{ + for( my $i=0;$i<2;$i++) + { + my $ip=$month-11-$i; + if( $ip<0 ) + { + $ip += 12; + }; + my $wd=$pwm[$ip]; + my $wdl = ($wd)%256; + my $wdh = int(($wd-$wdl)/256); + $buf3[2*$i]=$wdh; + $buf3[2*$i+1]=$wdl; + } + } + $senddata=1; + } + + #---- Time data + elsif( $buf2 eq $rTime ) + { + Log 3, "[NT5000 emulator] Request for time data received"; + @buf3=($year,$month,$day,$hour, + $min,0,0,0,0,0,0,0,0); + $senddata=1; + } + + #---- Serial number + elsif( $buf2 eq $rSerial ) + { + Log 3, "[NT5000 emulator] Request for serial number received"; + @buf3 = (ord('1'),ord('5'),ord('3'),ord('3'),ord('A'),ord('5'),ord('0'),ord('1'),ord('2'),ord('3'),ord('4'),ord('5')); + $senddata=1; + } + + #---- Protocol and Firmware + elsif( $buf2 eq $rProFW ) + { + Log 3, "[NT5000 emulator] Request for protocol version received"; + @buf3=(ord('1'),ord('1'),ord('1'),ord('-'),ord('2'),ord('3'),0,0,0,0,0,0); + $senddata=1; + } + #---- Set year + elsif( $buf4 eq $sYY ) + { + $year=ord(substr($buf2,3,1)); + Log 3, "[NT5000 eulator] Setting year to $year"; + } + #---- Set month + elsif( $buf4 eq $sLL ) + { + $month=ord(substr($buf2,3,1)); + Log 3, "[NT5000 emulator] Setting month to $month"; + } + #---- Set day + elsif( $buf4 eq $sDD ) + { + $day=ord(substr($buf2,3,1)); + Log 3, "[NT5000 emulator] Setting day to $day"; + } + #---- Set hour + elsif( $buf4 eq $sHH ) + { + $hour=ord(substr($buf2,3,1))-1; + Log 3, "[NT5000 emulator] Setting hour to $hour"; + } + #---- Set minute + elsif( $buf4 eq $sMM ) + { + $min=ord(substr($buf2,3,1))-1; + Log 3, "[NT5000 emulator] Setting minute to $min"; + } + #---- show content + else + { + Log 3, "[NT5000 emulator] Unknown request of 5 bytes received"; + for($i=0;$i<5;$i++) + { $j=int(ord(substr($buf2,$i,1))/16); + $k=ord(substr($buf2,$i,1))%16; + print "byte $i = 0x$j$k\n"; + } + } + #---- Other number of bytes received + } else + { + Log 3, "[NT5000 emulator] $count bytes received"; + for($i=0;$i<$count;$i++) + { $j=int(ord(substr($buf1,$i,1))/16); + $k=ord(substr($buf1,$i,1))%16; + print "byte $i = 0x$j$k\n"; + } + } + #-- Here we are really sending data back to the main program + if( $senddata==1 ) { + #-- calculate checksum + my $CS=0; + for($i=0,$i<=11,$i++) + { + $CS+=$buf3[$i]; + } + $CS=$CS%256; + my $data = sprintf("%c%c%c%c%c%c%c%c%c%c%c%c%c", + $buf3[0],$buf3[1],$buf3[2],$buf3[3], + $buf3[4],$buf3[5],$buf3[6],$buf3[7], + $buf3[8],$buf3[9],$buf3[10],$buf3[11],$CS); + + #-- control + #print "Sending out:"; + #for($i=0;$i<13;$i++) + # { $j=int(ord(substr($data,$i,1))/16); + # $k=ord(substr($data,$i,1))%16; + # print "byte $i = 0x$j$k\n"; + # } + $senddata=0; + return($data); + } + else + { + return undef; + } +} + + + +1; diff --git a/contrib/contrib/70_SolarView.pm b/contrib/contrib/70_SolarView.pm new file mode 100644 index 000000000..09ff46014 --- /dev/null +++ b/contrib/contrib/70_SolarView.pm @@ -0,0 +1,354 @@ +############################################################################## +# +# 70_SolarView.pm +# +# A FHEM module to read power/energy values from solarview. +# +# written 2012 by Tobe Toben +# +# $Id$ +# +############################################################################## +# +# SolarView is a powerful datalogger for photovoltaic systems that runs on +# an AVM Fritz!Box (and also on x86 systems). For details see the SV homepage: +# http://www.amhamberg.de/solarview_fritzbox.aspx +# +# SV supports many different inverters. To read the SV power values using +# this module, a TCP-Server must be enabled for SV by adding the parameter +# "-TCP " to the startscript (see the SV manual). +# +# usage: +# define SolarView [ []] +# +# If is positive, new values are read every seconds. +# If is 0, new values are read whenever a get request is called +# on . The default for is 300 (i.e. 5 minutes). +# +# get +# +# where is one of currentPower, totalEnergy, totalEnergyDay, +# totalEnergyMonth, totalEnergyYear, UDC, IDC, UDCB, IDCB, UDCC, IDCC, +# gridVoltage, gridCurrent and temperature. +# +############################################################################## +# +# Copyright notice +# +# (c) 2012 Tobe Toben +# +# This script is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# This copyright notice MUST APPEAR in all copies of the script! +# +############################################################################## + +package main; + +use strict; +use warnings; + +use IO::Socket::INET; + +my @gets = ('totalEnergyDay', # kWh + 'totalEnergyMonth', # kWh + 'totalEnergyYear', # kWh + 'totalEnergy', # kWh + 'currentPower', # W + 'UDC', 'IDC', 'UDCB', # V, A, V + 'IDCB', 'UDCC', 'IDCC', # A, V, A + 'gridVoltage', 'gridCurrent', # V, A + 'temperature'); # oC + +sub +SolarView_Initialize($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "SolarView_Define"; + $hash->{UndefFn} = "SolarView_Undef"; + $hash->{GetFn} = "SolarView_Get"; + $hash->{AttrList} = "loglevel:0,1,2,3,4,5"; +} + +sub +SolarView_Define($$) +{ + my ($hash, $def) = @_; + + my @args = split("[ \t]+", $def); + + if (int(@args) < 4) + { + return "SolarView_Define: too few arguments. Usage:\n" . + "define SolarView [ []]"; + } + + $hash->{Host} = $args[2]; + $hash->{Port} = $args[3]; + $hash->{Interval} = int(@args) >= 5 ? int($args[4]) : 300; + $hash->{Timeout} = int(@args) >= 6 ? int($args[5]) : 4; + + # config variables + $hash->{Invalid} = -1; # default value for invalid readings + $hash->{Sleep} = 0; # seconds to sleep before connect + $hash->{Debounce} = 50; # minimum level for debouncing (0 to disable) + $hash->{Rereads} = 2; # number of retries when reading curPwr of 0 + $hash->{NightOff} = 'yes'; # skip connection to SV at night? + $hash->{UseSVNight} = 'yes'; # use the on/off timings from SV (else: SUNRISE_EL) + $hash->{UseSVTime} = ''; # use the SV time as timestamp (else: TimeNow()) + + # internal variables + $hash->{Debounced} = 0; + + $hash->{STATE} = 'Initializing'; + + my $timenow = TimeNow(); + + for my $get (@gets) + { + $hash->{READINGS}{$get}{VAL} = $hash->{Invalid}; + $hash->{READINGS}{$get}{TIME} = $timenow; + } + + SolarView_Update($hash); + + Log 2, "$hash->{NAME} will read from solarview at $hash->{Host}:$hash->{Port} " . + ($hash->{Interval} ? "every $hash->{Interval} seconds" : "for every 'get $hash->{NAME} ' request"); + + return undef; +} + +sub +SolarView_Update($) +{ + my ($hash) = @_; + + if ($hash->{Interval} > 0) { + InternalTimer(gettimeofday() + $hash->{Interval}, "SolarView_Update", $hash, 0); + } + + # if NightOff is set and there has been a successful + # reading before, then skip this update "at night" + # + if ($hash->{NightOff} and SolarView_IsNight($hash) and + $hash->{READINGS}{currentPower}{VAL} != $hash->{Invalid}) + { + $hash->{STATE} = '0 W, '.$hash->{READINGS}{totalEnergyDay}{VAL}.' kWh (Night)'; + return undef; + } + + sleep($hash->{Sleep}) if $hash->{Sleep}; + + Log 4, "$hash->{NAME} tries to contact solarview at $hash->{Host}:$hash->{Port}"; + + my $success = 0; + my %readings = (); + my $timenow = TimeNow(); + my $rereads = $hash->{Rereads}; + + eval { + local $SIG{ALRM} = sub { die 'timeout'; }; + alarm $hash->{Timeout}; + + READ_SV: + my $socket = IO::Socket::INET->new(PeerAddr => $hash->{Host}, + PeerPort => $hash->{Port}, + Timeout => $hash->{Timeout}); + + if ($socket and $socket->connected()) + { + $socket->autoflush(1); + print $socket "00*\r\n"; + my $res = <$socket>; + close($socket); + + if ($res and $res =~ /^\{(00,[\d\.,]+)\},/) + { + my @vals = split(/,/, $1); + + if ($hash->{UseSVTime}) + { + $timenow = sprintf("%04d-%02d-%02d %02d:%02d:00", + $vals[3], $vals[2], $vals[1], $vals[4], $vals[5]); + } + + for my $i (6..19) + { + if (defined($vals[$i])) + { + $readings{$gets[$i - 6]} = 0 + $vals[$i]; + } + } + + if ($rereads and $readings{currentPower} == 0) + { + sleep(1); + $rereads = $rereads - 1; + goto READ_SV; + } + + alarm 0; + + # if Debounce is enabled (>0), then skip one! drop of + # currentPower from 'greater than Debounce' to 'Zero' + # + if ($hash->{Debounce} > 0 and + $hash->{Debounce} < $hash->{READINGS}{currentPower}{VAL} and + $readings{currentPower} == 0 and not $hash->{Debounced}) + { + # revert to the previous value + $readings{currentPower} = $hash->{READINGS}{currentPower}{VAL}; + $hash->{Debounced} = 1; + } else { + $hash->{Debounced} = 0; + } + + $success = 1; + } + } + }; + + alarm 0; + + if ($success) + { + for my $get (@gets) + { + # update and notify readings if they have changed + if ($hash->{READINGS}{$get}{VAL} != $readings{$get}) + { + $hash->{READINGS}{$get}{VAL} = $readings{$get}; + $hash->{READINGS}{$get}{TIME} = $timenow; + # + push @{$hash->{CHANGED}}, "$get: $readings{$get}"; + } + } + DoTrigger($hash->{NAME}, undef) if ($init_done); + } + + $hash->{STATE} = $hash->{READINGS}{currentPower}{VAL}.' W, '.$hash->{READINGS}{totalEnergyDay}{VAL}.' kWh'; + + if ($success) { + Log 4, "$hash->{NAME} got fresh values from solarview"; + } else { + $hash->{STATE} .= ' (Fail)'; + Log 4, "$hash->{NAME} was unable to get fresh values from solarview"; + } + + return undef; +} + +sub +SolarView_Get($@) +{ + my ($hash, @args) = @_; + + return 'SolarView_Get needs two arguments' if (@args != 2); + + SolarView_Update($hash) unless $hash->{Interval}; + + my $get = $args[1]; + my $val = $hash->{Invalid}; + + if (defined($hash->{READINGS}{$get})) { + $val = $hash->{READINGS}{$get}{VAL}; + } else { + return "SolarView_Get: no such reading: $get"; + } + + Log 3, "$args[0] $get => $val"; + + return $val; +} + +sub +SolarView_Undef($$) +{ + my ($hash, $args) = @_; + + RemoveInternalTimer($hash) if $hash->{Interval}; + + return undef; +} + +sub +SolarView_IsNight($) +{ + my ($hash) = @_; + + my $isNight = 0; + + my ($sec,$min,$hour,$mday,$mon) = localtime(time); + + # reset totalEnergyX if needed + if ($hour == 0) + { + my $timenow = TimeNow(); + + $hash->{READINGS}{totalEnergyDay}{VAL} = 0; + $hash->{READINGS}{totalEnergyDay}{TIME} = $timenow; + # + if ($mday == 1) + { + $hash->{READINGS}{totalEnergyMonth}{VAL} = 0; + $hash->{READINGS}{totalEnergyMonth}{TIME} = $timenow; + # + if ($mon == 0) + { + $hash->{READINGS}{totalEnergyYear}{VAL} = 0; + $hash->{READINGS}{totalEnergyYear}{TIME} = $timenow; + } + } + } + + if ($hash->{UseSVNight}) + { + # These are the on/off timings from Solarview, see + # http://www.amhamberg.de/solarview-fb_Installieren.pdf + # + if ($mon == 0) { # Jan + $isNight = ($hour < 7 or $hour > 17); + } elsif ($mon == 1) { # Feb + $isNight = ($hour < 7 or $hour > 18); + } elsif ($mon == 2) { # Mar + $isNight = ($hour < 6 or $hour > 19); + } elsif ($mon == 3) { # Apr + $isNight = ($hour < 5 or $hour > 20); + } elsif ($mon == 4) { # May + $isNight = ($hour < 5 or $hour > 21); + } elsif ($mon == 5) { # Jun + $isNight = ($hour < 5 or $hour > 21); + } elsif ($mon == 6) { # Jul + $isNight = ($hour < 5 or $hour > 21); + } elsif ($mon == 7) { # Aug + $isNight = ($hour < 5 or $hour > 21); + } elsif ($mon == 8) { # Sep + $isNight = ($hour < 6 or $hour > 20); + } elsif ($mon == 9) { # Oct + $isNight = ($hour < 7 or $hour > 19); + } elsif ($mon == 10) { # Nov + $isNight = ($hour < 7 or $hour > 17); + } elsif ($mon == 11) { # Dec + $isNight = ($hour < 8 or $hour > 16); + } + } else { # we use SUNRISE_EL + $isNight = not isday(); + } + + return $isNight; +} + +1; + diff --git a/contrib/contrib/86_FS10.pm b/contrib/contrib/86_FS10.pm new file mode 100644 index 000000000..29aaa710b --- /dev/null +++ b/contrib/contrib/86_FS10.pm @@ -0,0 +1,166 @@ +############################################## +package main; + +use strict; +use warnings; +use Device::SerialPort; +use IO::Socket::INET; + +my $fs10data = ""; +my $pcwsdsocket; + + +##################################### +sub +FS10_Initialize($) +{ + my ($hash) = @_; + + # Consumer + $hash->{DefFn} = "FS10_Define"; + $hash->{AttrList}= "model:FS10 loglevel:0,1,2,3,4,5,6"; +} + +##################################### +sub +FS10_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + Log 3, "FS10 Define: $a[0] $a[1] $a[2] $a[3]"; + + return "Define the host and portnr as a parameter i.e. 127.0.0.1 4711" + if(@a != 4); + + $hash->{Timer} = 600; + $hash->{Host} = $a[2]; + $hash->{Port} = $a[3]; + $hash->{STATE} = "Initialized"; + + my $dev = $a[2]; + Log 1, "FS10 device is none, commands will be echoed only" + if($dev eq "none"); + + $hash->{DeviceName} = $dev; + + FS10_GetStatus($hash); + return undef; +} + + +##################################### +sub +FS10_GetStatus($) +{ + my ($hash) = @_; + my $buf; + #my $banner; + my $reqcmd; + my $fs10time; + my $dt; + my $x; + my $result = ""; + + Log 3, "FS10_GetStatus"; + + # Call us in 5 minutes again. + InternalTimer(gettimeofday()+300, "FS10_GetStatus", $hash, 0); + + my $dnr = $hash->{DEVNR}; + my $name = $hash->{NAME}; + my $host = $hash->{Host}; + my $port = $hash->{Port}; + my %vals; + my $pcwsd ="$host:$port"; + my $pcwsdsocket = IO::Socket::INET->new( $pcwsd ) + or return "FS10 Can't bind to pcwsd" if(!$pcwsdsocket); + + my $banner = $pcwsdsocket->getline(); + my @x = split(" ", $banner); + my @y; + my $fs10name; + + for(my $i = 0; $i < 8; $i++) #Outdoor + { + $fs10name ="Ta$i"; + $reqcmd = "get od2temp $i\r\n"; + $pcwsdsocket->print($reqcmd); + $buf = $pcwsdsocket->getline(); + $result = "$result $buf"; + + @x = split(" ", $buf); + $fs10time = FmtDateTime($x[1]); + + $hash->{CHANGED}[$i] = "Ta$i: $x[0]"; + $hash->{READINGS}{$fs10name}{TIME} = $fs10time; + $hash->{READINGS}{$fs10name}{VAL} = $x[0]; + } + + $fs10name="Ti"; + $reqcmd = "get idtemp 7\r\n"; + $pcwsdsocket->print($reqcmd); + $buf = $pcwsdsocket->getline(); + @x = split(" ", $buf); + $fs10time = FmtDateTime($x[1]); + + $hash->{CHANGED}[8] = "Ti: $x[0]"; + $hash->{READINGS}{$fs10name}{TIME} = $fs10time; + $hash->{READINGS}{$fs10name}{VAL} = $x[0]; + + $fs10name="Rain"; + $reqcmd = "get rain 7\r\n"; + $pcwsdsocket->print($reqcmd); + $buf = $pcwsdsocket->getline(); + @x = split(" ", $buf); + $fs10time = FmtDateTime($x[1]); + + $hash->{CHANGED}[9] = "Rain: $x[0]"; + $hash->{READINGS}{$fs10name}{TIME} = $fs10time; + $hash->{READINGS}{$fs10name}{VAL} = $x[0]; + + $fs10name="Sun"; + $reqcmd = "get bright 7\r\n"; + $pcwsdsocket->print($reqcmd); + $buf = $pcwsdsocket->getline(); + @x = split(" ", $buf); + $fs10time = FmtDateTime($x[1]); + + $hash->{CHANGED}[10] = "Sun: $x[0]"; + $hash->{READINGS}{$fs10name}{TIME} = $fs10time; + $hash->{READINGS}{$fs10name}{VAL} = $x[0]; + + $fs10name="Windspeed"; + $reqcmd = "get wspd 7\r\n"; + $pcwsdsocket->print($reqcmd); + $buf = $pcwsdsocket->getline(); + @x = split(" ", $buf); + $fs10time = FmtDateTime($x[1]); + + $hash->{CHANGED}[11] = "Windspeed: $x[0]"; + $hash->{READINGS}{$fs10name}{TIME} = $fs10time; + $hash->{READINGS}{$fs10name}{VAL} = $x[0]; + + close($pcwsdsocket); + + $result =~ s/[\r\n]//g; + DoTrigger($name, undef) if($init_done); + + $hash->{STATE} = "$result"; + Log 3,"FS10 Result: $result"; + return $hash->{STATE}; +} + +##################################### +sub +FS10Log($$) +{ + my ($a1, $a2) = @_; + + #define n31 notify fs10 {FS10Log("@", "%")} + #define here notify action + + Log 2,"FS10 $a1 = $a2 old: $oldvalue{$a1}{TIME}=> $oldvalue{$a1}{VAL});"; +} + +1; diff --git a/contrib/contrib/90_EIBUPDOWN.pm b/contrib/contrib/90_EIBUPDOWN.pm new file mode 100644 index 000000000..68fcc0ad0 --- /dev/null +++ b/contrib/contrib/90_EIBUPDOWN.pm @@ -0,0 +1,211 @@ +############################################## +package main; + +use strict; +use warnings; + +my %eib_c2b1 = ( + "alloff" => "00", + "off" => "01", + "on" => "00", + "up" => "01", + "down" => "00", + "up-for-timer" => "01", + "down-for-timer" => "00", +); + +my %eib_c2b2 = ( + "alloff" => "00", + "off" => "00", + "on" => "01", + "up" => "00", + "down" => "01", + "up-for-timer" => "00", + "down-for-timer" => "01", +); + + +my %readonly = ( + "dummy" => 1, +); + +my $eib_simple ="alloff off on up down up-for-timer down-for-timer"; +my %models = ( +); + +sub +EIBUPDOWN_Initialize($) +{ + my ($hash) = @_; + + $hash->{Match} = "^B.*"; + $hash->{SetFn} = "EIBUPDOWN_Set"; + $hash->{StateFn} = "EIBUPDOWN_SetState"; + $hash->{DefFn} = "EIBUPDOWN_Define"; + $hash->{UndefFn} = "EIBUPDOWN_Undef"; + $hash->{ParseFn} = "EIBUPDOWN_Parse"; + $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:0,1 dummy:1,0 showtime:1,0 model:EIB loglevel:0,1,2,3,4,5,6"; + +} + + +############################# +sub +EIBUPDOWN_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + my $u = "wrong syntax: define EIBUPDOWN "; + + return $u if(int(@a) < 4); + return "Define $a[0]: wrong up group name format: specify as 0-255/0-255/0-255" + if( ($a[2] !~ m/^[0-9]{1,3}\/[0-9]{1,3}\/[0-9]{1,3}$/i)); + + return "Define $a[0]: wrong down group name format: specify as 0-255/0-255/0-255" + if( ($a[3] !~ m/^[0-9]{1,3}\/[0-9]{1,3}\/[0-9]{1,3}$/i)); + + my $groupname_up = eibupdown_name2hex($a[2]); + my $groupname_down = eibupdown_name2hex($a[3]); + + $hash->{GROUP_UP} = lc($groupname_up); + $hash->{GROUP_DOWN} = lc($groupname_down); + + my $code = "$groupname_up$groupname_down"; + my $ncode = 1; + my $name = $a[0]; + + $hash->{CODE}{$ncode++} = $code; + $modules{EIB}{defptr}{$code}{$name} = $hash; + + AssignIoPort($hash); +} + +############################# +sub +EIBUPDOWN_Undef($$) +{ + my ($hash, $name) = @_; + + foreach my $c (keys %{ $hash->{CODE} } ) { + $c = $hash->{CODE}{$c}; + + # As after a rename the $name may be different from the $defptr{$c}{$n} + # we look for the hash. + foreach my $dname (keys %{ $modules{EIB}{defptr}{$c} }) { + delete($modules{EIB}{defptr}{$c}{$dname}) + if($modules{EIB}{defptr}{$c}{$dname} == $hash); + } + } + return undef; +} + +##################################### +sub +EIBUPDOWN_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + $val = $1 if($val =~ m/^(.*) \d+$/); + return "Undefined value $val" if(!defined($eib_c2b1{$val})); + return undef; +} + +################################### +sub +EIBUPDOWN_Set($@) +{ + my ($hash, @a) = @_; + my $ret = undef; + my $na = int(@a); + + return "no set value specified" if($na < 2 || $na > 3); + return "Readonly value $a[1]" if(defined($readonly{$a[1]})); + + my $c_off = $eib_c2b1{"alloff"}; + my $c_up = $eib_c2b1{$a[1]}; + my $c_down = $eib_c2b2{$a[1]}; + if(!defined($c_off) || !defined($c_up) || !defined($c_down)) { + return "Unknown argument $a[1], choose one of " . + join(" ", sort keys %eib_c2b1); + } + + my $v = join(" ", @a); + Log GetLogLevel($a[0],2), "EIB set $v"; + (undef, $v) = split(" ", $v, 2); # Not interested in the name... + + # first of all switch off all channels + # just for being sure + IOWrite($hash, "B", "w" . $hash->{GROUP_UP} . $c_off); + select(undef,undef,undef,0.5); + IOWrite($hash, "B", "w" . $hash->{GROUP_DOWN} . $c_off); + select(undef,undef,undef,0.5); + + # now switch on the right channel + if($c_up ne $c_off) { + IOWrite($hash, "B", "w" . $hash->{GROUP_UP} . $c_up); + } + elsif($c_down ne $c_off) { + IOWrite($hash, "B", "w" . $hash->{GROUP_DOWN} . $c_down); + } + + ########################################### + # Delete any timer for on-for_timer + if($modules{EIB}{ldata}{$a[0]}) { + CommandDelete(undef, $a[0] . "_timer"); + delete $modules{EIB}{ldata}{$a[0]}; + } + + ########################################### + # Add a timer if any for-timer command has been chosen + if($a[1] =~ m/for-timer/ && $na == 3) { + my $dur = $a[2]; + my $to = sprintf("%02d:%02d:%02d", $dur/3600, ($dur%3600)/60, $dur%60); + $modules{EIB}{ldata}{$a[0]} = $to; + Log 4, "Follow: +$to set $a[0] alloff"; + CommandDefine(undef, $a[0] . "_timer at +$to set $a[0] alloff"); + } + + ########################## + # Look for all devices with the same code, and set state, timestamp + my $code = "$hash->{GROUP_UP}$hash->{GROUP_DOWN}"; + my $tn = TimeNow(); + foreach my $n (keys %{ $modules{EIB}{defptr}{$code} }) { + + my $lh = $modules{EIB}{defptr}{$code}{$n}; + $lh->{CHANGED}[0] = $v; + $lh->{STATE} = $v; + $lh->{READINGS}{state}{TIME} = $tn; + $lh->{READINGS}{state}{VAL} = $v; + } + return $ret; +} + +sub +EIBUPDOWN_Parse($$) +{ + my ($hash, $msg) = @_; + + Log(5,"EIBUPDOWN_Parse is not defined. msg: $msg"); + +} + +############################# +sub +eibupdown_name2hex($) +{ + my $v = shift; + my $r = $v; + Log(5, "name2hex: $v"); + if($v =~ /^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{1,3})$/) { + $r = sprintf("%01x%01x%02x",$1,$2,$3); + } + elsif($v =~ /^([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{1,3})$/) { + $r = sprintf("%01x%01x%02x",$1,$2,$3); + } + + return $r; +} + + +1; diff --git a/contrib/contrib/91_DbLog.pm b/contrib/contrib/91_DbLog.pm new file mode 100755 index 000000000..9d4b32ac9 --- /dev/null +++ b/contrib/contrib/91_DbLog.pm @@ -0,0 +1,97 @@ +############################################## +# Example for logging KS300 data into a DB. +# +# Prerequisites: +# - The DBI and the DBD:: modules must be installed. +# - a Database is created/configured +# - a db table: create table FHZLOG (TIMESTAMP varchar(20), TEMP varchar(5), +# HUM varchar(3), WIND varchar(4), RAIN varchar(8)); +# - Change the content of the dbconn variable below +# - extend your FHEM config file with +# notify .*H:.* {DbLog("@","%")} +# - copy this file into the /FHEM and restart fhem.pl +# +# If you want to change this setup, your starting point is the DbLog function + +my $dbconn = "Oracle:DBNAME:user:password"; + +package main; +use strict; +use warnings; +use DBI; + +my $dbh; + +sub DbDo($); +sub DbConnect(); + + +################################################################ +sub +DbLog_Initialize($) +{ + my ($hash) = @_; + + # Lets connect here, so we see the error at startup + DbConnect(); +} + +################################################################ +sub +DbLog($$) +{ + my ($a1, $a2) = @_; + + # a2 is like "T: 21.2 H: 37 W: 0.0 R: 0.0 IR: no" + my @a = split(" ", $a2); + my $tm = TimeNow(); + + DbDo("insert into FHZLOG (TIMESTAMP, TEMP, HUM, WIND, RAIN) values " . + "('$tm', '$a[1]', '$a[3]', '$a[5]', '$a[7]')"); +} + + +################################################################ +sub +DbConnect() +{ + return 1 if($dbh); + Log 5, "Connecting to database $dbconn"; + my @a = split(":", $dbconn); + $dbh = DBI->connect("dbi:$a[0]:$a[1]", $a[2], $a[3]); + if(!$dbh) { + Log 1, "Can't connect to $a[1]: $DBI::errstr"; + return 0; + } + Log 5, "Connection to db $a[1] established"; + return 1; +} + +################################################################ +sub +DbDo($) +{ + my $str = shift; + + return 0 if(!DbConnect()); + Log 5, "Executing $str"; + my $sth = $dbh->do($str); + if(!$sth) { + Log 2, "DB: " . $DBI::errstr; + $dbh->disconnect; + $dbh = 0; + return 0 if(!DbConnect()); +#retry + $sth = $dbh->do($str); + if($sth) + { + Log 2, "Retry ok: $str"; + return 1; + } +# + return 0; + } + return 1; +} + +1; diff --git a/contrib/contrib/95_FLOORPLAN/95_FLOORPLAN.pm b/contrib/contrib/95_FLOORPLAN/95_FLOORPLAN.pm new file mode 100644 index 000000000..ada1463f3 --- /dev/null +++ b/contrib/contrib/95_FLOORPLAN/95_FLOORPLAN.pm @@ -0,0 +1,611 @@ +################################################################################ +# 95 FLOORPLAN +# Feedback: http://groups.google.com/group/fhem-users +# Define Custom Floorplans +# Released : 26.02.2012 +# Version : 1.01 +# Revisions: +# 0001: Released to testers +# 0002: use local FP_select and FP_submit after clash with FHEMWEB update +# 0003: FP_arrange_default repaired +# 0004: WebApp-enabled links in floorplanlist, fixed message 'use of uninitialized value' (FW_pO - $FP_name) +# 0005: Change arrange-mode: When selected, display device-name instead of selection +# 0006: kicked out various routines previously copied from FHEMWEB - now using FW_*-versions thanks to addtl. global variables $FW_RET, $FW_wname, $FW_subdir, %FW_pos +# 0007: Added fp_default +# 0008: Changed name of background-picture from to fp_ to avoid display of picture in device-list at fhem-menu 'Everything' +# -> general release +# 0009: updated selection of add-device-list: suppress CUL$ only (instead of CUL.*) +# 0010: Added Style3, fp_stylesheetPrefix, fp_noMenu (Mar 13, 2012) +# 0011: Added Style4, code beautification, css review, minor $text2-fix (SVN 1342) +# 0012: Added startscreen-text when no floorplans defined, fixed startscreen-stylesheet, added div for bg-img, added arrangeByMouse (1368) +# 0013: implemented redirectCmd, fixed minor -error in html-output, fp_arrange for single web-devices, fp_arrange detail (Mar 23, 2012) +# +################################################################ +# +# Copyright notice +# +# (c) 2012 Copyright: Ulrich Maass +# All rights reserved +# +# This script free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +################################################################################ +# Usage +# define FLOORPLAN +# +# Step-by-Step HowTo - mind all is case sensitive: +# Step 1: +# define FLOORPLAN +# Example: define Groundfloor FLOORPLAN +# +# Step 2: +# store picture fp_.png in your modpath. This will be used as background-picture. +# Example: fhem/FHEM/Groundfloor.png +# +# Step 3: +# Activate 'Arrange-Mode' to have user-friendly fields to move items: +# attr fp_arrange 1 +# Delete this attribute when you're done with setup +# To make objects display, they will thereby get assigned +# attr fp_ ,,