2016-11-12 10:30:23 +00:00
#############################################################
#
2017-01-29 17:45:23 +00:00
# EQ3BT.pm (c) by Dominik Karall, 2016-2017
2016-11-12 10:30:23 +00:00
# dominik karall at gmail dot com
# $Id$
#
# FHEM module to communicate with EQ-3 Bluetooth thermostats
#
2017-12-18 21:56:11 +00:00
# Version: 2.0.3
2016-11-12 10:30:23 +00:00
#
#############################################################
#
2017-12-18 21:56:11 +00:00
# v2.0.3 - 20171218
# - FEATURE: support maxRetries and timeout attribute
# maxRetries...number of tries before error is counted
# timeout...timeout for the command
#
2017-11-18 19:48:43 +00:00
# v2.0.2 - 20171118
# - FEATURE: support remote bluetooth interfaces via SSH (thx@Cooltux!)
#
2017-03-05 10:09:10 +00:00
# v2.0.1 - 20170204
# - BUGFIX: fix lastChangeBy
# - BUGFIX: fix retry of updateStatus, updateSystemInformation
# if it BlockingCall timeouts
#
2017-01-29 17:45:23 +00:00
# v2.0.0 - 20170129
# - FEATURE: use all available bluetooth interfaces to communicate
# with the bluetooth thermostat
# - FEATURE: new reading bluetoothDevice (shows used hci device)
# - CHANGE: change maximum retries to 20
# - FEATURE: new set function resetErrorCounters
# - FEATURE: new set function resetConsumption (not today/yesterday)
# - FEATURE: new reading lastChangeBy FHEM or thermostat
# indicates who was responsible for the last change
# - FEATURE: support $readingFnAttributes
# - FEATURE: add VERSION internal and log output
# - CHANGE: updateStatus is now 3min intervall starting from
# last working updateStatus
# - BUGFIX: do not run parallel gatttool commands for the same device
#
2016-12-11 14:51:28 +00:00
# v1.1.3 - 20161211
# - BUGFIX: better error handling if no notification was received
# - BUGFIX: update system information fixed
# - CHANGE: allow multiple gatttools to be executed in parallel
# - CHANGE: remove error reading
# - CHANGE: add errorCounters based on function (update/...)
# which will be increased if reading from the thermostat
# fails 30 times for one command
# - BUGFIX: retry mechanism for commands with notifications (updateStatus)
# - BUGFIX: remain consumption values after restart
#
2016-11-12 10:30:23 +00:00
# v1.1.2 - 20161108
# - FEATURE: support set <name> eco (eco temperature)
# - FEATURE: support set <name> comfort (comfort temperature)
# - CHANGE: updated commandref
#
# v1.1.1 - 20161106
# - FEATURE: new reading consumption today/yesterday
# - FEATURE: new reading firmware which shows the current version
# - FEATURE: support set <name> mode automatic/manual
#
# v1.1.0 - 20161105
# - CHANGE: code cleanup to make support of new functions easier
# - FEATURE: support boost on/off command
# - BUGFIX: redirect stderr to stdout to avoid "Device or ressource busy"
# and other error messages in the log output, only
# if an action fails 20 times an error will be shown in the log
#
# v1.0.7 - 20161101
# - FEATURE: new reading consumption
# calculation based on valvePosition and time (unit = %h)
# - FEATURE: new reading battery
# - FEATURE: new reading boost
# - FEATURE: new reading windowOpen
# - CHANGE: change mode reading to Automatic/Manual only
# - FEATURE: new reading ecoMode (=holiday)
#
# v1.0.6 - 20161028
# - BUGFIX: support temperature down to 4.5 (=OFF) degrees
#
# v1.0.5 - 20161027
# - BUGFIX: fix wrong date/time after updateStatus again
#
# v1.0.4 - 20161025
# - BUGFIX: remove unnecessary scan command on define
#
# v1.0.3 - 20161024
# - BUGFIX: another fix for retry mechanism
# - BUGFIX: wait before gatttool execution when
# another gatttool/hcitool process is running
# - BUGFIX: fix wrong date/time after updateStatus
#
# v1.0.2 - 20161020
# - FEATURE: automatically pair/trust device on define
# - FEATURE: add updateStatus method to update all values
# - BUGFIX: fix retry mechanism for setDesiredTemperature
# - BUGFIX: fix valvePosition value
# - BUGFIX: fix uninitialized value error
# - BUGFIX: RemoveTimer if set desired temp works again
# - BUGFIX: set error reading to "" after it works again
# - BUGFIX: disconnect device on define (startup)
#
# v1.0.1 - 20161016
# - FEATURE: read mode/desiredTemp/valvePos every 2 hours
# might have impact on battery life!
# - CHANGED: temperature renamed to desiredTemperature
# - FEATURE: retry setTemperature 20 times if it fails
#
# v1.0.0 - 20161015
# - FEATURE: first public release
#
# NOTES
# command dec
# DONE: boost mode command 69 00/01
# temperature offset 19 (x*2)+7
# request profile 32 01-07
# vacation mode 64 ...
# system info 00 => frameType=1,version=value[1],typeCode=value[2]
# window 20 t*2 time*5
# factory reset -16
# DONE: comfort temp 67
# lock -128 00/01
# DONE: mode 64 mode<<6
# DONE: temp 65 temp*2
# timer 3...
# start FW update -96
# DONE: eco mode 68
# FW data -95 ...
# profile set 16 ...
# set tempconf 17 comfort*2 eco*2
#
# TODOs
2017-01-29 17:45:23 +00:00
# - create virtual device (wohnzimmer)
2016-11-12 10:30:23 +00:00
# - read/set eco/comfort temperature
# - read/set tempOffset
# - read/set windowOpen time settings
# - read/set profiles per day
#
#############################################################
package main ;
use strict ;
use warnings ;
use Blocking ;
use Encode ;
use SetExtensions ;
sub EQ3BT_Initialize ($) {
my ( $ hash ) = @ _ ;
$ hash - > { DefFn } = 'EQ3BT_Define' ;
$ hash - > { UndefFn } = 'EQ3BT_Undef' ;
$ hash - > { GetFn } = 'EQ3BT_Get' ;
$ hash - > { SetFn } = 'EQ3BT_Set' ;
$ hash - > { AttrFn } = 'EQ3BT_Attribute' ;
2017-12-18 21:56:11 +00:00
$ hash - > { AttrList } = 'sshHost maxRetries timeout ' .
2017-11-18 19:48:43 +00:00
$ readingFnAttributes ;
2016-11-12 10:30:23 +00:00
return undef ;
}
sub EQ3BT_Define ($$) {
#save BTMAC address
my ( $ hash , $ def ) = @ _ ;
my @ a = split ( "[ \t]+" , $ def ) ;
my $ name = $ a [ 0 ] ;
my $ mac ;
2017-11-18 19:48:43 +00:00
my $ sshHost ;
2016-11-12 10:30:23 +00:00
$ hash - > { STATE } = "initialized" ;
2017-12-18 21:56:11 +00:00
$ hash - > { VERSION } = "2.0.3" ;
2017-01-29 17:45:23 +00:00
Log3 $ hash , 3 , "EQ3BT: EQ-3 Bluetooth Thermostat " . $ hash - > { VERSION } ;
2016-11-12 10:30:23 +00:00
2017-11-18 19:48:43 +00:00
if ( int ( @ a ) > 4 ) {
return 'EQ3BT: Wrong syntax, must be define <name> EQ3BT <mac address> "<sshHost-IP>"' ;
2016-11-12 10:30:23 +00:00
} elsif ( int ( @ a ) == 3 ) {
$ mac = $ a [ 2 ] ;
$ hash - > { MAC } = $ a [ 2 ] ;
2017-11-18 19:48:43 +00:00
} elsif ( int ( @ a ) == 4 ) {
$ mac = $ a [ 2 ] ;
$ hash - > { MAC } = $ a [ 2 ] ;
$ attr { $ name } { sshHost } = $ a [ 3 ] ;
2016-11-12 10:30:23 +00:00
}
2017-01-29 17:45:23 +00:00
EQ3BT_updateHciDevicelist ( $ hash ) ;
2016-11-12 10:30:23 +00:00
BlockingCall ( "EQ3BT_pairDevice" , $ name . "|" . $ hash - > { MAC } ) ;
RemoveInternalTimer ( $ hash ) ;
2017-01-29 17:45:23 +00:00
InternalTimer ( gettimeofday ( ) + 60 , "EQ3BT_updateStatus" , $ hash , 0 ) ;
InternalTimer ( gettimeofday ( ) + 20 , "EQ3BT_updateSystemInformation" , $ hash , 0 ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
2017-01-29 17:45:23 +00:00
sub EQ3BT_updateHciDevicelist {
my ( $ hash ) = @ _ ;
2017-11-18 19:48:43 +00:00
my $ name = $ hash - > { NAME } ;
2017-01-29 17:45:23 +00:00
#check for hciX devices
$ hash - > { helper } { hcidevices } = ( ) ;
2017-11-18 19:48:43 +00:00
my @ btDevices ;
my $ sshHost = AttrVal ( $ name , "sshHost" , "none" ) ;
if ( $ sshHost ne 'none' ) {
@ btDevices = split ( "\n" , qx( ssh $sshHost 'hcitool dev' ) ) ;
} else {
@ btDevices = split ( "\n" , qx( hcitool dev ) ) ;
}
2017-01-29 17:45:23 +00:00
foreach my $ btDevLine ( @ btDevices ) {
if ( $ btDevLine =~ /hci(.)/ ) {
push ( @ { $ hash - > { helper } { hcidevices } } , $ 1 ) ;
}
}
$ hash - > { helper } { currenthcidevice } = 0 ;
readingsSingleUpdate ( $ hash , "bluetoothDevice" , "hci" . $ hash - > { helper } { hcidevices } [ $ hash - > { helper } { currenthcidevice } ] , 1 ) ;
return undef ;
}
2016-11-12 10:30:23 +00:00
sub EQ3BT_pairDevice {
my ( $ string ) = @ _ ;
my ( $ name , $ mac ) = split ( "\\|" , $ string ) ;
2017-11-18 19:48:43 +00:00
my $ sshHost = AttrVal ( $ name , "sshHost" , "none" ) ;
2016-11-12 10:30:23 +00:00
2017-11-18 19:48:43 +00:00
if ( $ sshHost ne 'none' ) {
qx( ssh $sshHost 'echo "pair $mac\\n";sleep 7;echo "trust $mac\\ndisconnect $mac\\n";sleep 2; echo "quit\\n" | bluetoothctl' ) ;
} else {
qx( echo "pair $mac\\n";sleep 7;echo "trust $mac\\ndisconnect $mac\\n";sleep 2; echo "quit\\n" | bluetoothctl ) ;
}
2016-11-12 10:30:23 +00:00
return $ name ;
}
sub EQ3BT_Attribute ($$$$) {
my ( $ mode , $ devName , $ attrName , $ attrValue ) = @ _ ;
if ( $ mode eq "set" ) {
} elsif ( $ mode eq "del" ) {
}
return undef ;
}
sub EQ3BT_Set ($@) {
#set temperature/mode/...
#BlockingCall for gatttool
#handle result from BlockingCall in separate function and
# write result into readings
#
my ( $ hash , $ name , @ params ) = @ _ ;
my $ workType = shift ( @ params ) ;
2017-01-29 17:45:23 +00:00
my $ list = "desiredTemperature:slider,4.5,0.5,29.5,1 updateStatus:noArg boost:on,off mode:manual,automatic eco:noArg comfort:noArg " .
"resetErrorCounters:noArg resetConsumption:noArg" ;
2016-11-12 10:30:23 +00:00
# check parameters for set function
if ( $ workType eq "?" ) {
return SetExtensions ( $ hash , $ list , $ name , $ workType , @ params ) ;
}
if ( $ workType eq "desiredTemperature" ) {
return "EQ3BT: desiredTemperature requires <temperature> in celsius degrees as additional parameter" if ( int ( @ params ) < 1 ) ;
return "EQ3BT: desiredTemperature supports temperatures from 4.5 - 29.5 degrees" if ( $ params [ 0 ] < 4.5 || $ params [ 0 ] > 29.5 ) ;
EQ3BT_setDesiredTemperature ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "updateStatus" ) {
$ hash - > { helper } { retryUpdateStatusCounter } = 0 ;
EQ3BT_updateStatus ( $ hash , 1 ) ;
} elsif ( $ workType eq "boost" ) {
return "EQ3BT: boost requires on/off as additional parameter" if ( int ( @ params ) < 1 ) ;
EQ3BT_setBoost ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "mode" ) {
return "EQ3BT: mode requires automatic/manual as additional parameter" if ( int ( @ params ) < 1 ) ;
EQ3BT_setMode ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "eco" ) {
EQ3BT_setEco ( $ hash ) ;
} elsif ( $ workType eq "comfort" ) {
EQ3BT_setComfort ( $ hash ) ;
2017-01-29 17:45:23 +00:00
} elsif ( $ workType eq "resetErrorCounters" ) {
EQ3BT_setResetErrorCounters ( $ hash ) ;
} elsif ( $ workType eq "resetConsumption" ) {
EQ3BT_setResetConsumption ( $ hash ) ;
2016-11-12 10:30:23 +00:00
} elsif ( $ workType eq "childlock" ) {
return "EQ3BT: childlock requires on/off as additional parameter" if ( int ( @ params ) < 1 ) ;
EQ3BT_setChildlock ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "holidaymode" ) {
return "EQ3BT: holidaymode requires YYMMDDHHMM as additional parameter" if ( int ( @ params ) < 1 ) ;
EQ3BT_setHolidaymode ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "datetime" ) {
return "EQ3BT: datetime requires YYMMDDHHMM as additional parameter" if ( int ( @ params ) < 1 ) ;
EQ3BT_setDatetime ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "window" ) {
return "EQ3BT: windows requires open/closed as additional parameter" if ( int ( @ params ) < 1 ) ;
EQ3BT_setWindow ( $ hash , $ params [ 0 ] ) ;
} elsif ( $ workType eq "program" ) {
return "EQ3BT: programming the device is not supported yet" ;
} else {
return SetExtensions ( $ hash , $ list , $ name , $ workType , @ params ) ;
}
return undef ;
}
2017-01-29 17:45:23 +00:00
### resetErrorCounters ###
sub EQ3BT_setResetErrorCounters {
2016-11-12 10:30:23 +00:00
my ( $ hash ) = @ _ ;
2017-01-29 17:45:23 +00:00
foreach my $ reading ( keys % { $ hash - > { READINGS } } ) {
if ( $ reading =~ /errorCount-.*/ ) {
readingsSingleUpdate ( $ hash , $ reading , 0 , 1 ) ;
}
}
return undef ;
2016-12-11 14:51:28 +00:00
}
2017-01-29 17:45:23 +00:00
### resetConsumption ###
sub EQ3BT_setResetConsumption {
2016-12-11 14:51:28 +00:00
my ( $ hash ) = @ _ ;
2017-01-29 17:45:23 +00:00
readingsSingleUpdate ( $ hash , "consumption" , 0 , 1 ) ;
2016-12-11 14:51:28 +00:00
return undef ;
2016-11-12 10:30:23 +00:00
}
2017-01-29 17:45:23 +00:00
### updateSystemInformation ###
sub EQ3BT_updateSystemInformation {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
2017-03-05 10:09:10 +00:00
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|updateSystemInformation|0x0411|00|listen" , "EQ3BT_processGatttoolResult" , 300 , "EQ3BT_updateSystemInformationFailed" , $ hash ) ;
2017-01-29 17:45:23 +00:00
}
2016-11-12 10:30:23 +00:00
sub EQ3BT_updateSystemInformationSuccessful {
my ( $ hash , $ handle , $ value ) = @ _ ;
2017-01-29 17:45:23 +00:00
InternalTimer ( gettimeofday ( ) + 7200 + int ( rand ( 180 ) ) , "EQ3BT_updateSystemInformation" , $ hash , 0 ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
sub EQ3BT_updateSystemInformationRetry {
my ( $ hash ) = @ _ ;
2016-12-11 14:51:28 +00:00
EQ3BT_updateSystemInformation ( $ hash ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
2017-01-29 17:45:23 +00:00
sub EQ3BT_updateSystemInformationFailed {
2016-12-11 14:51:28 +00:00
my ( $ hash ) = @ _ ;
2017-01-29 17:45:23 +00:00
InternalTimer ( gettimeofday ( ) + 7000 + int ( rand ( 180 ) ) , "EQ3BT_updateSystemInformation" , $ hash , 0 ) ;
return undef ;
2016-11-12 10:30:23 +00:00
}
2017-01-29 17:45:23 +00:00
### updateStatus ###
2016-11-12 10:30:23 +00:00
sub EQ3BT_updateStatus {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
2017-03-05 10:09:10 +00:00
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|updateStatus|0x0411|03|listen" , "EQ3BT_processGatttoolResult" , 300 , "EQ3BT_updateStatusFailed" , $ hash ) ;
2016-11-12 10:30:23 +00:00
}
sub EQ3BT_updateStatusSuccessful {
my ( $ hash , $ handle , $ value ) = @ _ ;
2017-01-29 17:45:23 +00:00
InternalTimer ( gettimeofday ( ) + 140 + int ( rand ( 60 ) ) , "EQ3BT_updateStatus" , $ hash , 0 ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
sub EQ3BT_updateStatusRetry {
my ( $ hash ) = @ _ ;
2016-12-11 14:51:28 +00:00
EQ3BT_updateStatus ( $ hash ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
2017-01-29 17:45:23 +00:00
sub EQ3BT_updateStatusFailed {
my ( $ hash , $ handle , $ value ) = @ _ ;
InternalTimer ( gettimeofday ( ) + 170 + int ( rand ( 60 ) ) , "EQ3BT_updateStatus" , $ hash , 0 ) ;
return undef ;
}
2016-11-12 10:30:23 +00:00
### setDesiredTemperature ###
sub EQ3BT_setDesiredTemperature ($$) {
my ( $ hash , $ desiredTemp ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ eq3Temp = sprintf ( "%02X" , $ desiredTemp * 2 ) ;
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|setDesiredTemperature|0x0411|41" . $ eq3Temp , "EQ3BT_processGatttoolResult" , 60 , "EQ3BT_killGatttool" , $ hash ) ;
return undef ;
}
sub EQ3BT_setDesiredTemperatureSuccessful {
my ( $ hash , $ handle , $ tempVal ) = @ _ ;
my $ temp = ( hex ( $ tempVal ) - 0x4100 ) / 2 ;
2017-01-29 17:45:23 +00:00
readingsSingleUpdate ( $ hash , "desiredTemperature" , sprintf ( "%.1f" , $ temp ) , 1 ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
sub EQ3BT_setDesiredTemperatureRetry {
my ( $ hash ) = @ _ ;
EQ3BT_retryGatttool ( $ hash , "setDesiredTemperature" ) ;
return undef ;
}
### setBoost ###
sub EQ3BT_setBoost {
my ( $ hash , $ onoff ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ data = "01" ;
$ data = "00" if ( $ onoff eq "off" ) ;
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|setBoost|0x0411|45" . $ data , "EQ3BT_processGatttoolResult" , 60 , "EQ3BT_killGatttool" , $ hash ) ;
return undef ;
}
sub EQ3BT_setBoostSuccessful {
my ( $ hash , $ handle , $ value ) = @ _ ;
my $ val = ( hex ( $ value ) - 0x4500 ) ;
readingsSingleUpdate ( $ hash , "boost" , $ val , 1 ) ;
return undef ;
}
sub EQ3BT_setBoostRetry {
my ( $ hash ) = @ _ ;
EQ3BT_retryGatttool ( $ hash , "setBoost" ) ;
return undef ;
}
### setMode ###
sub EQ3BT_setMode {
my ( $ hash , $ mode ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ data = "40" ;
$ data = "00" if ( $ mode eq "automatic" ) ;
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|setMode|0x0411|40" . $ data . "|listen" , "EQ3BT_processGatttoolResult" , 60 , "EQ3BT_killGatttool" , $ hash ) ;
return undef ;
}
sub EQ3BT_setModeSuccessful {
my ( $ hash , $ handle , $ value ) = @ _ ;
return undef ;
}
sub EQ3BT_setModeRetry {
my ( $ hash ) = @ _ ;
EQ3BT_retryGatttool ( $ hash , "setMode" ) ;
return undef ;
}
### setEco ###
sub EQ3BT_setEco {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|setEco|0x0411|44|listen" , "EQ3BT_processGatttoolResult" , 60 , "EQ3BT_killGatttool" , $ hash ) ;
return undef ;
}
sub EQ3BT_setEcoSuccessful {
my ( $ hash , $ handle , $ value ) = @ _ ;
return undef ;
}
sub EQ3BT_setEcoRetry {
my ( $ hash ) = @ _ ;
EQ3BT_retryGatttool ( $ hash , "setEco" ) ;
return undef ;
}
### setComfort ###
sub EQ3BT_setComfort {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ name . "|" . $ hash - > { MAC } . "|setComfort|0x0411|43|listen" , "EQ3BT_processGatttoolResult" , 60 , "EQ3BT_killGatttool" , $ hash ) ;
return undef ;
}
sub EQ3BT_setComfortSuccessful {
my ( $ hash , $ handle , $ value ) = @ _ ;
return undef ;
}
sub EQ3BT_setComfortRetry {
my ( $ hash ) = @ _ ;
EQ3BT_retryGatttool ( $ hash , "setEco" ) ;
return undef ;
}
### Gatttool functions ###
sub EQ3BT_retryGatttool {
my ( $ hash , $ workType ) = @ _ ;
2016-12-11 14:51:28 +00:00
$ hash - > { helper } { RUNNING_PID } = BlockingCall ( "EQ3BT_execGatttool" , $ hash - > { NAME } . "|" . $ hash - > { MAC } . "|$workType|" . $ hash - > { helper } { "handle$workType" } . "|" . $ hash - > { helper } { "value$workType" } . "|" . $ hash - > { helper } { "listen$workType" } , "EQ3BT_processGatttoolResult" , 60 , "EQ3BT_killGatttool" , $ hash ) ;
2016-11-12 10:30:23 +00:00
return undef ;
}
sub EQ3BT_execGatttool ($) {
my ( $ string ) = @ _ ;
my ( $ name , $ mac , $ workType , $ handle , $ value , $ listen ) = split ( "\\|" , $ string ) ;
my $ wait = 1 ;
2017-01-29 17:45:23 +00:00
my $ hash = $ main:: defs { $ name } ;
2016-11-12 10:30:23 +00:00
my $ gatttool = qx( which gatttool ) ;
chomp $ gatttool ;
if ( - x $ gatttool ) {
my $ gtResult ;
2017-11-18 19:48:43 +00:00
my $ cmd ;
my $ sshHost = AttrVal ( $ name , "sshHost" , "none" ) ;
2016-11-12 10:30:23 +00:00
while ( $ wait ) {
2017-01-29 17:45:23 +00:00
my $ grepGatttool = qx( ps ax| grep -E \'gatttool -b $mac\' | grep -v grep ) ;
2016-11-12 10:30:23 +00:00
if ( not $ grepGatttool =~ /^\s*$/ ) {
#another gattool is running
2017-01-29 17:45:23 +00:00
Log3 $ name , 5 , "EQ3BT ($name): another gatttool process is running. waiting..." ;
2016-11-12 10:30:23 +00:00
sleep ( 1 ) ;
} else {
$ wait = 0 ;
}
}
2017-01-29 17:45:23 +00:00
2016-11-12 10:30:23 +00:00
if ( $ value eq "03" ) {
my ( $ sec , $ min , $ hour , $ mday , $ mon , $ year , $ wday , $ yday , $ isdst ) = localtime ( time ) ;
my $ currentDate = sprintf ( "%02X%02X%02X%02X%02X" , $ year + 1900 - 2000 , $ mon + 1 , $ mday , $ hour , $ min ) ;
$ value . = $ currentDate ;
}
2017-01-29 17:45:23 +00:00
my $ hciDevice = "hci" . $ hash - > { helper } { hcidevices } [ $ hash - > { helper } { currenthcidevice } ] ;
2017-11-18 19:48:43 +00:00
#my $cmd = "gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value";
if ( $ sshHost ne 'none' ) {
$ cmd = "ssh $sshHost 'gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value" ;
} else {
$ cmd = "gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value" ;
}
2016-11-12 10:30:23 +00:00
if ( defined ( $ listen ) && $ listen eq "listen" ) {
2017-12-18 21:56:11 +00:00
$ cmd = "timeout " . AttrVal ( $ name , "timeout" , 15 ) . " " . $ cmd . " --listen" ;
2016-11-12 10:30:23 +00:00
}
#redirect stderr to stdout
2017-11-18 19:48:43 +00:00
if ( $ sshHost ne 'none' ) {
$ cmd . = " 2>&1'" ;
} else {
$ cmd . = " 2>&1" ;
}
2016-11-12 10:30:23 +00:00
Log3 $ name , 5 , "EQ3BT ($name): $cmd" ;
$ gtResult = qx( $cmd ) ;
chomp $ gtResult ;
my @ gtResultArr = split ( "\n" , $ gtResult ) ;
Log3 $ name , 4 , "EQ3BT ($name): gatttool result: " . join ( "," , @ gtResultArr ) ;
if ( defined ( $ gtResultArr [ 0 ] ) && $ gtResultArr [ 0 ] eq "Characteristic value was written successfully" ) {
#read notification
if ( defined ( $ gtResultArr [ 1 ] ) && $ gtResultArr [ 1 ] =~ /Notification handle = 0x0421 value: (.*)/ ) {
return "$name|$mac|ok|$workType|$handle|$value|$1" ;
} else {
2016-12-11 14:51:28 +00:00
if ( defined ( $ listen ) && $ listen eq "listen" ) {
return "$name|$mac|error|$workType|$handle|$value|notification missing" ;
} else {
return "$name|$mac|ok|$workType|$handle|$value" ;
}
2016-11-12 10:30:23 +00:00
}
} else {
return "$name|$mac|error|$workType|$handle|$value|$workType failed" ;
}
} else {
return "$name|$mac|error|$workType|$handle|$value|no gatttool binary found. Please check if bluez-package is properly installed" ;
}
}
sub EQ3BT_processGatttoolResult ($) {
my ( $ string ) = @ _ ;
return unless ( defined ( $ string ) ) ;
my @ a = split ( "\\|" , $ string ) ;
my $ name = $ a [ 0 ] ;
my $ hash = $ defs { $ name } ;
my $ mac = $ a [ 1 ] ;
my $ ret = $ a [ 2 ] ;
my $ workType = $ a [ 3 ] ;
my $ handle = $ a [ 4 ] ;
my $ value = $ a [ 5 ] ;
my $ notification = $ a [ 6 ] ;
2017-01-29 17:45:23 +00:00
delete ( $ hash - > { helper } { RUNNING_PID } ) ;
2016-11-12 10:30:23 +00:00
Log3 $ hash , 5 , "EQ3BT ($name): gatttool return string: $string" ;
$ hash - > { helper } { "handle$workType" } = $ handle ;
$ hash - > { helper } { "value$workType" } = $ value ;
2016-12-11 14:51:28 +00:00
$ hash - > { helper } { "listen$workType" } = $ notification ;
2016-11-12 10:30:23 +00:00
if ( $ ret eq "ok" ) {
#process notification
if ( defined ( $ notification ) ) {
EQ3BT_processNotification ( $ hash , $ notification ) ;
}
2017-01-29 17:45:23 +00:00
if ( $ workType =~ /set.*/ ) {
readingsSingleUpdate ( $ hash , "lastChangeBy" , "FHEM" , 1 ) ;
}
2016-11-12 10:30:23 +00:00
#call WorkTypeSuccessful function
my $ call = "EQ3BT_" . $ workType . "Successful" ;
no strict "refs" ;
2017-01-29 17:45:23 +00:00
eval {
& { $ call } ( $ hash , $ handle , $ value ) ;
} ;
2016-11-12 10:30:23 +00:00
use strict "refs" ;
RemoveInternalTimer ( $ hash , "EQ3BT_" . $ workType . "Retry" ) ;
$ hash - > { helper } { "retryCounter$workType" } = 0 ;
} else {
$ hash - > { helper } { "retryCounter$workType" } = 0 if ( ! defined ( $ hash - > { helper } { "retryCounter$workType" } ) ) ;
$ hash - > { helper } { "retryCounter$workType" } + + ;
Log3 $ hash , 4 , "EQ3BT ($name): $workType failed ($handle, $value, $notification)" ;
2017-12-18 21:56:11 +00:00
if ( $ hash - > { helper } { "retryCounter$workType" } > AttrVal ( $ name , "maxRetries" , 20 ) ) {
2016-12-11 14:51:28 +00:00
my $ errorCount = ReadingsVal ( $ hash - > { NAME } , "errorCount-$workType" , 0 ) ;
readingsSingleUpdate ( $ hash , "errorCount-$workType" , $ errorCount + 1 , 1 ) ;
2017-01-29 17:45:23 +00:00
Log3 $ hash , 3 , "EQ3BT ($name): $workType, $handle, $value failed 20 times." ;
2016-11-12 10:30:23 +00:00
$ hash - > { helper } { "retryCounter$workType" } = 0 ;
2017-01-29 17:45:23 +00:00
$ hash - > { helper } { "retryCounterHci" . $ hash - > { helper } { currenthcidevice } } = 0 ;
#call WorkTypeFailed function
my $ call = "EQ3BT_" . $ workType . "Failed" ;
no strict "refs" ;
eval {
& { $ call } ( $ hash , $ handle , $ value ) ;
} ;
use strict "refs" ;
#update hci devicelist
EQ3BT_updateHciDevicelist ( $ hash ) ;
2016-11-12 10:30:23 +00:00
} else {
2017-01-29 17:45:23 +00:00
$ hash - > { helper } { "retryCounterHci" . $ hash - > { helper } { currenthcidevice } } = 0 if ( ! defined ( $ hash - > { helper } { "retryCounterHci" . $ hash - > { helper } { currenthcidevice } } ) ) ;
$ hash - > { helper } { "retryCounterHci" . $ hash - > { helper } { currenthcidevice } } + + ;
if ( $ hash - > { helper } { "retryCounterHci" . $ hash - > { helper } { currenthcidevice } } > 7 ) {
#reset error counter
$ hash - > { helper } { "retryCounterHci" . $ hash - > { helper } { currenthcidevice } } = 0 ;
#use next hci device next time
$ hash - > { helper } { currenthcidevice } += 1 ;
my $ maxHciDevices = @ { $ hash - > { helper } { hcidevices } } - 1 ;
if ( $ hash - > { helper } { currenthcidevice } > $ maxHciDevices ) {
$ hash - > { helper } { currenthcidevice } = 0 ;
}
#update reading
readingsSingleUpdate ( $ hash , "bluetoothDevice" , "hci" . $ hash - > { helper } { hcidevices } [ $ hash - > { helper } { currenthcidevice } ] , 1 ) ;
}
InternalTimer ( gettimeofday ( ) + 3 + int ( rand ( 5 ) ) , "EQ3BT_" . $ workType . "Retry" , $ hash , 0 ) ;
2016-11-12 10:30:23 +00:00
}
}
return undef ;
}
sub EQ3BT_processNotification {
my ( $ hash , $ notification ) = @ _ ;
my @ vals = split ( " " , $ notification ) ;
my $ frameType = $ vals [ 0 ] ;
if ( $ frameType eq "01" ) {
my $ version = hex ( $ vals [ 1 ] ) ;
my $ typeCode = hex ( $ vals [ 2 ] ) ;
readingsSingleUpdate ( $ hash , "firmware" , $ version , 1 ) ;
#readingsSingleUpdate($hash, "typeCode", $typeCode, 1);
} elsif ( $ frameType eq "02" ) {
return undef if ( ! defined ( $ vals [ 2 ] ) ) ;
#vals[2]
my $ mode = hex ( $ vals [ 2 ] ) & 1 ;
my $ modeStr = "Manual" ;
if ( $ mode == 0 ) {
$ modeStr = "Automatic" ;
}
my $ eco = ( hex ( $ vals [ 2 ] ) & 2 ) >> 1 ;
my $ isBoost = ( hex ( $ vals [ 2 ] ) & 4 ) >> 2 ;
my $ dst = ( hex ( $ vals [ 2 ] ) & 8 ) >> 3 ;
my $ wndOpen = ( hex ( $ vals [ 2 ] ) & 16 ) >> 4 ;
my $ unknown = ( hex ( $ vals [ 2 ] ) & 32 ) >> 5 ;
$ unknown = ( hex ( $ vals [ 2 ] ) & 64 ) >> 6 ;
my $ isLowBattery = ( hex ( $ vals [ 2 ] ) & 128 ) >> 7 ;
my $ batteryStr = "ok" ;
if ( $ isLowBattery > 0 ) {
$ batteryStr = "low" ;
}
#vals[3]
my $ pct = hex ( $ vals [ 3 ] ) ;
#vals[5]
my $ temp = hex ( $ vals [ 5 ] ) / 2 ;
my $ timeSinceLastChange = ReadingsAge ( $ hash - > { NAME } , "valvePosition" , 0 ) ;
my $ consumption = ReadingsVal ( $ hash - > { NAME } , "consumption" , 0 ) ;
my $ consumptionToday = ReadingsVal ( $ hash - > { NAME } , "consumptionToday" , 0 ) ;
2016-12-11 14:51:28 +00:00
my $ consumptionTodaySecSinceLastChange = ReadingsAge ( $ hash - > { NAME } , "consumptionToday" , 0 ) ;
2016-11-12 10:30:23 +00:00
my $ oldVal = ReadingsVal ( $ hash - > { NAME } , "valvePosition" , 0 ) ;
my $ consumptionDiff = 0 ;
2017-01-29 17:45:23 +00:00
if ( $ timeSinceLastChange < 600 ) {
2016-11-12 10:30:23 +00:00
$ consumptionDiff += ( $ oldVal + $ pct ) / 2 * $timeSinceLastChange / 3600 ;
}
2017-01-29 17:45:23 +00:00
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "consumption" , sprintf ( "%.3f" , $ consumption + $ consumptionDiff ) ) ;
2016-11-12 10:30:23 +00:00
my ( $ sec , $ min , $ hour , $ mday , $ mon , $ year , $ wday , $ yday , $ isdst ) = localtime ( time ) ;
2016-12-11 14:51:28 +00:00
if ( $ consumptionTodaySecSinceLastChange > ( $ hour * 3600 + $ min * 60 + $ sec ) ) {
readingsSingleUpdate ( $ hash , "consumptionYesterday" , $ consumptionToday + $ consumptionDiff / 2 , 1 ) ;
readingsSingleUpdate ( $ hash , "consumptionToday" , 0 + $ consumptionDiff / 2 , 1 ) ;
2016-11-12 10:30:23 +00:00
} else {
2017-01-29 17:45:23 +00:00
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "consumptionToday" , sprintf ( "%.3f" , $ consumptionToday + $ consumptionDiff ) ) ;
2016-11-12 10:30:23 +00:00
}
2017-01-29 17:45:23 +00:00
readingsSingleUpdate ( $ hash , "valvePosition" , $ pct , 1 ) ;
#changes below this line will set lastchangeby
2017-03-05 10:09:10 +00:00
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "windowOpen" , $ wndOpen , 1 ) ;
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "ecoMode" , $ eco , 1 ) ;
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "battery" , $ batteryStr , 1 ) ;
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "boost" , $ isBoost , 1 ) ;
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "mode" , $ modeStr , 1 ) ;
EQ3BT_readingsSingleUpdateIfChanged ( $ hash , "desiredTemperature" , sprintf ( "%.1f" , $ temp ) , 1 ) ;
2016-11-12 10:30:23 +00:00
}
return undef ;
}
2017-01-29 17:45:23 +00:00
sub EQ3BT_readingsSingleUpdateIfChanged {
my ( $ hash , $ reading , $ value , $ setLastChange ) = @ _ ;
my $ curVal = ReadingsVal ( $ hash - > { NAME } , $ reading , "" ) ;
if ( $ curVal ne $ value ) {
readingsSingleUpdate ( $ hash , $ reading , $ value , 1 ) ;
if ( defined ( $ setLastChange ) ) {
readingsSingleUpdate ( $ hash , "lastChangeBy" , "Thermostat" , 1 ) ;
}
}
}
2016-11-12 10:30:23 +00:00
sub EQ3BT_killGatttool ($) {
}
sub EQ3BT_setDaymode ($) {
my ( $ hash ) = @ _ ;
}
sub EQ3BT_setNightmode ($) {
my ( $ hash ) = @ _ ;
}
sub EQ3BT_setChildlock ($$) {
my ( $ hash , $ desiredState ) = @ _ ;
}
sub EQ3BT_setHolidaymode ($$) {
my ( $ hash , $ holidayEndTime ) = @ _ ;
}
sub EQ3BT_setDatetime ($$) {
my ( $ hash , $ currentDatetime ) = @ _ ;
}
sub EQ3BT_setWindow ($$) {
my ( $ hash , $ desiredState ) = @ _ ;
}
sub EQ3BT_setProgram ($$) {
my ( $ hash , $ program ) = @ _ ;
}
sub EQ3BT_Undef ($) {
my ( $ hash ) = @ _ ;
#remove internal timer
RemoveInternalTimer ( $ hash ) ;
return undef ;
}
sub EQ3BT_Get ($$) {
return undef ;
}
1 ;
= pod
= item device
= item summary Control EQ3 Bluetooth Smart Radiator Thermostat
= item summary_DE Steuerung des EQ3 Bluetooth Thermostats
= begin html
< a name = "EQ3BT" > </a>
<h3> EQ3BT </h3>
<ul>
EQ3BT is used to control a EQ3 Bluetooth Smart Radiator Thermostat <br> <br>
<b> Note: </b> The bluez package is required to run this module . Please check if gatttool executable is available on your system .
<br>
<br>
< a name = "EQ3BTdefine" id = "EQ3BTdefine" > </a>
<b> Define </b>
<ul>
<code> define & lt ; name & gt ; EQ3BT & lt ; mac address & gt ; </code> <br>
<br>
Example:
<ul>
<code> define livingroom . thermostat EQ3BT 00 : 33 : 44 : 33 : 22 : 11 </code> <br>
</ul>
</ul>
<br>
< a name = "EQ3BTset" id = "EQ3BTset" > </a>
<b> Set </b>
<ul>
<code> set & lt ; name & gt ; & lt ; command & gt ; [ & lt ; parameter & gt ; ] </code> <br>
The following commands are defined : <br> <br>
<ul>
<li> <code> <b> desiredTemperature </b> [ 4.5 ... 29.5 ] </code> & nbsp ; & nbsp ; - & nbsp ; & nbsp ; set the temperature </li>
<li> <code> <b> boost </b> on /off</co de > & nbsp ; & nbsp ; - & nbsp ; & nbsp ; activate boost command </li>
<li> <code> <b> mode </b> manual /automatic</co de > & nbsp ; & nbsp ; - & nbsp ; & nbsp ; set manual /automatic mode</ li >
<li> <code> <b> updateStatus </b> </code> & nbsp ; & nbsp ; - & nbsp ; & nbsp ; read current thermostat state and update readings </li>
<li> <code> <b> eco </b> </code> & nbsp ; & nbsp ; - & nbsp ; & nbsp ; set eco temperature </li>
<li> <code> <b> comfort </b> </code> & nbsp ; & nbsp ; - & nbsp ; & nbsp ; set comfort temperature </li>
</ul>
<br>
</ul>
< a name = "EQ3BTget" id = "EQ3BTget" > </a>
2016-12-11 14:51:28 +00:00
<b> Get </b>
<ul>
<code> n /a</co de >
</ul>
<br>
2017-11-18 19:48:43 +00:00
< a name = "EQ3BTattr" id = "EQ3BTattr" > </a>
<b> attr </b>
<ul>
<li> sshHost - FQD - Name or IP of ssh remote system / you must configure your ssh system for certificate authentication. For better handling you can config ssh Client with .ssh/co nfig file </li>
</ul>
<br>
2016-11-12 10:30:23 +00:00
</ul>
= end html
= cut