| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244 |
- ######################################################
- # $Id: 98_Dooya.pm 15400 2017-11-05 18:11:51Z Sidey $
- #
- # Dooya module for FHEM
- # Thanks for templates/coding from Somfy and SIGNALduino team
- #
- # Needs SIGNALduino.
- # Published under GNU GPL License, v2
- # History:
- # 0.10 2016-02-16 Jarnsen initial template
- # 0.20 2016-03-06 darkmission first functions, renamed from 10_ to 99_
- # 0.21 2016-03-06 darkmission Bug default channel corrected, changed attribute repetition to SignalRepeats
- # 0.22 2016-03-10 darkmission code cleaned, renamed from 99_ to 98_
- # 0.23 2016-03-11 Jarnsen AttrList cleaned and change priority
- # 1.00 2016-03-12 darkmission autocreate, parse communication from SIGNALduino for correct position when using remote from doooya
- # 1.10 2016-03-13 Ralf9 changed SendCommand with sendMsg
- # 1.11 2016-03-17 Ralf9 ID + Channel = DeviceCode
- # 1.12 2016-04-26 Jarnsen im Dooya parse cmd geändert
- # 1.13 2017-08-26 darkmission Update state when called by remote, little code cleaning (setlist and go-my deleted), some more debug messages
- #TODOS:
- # - Groups, diff by channels
- #
- ######################################################
- package main;
- use strict;
- use warnings;
- #use List::Util qw(first max maxstr min minstr reduce shuffle sum);
- my %codes = (
- "01010101" => "stop", # stop the current movement
- "00010001" => "off", # go "up"
- "00110011" => "on", # go "down"
- "11001100" => "prog", # finish pairing
- );
- my %sets = (
- "off" => "noArg",
- "on" => "noArg",
- "down"=> "noArg",
- "stop" => "noArg",
- "prog" => "noArg",
- # "on-for-timer" => "textField",
- # "off-for-timer" => "textField",
- # "pos" => "0,10,20,30,40,50,60,70,80,90,100" # Todo: Warum nicht als Slider?
- "pos" => "slider,0,10,100"
- );
- my %sendCommands = (
- "off" => "off",
- "open" => "off",
- "on" => "on",
- "close" => "on",
- "prog" => "prog",
- "stop" => "stop"
- );
- my %dooya_c2b; # Todo: Als internal speichern
- my $dooya_updateFreq = 3; # Interval for State update # Todo: Als internal speichern
- # supported models (blinds and shutters)
- my %models = (
- dooyablinds => 'blinds',
- dooyashutter => 'shutter'
- );
-
- ##################################################
- # new globals for new set
- #
- my $dooya_posAccuracy = 2; # Todo: Als internal speichern
- my $dooya_maxRuntime = 50; # Todo: Als internal speichern
- my %positions = (
- "moving" => "50",
- "open" => "0",
- "off" => "0",
- "down" => "150",
- "closed" => "200",
- "on" => "200"
- );
- my %translations = (
- "0" => "open",
- "10" => "10",
- "20" => "20",
- "30" => "30",
- "40" => "40",
- "50" => "50",
- "60" => "60",
- "70" => "70",
- "80" => "80",
- "90" => "90",
- "100" => "100",
- "150" => "down",
- "200" => "closed"
- );
- ######################################################
- # Forward declarations
- #
- sub Dooya_CalcCurrentPos($$$$);
- ######################################################
- sub myUtilsDooya_Initialize($) {
- $modules{Dooya}{LOADED} = 1;
- my $hash = $modules{Dooya};
- Dooya_Initialize($hash);
- } # end sub myUtilsDooya_initialize
- ######################################################
- sub Dooya_Initialize($) {
- my ($hash) = @_;
- # map commands from web interface to codes used in Dooya
- foreach my $k ( keys %codes ) {
- $dooya_c2b{ $codes{$k} } = $k;
- }
- $hash->{SetFn} = "Dooya_Set";
- #$hash->{StateFn} = "Dooya_SetState";
- $hash->{DefFn} = "Dooya_Define";
- $hash->{UndefFn} = "Dooya_Undef";
- $hash->{ParseFn} = "Dooya_Parse";
- $hash->{AttrFn} = "Dooya_Attr";
- $hash->{Match} = "^P16#[A-Fa-f0-9]+";
- $hash->{AttrList} = " IODev"
- . " SignalRepeats:5,10,15,20"
- . " channel:0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15"
- . " drive-down-time-to-100"
- . " drive-down-time-to-close"
- . " drive-up-time-to-100"
- . " drive-up-time-to-open"
- . " additionalPosReading"
- . " $readingFnAttributes"
- # . " setList"
- . " ignore:0,1"
- . " dummy:1,0"
- # . " model:dooyablinds,dooyashutter"
- . " loglevel:0,1,2,3,4,5,6";
-
- $hash->{AutoCreate} =
- { "Dooya.*" => { ATTR => "event-min-interval:.*:300 event-on-change-reading:.*",
- FILTER => "%NAME",
- autocreateThreshold => "2:10" } };
- }
- ######################################################
- sub Dooya_StartTime($) {
- my ($d) = @_;
- my ($s, $ms) = gettimeofday();
- my $t = $s + ($ms / 1000000); # 10 msec
- my $t1 = 0;
- $t1 = $d->{'starttime'} if(exists($d->{'starttime'} ));
- $d->{'starttime'} = $t;
- my $dt = sprintf("%.2f", $t - $t1);
- return $dt;
- } # end sub Dooya_StartTime
- ######################################################
- sub Dooya_Define($$) {
- my ( $hash, $def ) = @_;
- my @a = split( "[ \t][ \t]*", $def );
- my $u = "wrong syntax: define <name> Dooya id ";
- # fail early and display syntax help
- if ( int(@a) < 3 ) {
- return $u;
- }
- my ($id, $channel) = split('_', $a[2]);
- Log3 $hash,4 ,"Dooya_Define: id = $id channel = $channel";
-
- # check id format (28 binaer digits)
- if ( ( $id !~ m/^[0-1]{28}$/i ) ) {
- return "Define $a[0]: wrong address format: specify a 28 binaer id value "
- }
- # group devices by their id
- my $name = $a[0];
- $hash->{ID} = uc($id);
- $hash->{CHANNEL} = $channel;
- my $tn = TimeNow();
- my $code = uc($a[2]);
- my $ncode = 1;
- $hash->{CODE}{ $ncode++ } = $code;
- $modules{Dooya}{defptr}{$code}{$name} = $hash;
- $hash->{move} = 'stop';
- AssignIoPort($hash);
- }
- ######################################################
- sub Dooya_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 %{ $modules{Dooya}{defptr}{$c} } ) {
- if ( $modules{Dooya}{defptr}{$c}{$dname} == $hash ) {
- delete( $modules{Dooya}{defptr}{$c}{$dname} );
- }
- }
- }
- return undef;
- }
- ######################################################
- sub Dooya_SendCommand($@){
- my ($hash, @args) = @_;
- my $ret = undef;
- my $cmd = $args[0];
- my $message;
- my $chan;
- my $channel;
- my $SignalRepeats;
- my $name = $hash->{NAME};
- my $bin;
- my $numberOfArgs = int(@args);
- Log3($name,4,"Dooya_sendCommand: $name -> cmd :$cmd: ");
-
- my $command = $dooya_c2b{ $cmd };
-
- # eigentlich ueberfluessig, da oben schon auf Existenz geprueft wird -> %sets
- if ( !defined($command) ) {
- return "Unknown argument $cmd, choose one of "
- . join( " ", sort keys %dooya_c2b );
- }
-
- my $io = $hash->{IODev};
- $SignalRepeats = AttrVal($name,'SignalRepeats', '10');
- Log3 $io,4, "Dooya set SignalRepeats: $SignalRepeats for $io->{NAME}";
-
- $chan = AttrVal($name,'channel', undef);
- if (!defined($chan)) {
- $chan = $hash->{CHANNEL};
- }
- $channel = sprintf("%04b",$chan);
- Log3 $io,4, "Dooya set channel: $chan ($channel) for $io->{NAME}";
-
- my $value = $name ." ". join(" ", @args);
- $bin = uc($hash->{ID}) . $channel . $command;
- #print ("data = $bin \n");
-
- Log3 $io, 4, "Dooya set value = $value";
- ## Send Message to IODev using IOWrite
- $message = 'P16#' . $bin . '#R' . $SignalRepeats;
- Log3 $io, 4, "Dooya_sendCommand: $name -> message :$message: ";
- IOWrite($hash, 'sendMsg', $message);
- return $ret;
- } # end sub Dooya_SendCommand
- ######################################################
- sub Dooya_Runden($) {
- my ($v) = @_;
- if ( ( $v > 105 ) && ( $v < 195 ) ) {
- $v = 150;
- } else {
- $v = int(($v + 5) /10) * 10;
- }
-
- return sprintf("%d", $v );
- } # end sub Dooya_Runden
- ######################################################
- sub Dooya_Translate($) {
- my ($v) = @_;
- if(exists($translations{$v})) {
- $v = $translations{$v}
- }
- return $v
- } # end sub Dooya_Runden
- ######################################################
- sub Dooya_Parse($$) {
- my ($hash, $msg) = @_;
- my (undef ,$rawData) = split("#",$msg);
- my $hlen = length($rawData);
- my $blen = $hlen * 4;
- my $bitData = unpack("B$blen", pack("H$hlen", $rawData));
- Log3 $hash, 4, "Dooya_Parse: rawData = $rawData length: $hlen";
- Log3 $hash, 4, "Dooya_Parse: converted to bits: $bitData";
- # get id, channel, cmd
- my $id = substr($bitData, 0, 28);
- my $BitChannel = substr($bitData, 28, 4); #noch nicht benoetigt
- my $channel = oct("0b" . $BitChannel);
- my $cmd = substr($bitData, 32, 4);
- my $newstate = $codes{ $cmd . $cmd}; # set new state
- my $deviceCode = $id . '_' . $channel;
- Log3 $hash, 4, "Dooya_Parse: device ID: $id";
- Log3 $hash, 4, "Dooya_Parse: Channel: $channel";
- Log3 $hash, 4, "Dooya_Parse: Cmd: $cmd Newstate: $newstate";
- Log3 $hash, 4, "Dooya_Parse: deviceCode: $deviceCode";
- my $def = $modules{Dooya}{defptr}{$deviceCode};
- if($def) {
- my @list;
- foreach my $name (keys %{ $def }) {
- my $lh = $def->{$name};
- $name = $lh->{NAME}; # It may be renamed
- return "" if(IsIgnored($name)); # Little strange.
- # update the state and log it
- ### NEEDS to be deactivated due to the state being maintained by the timer
- # readingsSingleUpdate($lh, "state", $newstate, 1);
- readingsSingleUpdate($lh, "parsestate", $newstate, 1);
-
- Log3 $name, 4, "Dooya_Parse n:$name ns:$newstate lhn:$lh->{NAME} lht:$lh->{TYPE}";
- Dooya_Set( $lh, $name, 'virtual', $newstate );
- push(@list, $name);
- }
- # return list of affected devices
- return @list;
- } else {
- Log3 $hash, 3, "Dooya Unknown device $deviceCode, please define it";
- return "UNDEFINED Dooya_$deviceCode Dooya $deviceCode";
- }
- }
- ######################################################
- sub Dooya_Attr(@) {
- my ($cmd,$name,$aName,$aVal) = @_;
- my $hash = $defs{$name};
- return "\"Dooya Attr: \" $name does not exist" if (!defined($hash));
- # $cmd can be "del" or "set"
- # $name is device name
- # aName and aVal are Attribute name and value
- if ($cmd eq "set") {
- if($aName eq 'drive-up-time-to-100') {
- return "Dooya_attr: value must be >=0 and <= 100" if($aVal < 0 || $aVal > 100);
- } elsif ($aName =~/drive-(down|up)-time-to.*/) {
- # check name and value
- return "Dooya_attr: value must be >0 and <= 100" if($aVal <= 0 || $aVal > 100);
- }
- if ($aName eq 'drive-down-time-to-100') {
- $attr{$name}{'drive-down-time-to-100'} = $aVal;
- $attr{$name}{'drive-down-time-to-close'} = $aVal if(!defined($attr{$name}{'drive-down-time-to-close'}) || ($attr{$name}{'drive-down-time-to-close'} < $aVal));
- } elsif($aName eq 'drive-down-time-to-close') {
- $attr{$name}{'drive-down-time-to-close'} = $aVal;
- $attr{$name}{'drive-down-time-to-100'} = $aVal if(!defined($attr{$name}{'drive-down-time-to-100'}) || ($attr{$name}{'drive-down-time-to-100'} > $aVal));
- } elsif($aName eq 'drive-up-time-to-100') {
- $attr{$name}{'drive-up-time-to-100'} = $aVal;
- } elsif($aName eq 'drive-up-time-to-open') {
- $attr{$name}{'drive-up-time-to-open'} = $aVal;
- $attr{$name}{'drive-up-time-to-100'} = 0 if(!defined($attr{$name}{'drive-up-time-to-100'}) || ($attr{$name}{'drive-up-time-to-100'} > $aVal));
- }
- }
- return undef;
- }
- ##################################################
- ### New set (state) method (using internalset)
- ###
- ### Reimplemented calculations for position readings and state
- ### Allowed sets to be done without sending actually commands to the blinds
- ### syntax set <name> [ <virtual|send> ] <normal set parameter>
- ### position and state are also updated on stop or other commands based on remaining time
- ### position is handled between 0 and 100 blinds down but not completely closed and 200 completely closed
- ### if timings for 100 and close are equal no position above 100 is used (then 100 == closed)
- ### position is rounded to a value of 5 and state is rounded to a value of 10
- #
- ### General assumption times are rather on the upper limit to reach desired state
- # Readings
- ## state contains rounded (to 10) position and/or textField
- ## position contains rounded position (limited detail)
- # STATE
- ## might contain position or textual form of the state (same as STATE reading)
- ######################################################
- # call with hash, name, [virtual/send], set-args (send is default if ommitted)
- sub Dooya_Set($@) {
- my ( $hash, $name, @args ) = @_;
- Log3 $name, 4, "Dooya_Set: Called";
-
- if ( lc($args[0]) =~m/(virtual|send)/ ) {
- Log3 $name, 4, "Dooya_InternalSet call $args[0] ";
- Dooya_InternalSet( $hash, $name, @args );
- } else {
- Log3 $name, 4, "Dooya_InternalSet call send $hash->{NAME} $hash->{TYPE} $name";
- Dooya_InternalSet( $hash, $name, 'send', @args );
- }
- }
- ######################################################
- # call with hash, name, virtual/send, set-args
- sub Dooya_InternalSet($@) {
- my ( $hash, $name, $mode, @args ) = @_;
-
- ### Check Args
- return "Dooya_InternalSet: mode must be virtual or send: $mode " if ( $mode !~m/(virtual|send)/ );
- my $numberOfArgs = int(@args);
- return "Dooya_set: No set value specified" if ( $numberOfArgs < 1 );
- my $cmd = lc($args[0]);
- # just a number provided, assume "pos" command
- if ($cmd =~ m/\d{1,3}/) {
- pop @args;
- push @args, "pos";
- push @args, $cmd;
- $cmd = "pos";
- $numberOfArgs = int(@args);
- }
- if(!exists($sets{$cmd})) {
- my @cList;
- foreach my $k (sort keys %sets) {
- my $opts = undef;
- $opts = $sets{$k};
- if (defined($opts)) {
- push(@cList,$k . ':' . $opts);
- } else {
- push (@cList,$k);
- }
- } # end foreach
- return "Dooya_set: Unknown argument $cmd, choose one of " . join(" ", @cList);
- } # error unknown cmd handling
- my $arg1 = "";
- if ( $numberOfArgs >= 2 ) {
- $arg1 = $args[1];
- }
-
- return "Dooya_set: Bad time spec" if($cmd =~m/(on|off)-for-timer/ && $numberOfArgs == 2 && $arg1 !~ m/^\d*\.?\d+$/);
- # read timing variables
- my ($t1down100, $t1downclose, $t1upopen, $t1up100) = Dooya_getTimingValues($hash);
- Log3($name,5,"Dooya_set: $name -> timings -> td1:$t1down100: tdc :$t1downclose: tuo :$t1upopen: tu1 :$t1up100: ");
- my $model = AttrVal($name,'model',$models{dooyablinds});
-
- if($cmd eq 'pos') {
- return "Dooya_set: No pos specification" if(!defined($arg1));
- return "Dooya_set: $arg1 must be > 0 and < 100 for pos" if($arg1 < 0 || $arg1 > 100);
- return "Dooya_set: Please set attr drive-down-time-to-100, drive-down-time-to-close, etc"
- if(!defined($t1downclose) || !defined($t1down100) || !defined($t1upopen) || !defined($t1up100));
- }
- ### initialize locals
- my $drivetime = 0; # timings until halt command to be sent for on/off-for-timer and pos <value> -> move by time
- my $updatetime = 0; # timing until update of pos to be done for any unlimited move move to endpos stop
- my $move = $cmd;
- my $newState;
- my $updateState;
-
- # get current infos
- my $state = $hash->{STATE};
- my $pos = ReadingsVal($name,'exact',undef);
- if ( !defined($pos) ) {
- $pos = ReadingsVal($name,'position',undef);
- }
- # translate state info to numbers - closed = 200 , open = 0 (correct missing values)
- if ( !defined($pos) ) {
- if(exists($positions{$state})) {
- $pos = $positions{$state};
- } else {
- $pos = $state;
- }
- $pos = sprintf( "%d", $pos );
- }
- Log3($name,4,"Dooya_set: $name -> entering with mode :$mode: cmd :$cmd: arg1 :$arg1: pos :$pos: ");
- # check timer running - stop timer if running and update detail pos
- # recognize timer running if internal updateState is still set
- if ( defined( $hash->{updateState} )) {
- # timer is running so timer needs to be stopped and pos needs update
- RemoveInternalTimer($hash);
-
- $pos = Dooya_CalcCurrentPos( $hash, $hash->{move}, $pos, Dooya_UpdateStartTime($hash) );
- delete $hash->{starttime};
- delete $hash->{updateState};
- delete $hash->{runningtime};
- delete $hash->{runningcmd};
- }
- ################ No error returns after this point to avoid stopped timer causing confusion...
- # calc posRounded
- my $posRounded = Dooya_RoundInternal( $pos );
- ### handle commands
- if(!defined($t1downclose) || !defined($t1down100) || !defined($t1upopen) || !defined($t1up100)) {
- #if timings not set
- if($cmd eq 'on') {
- $newState = 'closed';
- # $newState = 'moving';
- # $updatetime = $dooya_maxRuntime;
- # $updateState = 'closed';
- } elsif($cmd eq 'off') {
- $newState = 'open';
- # $newState = 'moving';
- # $updatetime = $dooya_maxRuntime;
- # $updateState = 'open';
- } elsif($cmd eq 'on-for-timer') {
- # elsif cmd == on-for-timer - time x
- $move = 'on';
- $newState = 'moving';
- $drivetime = $arg1;
- if ( $drivetime == 0 ) {
- $move = 'stop';
- } else {
- $updateState = 'moving';
- }
- } elsif($cmd eq 'off-for-timer') {
- # elsif cmd == off-for-timer - time x
- $move = 'off';
- $newState = 'moving';
- $drivetime = $arg1;
- if ( $drivetime == 0 ) {
- $move = 'stop';
- } else {
- $updateState = 'moving';
- }
- } elsif($cmd =~m/stop/) {
- $move = 'stop';
- $newState = $state
- } else {
- $newState = $state;
- }
- ###else (here timing is set)
- } else {
- # default is roundedPos as new StatePos
- $newState = $posRounded;
- if($cmd eq 'on') {
- if ( $posRounded == 200 ) {
- # if pos == 200 - no state pos change / no timer
- } elsif ( $posRounded >= 100 ) {
- # elsif pos >= 100 - set timer for 100-to-closed --> update timer(newState 200)
- my $remTime = ( $t1downclose - $t1down100 ) * ( (200-$pos) / 100 );
- $updatetime = $remTime;
- $updateState = 200;
- } elsif ( $posRounded < 100 ) {
- # elseif pos < 100 - set timer for remaining time to 100+time-to-close --> update timer( newState 200)
- my $remTime = $t1down100 * ( (100 - $pos) / 100 );
- $updatetime = ( $t1downclose - $t1down100 ) + $remTime;
- $updateState = 200;
- } else {
- # else - unknown pos - assume pos 0 set timer for full time --> update timer( newState 200)
- $newState = 0;
- $updatetime = $t1downclose;
- $updateState = 200;
- }
- } elsif($cmd eq 'off') {
- if ( $posRounded == 0 ) {
- # if pos == 0 - no state pos change / no timer
- } elsif ( $posRounded <= 100 ) {
- # elsif pos <= 100 - set timer for remaining time to 0 --> update timer( newState 0 )
- my $remTime = ( $t1upopen - $t1up100 ) * ( $pos / 100 );
- $updatetime = $remTime;
- $updateState = 0;
- } elsif ( $posRounded > 100 ) {
- # elseif ( pos > 100 ) - set timer for remaining time to 100+time-to-open --> update timer( newState 0 )
- my $remTime = $t1up100 * ( ($pos - 100 ) / 100 );
- $updatetime = ( $t1upopen - $t1up100 ) + $remTime;
- $updateState = 0;
- } else {
- # else - unknown pos assume pos 200 set time for full time --> update timer( newState 0 )
- $newState = 200;
- $updatetime = $t1upopen;
- $updateState = 0;
- }
- } elsif($cmd eq 'pos') {
- if ( $pos < $arg1 ) {
- # if pos < x - set halt timer for remaining time to x / cmd close --> halt timer`( newState x )
- $move = 'on';
- my $remTime = $t1down100 * ( ( $arg1 - $pos ) / 100 );
- $drivetime = $remTime;
- $updateState = $arg1;
- } elsif ( ( $pos >= $arg1 ) && ( $posRounded <= 100 ) ) {
- # elsif pos <= 100 & pos > x - set halt timer for remaining time to x / cmd open --> halt timer ( newState x )
- $move = 'off';
- my $remTime = ( $t1upopen - $t1up100 ) * ( ( $pos - $arg1) / 100 );
- $drivetime = $remTime;
- if ( $drivetime == 0 ) {
- # $move = 'stop'; # avoid sending stop to move to my-pos
- $move = 'none';
- } else {
- $updateState = $arg1;
- }
- } elsif ( $pos > 100 ) {
- # else if pos > 100 - set timer for remaining time to 100+time for 100-x / cmd open --> halt timer ( newState x )
- $move = 'off';
- my $remTime = ( $t1upopen - $t1up100 ) * ( ( 100 - $arg1) / 100 );
- my $posTime = $t1up100 * ( ( $pos - 100) / 100 );
- $drivetime = $remTime + $posTime;
- $updateState = $arg1;
- } else {
- # else - send error (might be changed to first open completely then drive to pos x) / assume open
- $newState = 0;
- $move = 'on';
- my $remTime = $t1down100 * ( ( $arg1 - 0 ) / 100 );
- $drivetime = $remTime;
- $updateState = $arg1;
- ### return "Dooya_set: Pos not currently known please open or close first";
- }
- } elsif($cmd =~m/stop/) {
- # update pos according to current detail pos
- $move = 'stop';
-
- } elsif($cmd eq 'off-for-timer') {
- # calcPos at new time y / cmd close --> halt timer ( newState y )
- $move = 'off';
- $drivetime = $arg1;
- if ( $drivetime == 0 ) {
- $move = 'stop';
- } else {
- $updateState = Dooya_CalcCurrentPos( $hash, $move, $pos, $arg1 );
- }
- } elsif($cmd eq 'on-for-timer') {
- # calcPos at new time y / cmd open --> halt timer ( newState y )
- $move = 'on';
- $drivetime = $arg1;
- if ( $drivetime == 0 ) {
- $move = 'stop';
- } else {
- $updateState = Dooya_CalcCurrentPos( $hash, $move, $pos, $arg1 );
- }
- }
- ## special case close is at 100 ("markisen")
- if( ( $t1downclose == $t1down100) && ( $t1up100 == 0 ) ) {
- if ( defined( $updateState )) {
- $updateState = min( 100, $updateState );
- }
- $newState = min( 100, $posRounded );
- }
- }
- ### update hash / readings
- Log3($name,3,"Dooya_set: handled command $cmd --> move :$move: newState :$newState: ");
- if ( defined($updateState)) {
- Log3($name,5,"Dooya_set: handled for drive/udpate: updateState :$updateState: drivet :$drivetime: updatet :$updatetime: ");
- } else {
- Log3($name,5,"Dooya_set: handled for drive/udpate: updateState :: drivet :$drivetime: updatet :$updatetime: ");
- }
-
- # bulk update should do trigger if virtual mode
- Dooya_UpdateState( $hash, $newState, $move, $updateState, ( $mode eq 'virtual' ) );
-
- ### send command
- if ( $mode ne 'virtual' ) {
- if(exists($sendCommands{$move})) {
- $args[0] = $sendCommands{$move};
- Dooya_SendCommand($hash,@args);
- } elsif ( $move eq 'none' ) {
- # do nothing if commmand / move is set to none
- } else {
- Log3($name,1,"Dooya_set: Error - unknown move for sendCommands: $move");
- }
- }
- ### start timer
- if ( $mode eq 'virtual' ) {
- # in virtual mode define drivetime as updatetime only, so no commands will be send
- if ( $updatetime == 0 ) {
- $updatetime = $drivetime;
- }
- $drivetime = 0;
- }
-
- ### update time stamp
- Dooya_UpdateStartTime($hash);
- $hash->{runningtime} = 0;
- if($drivetime > 0) {
- $hash->{runningcmd} = 'stop';
- $hash->{runningtime} = $drivetime;
- } elsif($updatetime > 0) {
- $hash->{runningtime} = $updatetime;
- }
- if($hash->{runningtime} > 0) {
- # timer fuer stop starten
- if ( defined( $hash->{runningcmd} )) {
- Log3($name,4,"Dooya_set: $name -> stopping in $hash->{runningtime} sec");
- } else {
- Log3($name,4,"Dooya_set: $name -> update state in $hash->{runningtime} sec");
- }
- my $utime = $hash->{runningtime} ;
- if($utime > $dooya_updateFreq) {
- $utime = $dooya_updateFreq;
- }
- InternalTimer(gettimeofday()+$utime,"Dooya_TimedUpdate",$hash,0);
- } else {
- delete $hash->{runningtime};
- delete $hash->{starttime};
- }
- return undef;
- } # end sub Dooya_setFN
- ######################################################
- ######################################################
- ###
- ### Helper for set routine
- ###
- ######################################################
- ######################################################
- sub Dooya_RoundInternal($) { # Todo: kann das nicht die Round Funktion von FHEM?
- my ($v) = @_;
- return sprintf("%d", ($v + ($dooya_posAccuracy/2)) / $dooya_posAccuracy) * $dooya_posAccuracy;
- } # end sub Dooya_RoundInternal
- ######################################################
- sub Dooya_UpdateStartTime($) {
- my ($d) = @_;
- my ($s, $ms) = gettimeofday();
- my $t = $s + ($ms / 1000000); # 10 msec
- my $t1 = 0;
- $t1 = $d->{starttime} if(exists($d->{starttime} ));
- $d->{starttime} = $t;
- my $dt = sprintf("%.2f", $t - $t1);
-
- return $dt;
- } # end sub Dooya_UpdateStartTime
- ######################################################
- sub Dooya_TimedUpdate($) {
- my ($hash) = @_;
- Log3($hash->{NAME},4,"Dooya_TimedUpdate");
-
- # get current infos
- my $pos = ReadingsVal($hash->{NAME},'exact',undef);
- Log3($hash->{NAME},5,"Dooya_TimedUpdate : pos so far : $pos");
-
- my $dt = Dooya_UpdateStartTime($hash);
- my $nowt = gettimeofday();
-
- $pos = Dooya_CalcCurrentPos( $hash, $hash->{move}, $pos, $dt );
- # my $posRounded = Dooya_RoundInternal( $pos );
-
- Log3($hash->{NAME},5,"Dooya_TimedUpdate : delta time : $dt new rounde pos (rounded): $pos ");
-
- $hash->{runningtime} = $hash->{runningtime} - $dt;
- if ( $hash->{runningtime} <= 0.1) {
- if ( defined( $hash->{runningcmd} ) ) {
- Dooya_SendCommand($hash, $hash->{runningcmd});
- }
- # trigger update from timer
- Dooya_UpdateState( $hash, $hash->{updateState}, 'stop', undef, 1 );
- delete $hash->{starttime};
- delete $hash->{runningtime};
- delete $hash->{runningcmd};
- } else {
- my $utime = $hash->{runningtime} ;
- if($utime > $dooya_updateFreq) {
- $utime = $dooya_updateFreq;
- }
- Dooya_UpdateState( $hash, $pos, $hash->{move}, $hash->{updateState}, 1 );
- if ( defined( $hash->{runningcmd} )) {
- Log3($hash->{NAME},4,"Dooya_TimedUpdate: $hash->{NAME} -> stopping in $hash->{runningtime} sec");
- } else {
- Log3($hash->{NAME},4,"Dooya_TimedUpdate: $hash->{NAME} -> update state in $hash->{runningtime} sec");
- }
- my $nstt = max($nowt+$utime-0.01, gettimeofday()+.1 );
- Log3($hash->{NAME},5,"Dooya_TimedUpdate: $hash->{NAME} -> next time to stop: $nstt");
- InternalTimer($nstt,"Dooya_TimedUpdate",$hash,0);
- }
-
- Log3($hash->{NAME},5,"Dooya_TimedUpdate DONE");
- } # end sub Dooya_TimedUpdate
- ######################################################
- # Dooya_UpdateState( $hash, $newState, $move, $updateState );
- sub Dooya_UpdateState($$$$$) {
- my ($hash, $newState, $move, $updateState, $doTrigger) = @_;
- my $addtlPosReading = AttrVal($hash->{NAME},'additionalPosReading',undef);
- if ( defined($addtlPosReading )) {
- $addtlPosReading = undef if ( ( $addtlPosReading eq "" ) or ( $addtlPosReading eq "state" ) or ( $addtlPosReading eq "position" ) or ( $addtlPosReading eq "exact" ) );
- }
- readingsBeginUpdate($hash);
- if(exists($positions{$newState})) {
- readingsBulkUpdate($hash,"state",$newState);
- $hash->{STATE} = $newState;
- readingsBulkUpdate($hash,"position",$positions{$newState});
- $hash->{position} = $positions{$newState};
-
- readingsBulkUpdate($hash,$addtlPosReading,$positions{$newState}) if ( defined($addtlPosReading) );
- } else {
- my $rounded = Dooya_Runden( $newState );
- my $stateTrans = Dooya_Translate( $rounded );
- readingsBulkUpdate($hash,"state",$stateTrans);
- $hash->{STATE} = $stateTrans;
- readingsBulkUpdate($hash,"position",$rounded);
- $hash->{position} = $rounded;
- readingsBulkUpdate($hash,$addtlPosReading,$rounded) if ( defined($addtlPosReading) );
-
- }
- readingsBulkUpdate($hash,"exact",$newState);
- $hash->{exact} = $newState;
- if ( defined( $updateState ) ) {
- $hash->{updateState} = $updateState;
- } else {
- delete $hash->{updateState};
- }
- $hash->{move} = $move;
-
- readingsEndUpdate($hash,$doTrigger);
- } # end sub Dooya_UpdateState
- ######################################################
- # Return timingvalues from attr and after correction
- sub Dooya_getTimingValues($) {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $t1down100 = AttrVal($name,'drive-down-time-to-100',undef);
- my $t1downclose = AttrVal($name,'drive-down-time-to-close',undef);
- my $t1upopen = AttrVal($name,'drive-up-time-to-open',undef);
- my $t1up100 = AttrVal($name,'drive-up-time-to-100',undef);
- return (undef, undef, undef, undef) if(!defined($t1downclose) || !defined($t1down100) || !defined($t1upopen) || !defined($t1up100));
- if ( ( $t1downclose < 0 ) || ( $t1down100 < 0 ) || ( $t1upopen < 0 ) || ( $t1up100 < 0 ) ) {
- Log3($name,1,"Dooya_getTimingValues: $name time values need to be positive values");
- return (undef, undef, undef, undef);
- }
- if ( $t1downclose < $t1down100 ) {
- Log3($name,1,"Dooya_getTimingValues: $name close time needs to be higher or equal than time to pos100");
- return (undef, undef, undef, undef);
- } elsif ( $t1downclose == $t1down100 ) {
- $t1up100 = 0;
- }
- if ( $t1upopen <= $t1up100 ) {
- Log3($name,1,"Dooya_getTimingValues: $name open time needs to be higher or equal than time to pos100");
- return (undef, undef, undef, undef);
- }
- if ( $t1upopen < 1 ) {
- Log3($name,1,"Dooya_getTimingValues: $name time to open needs to be at least 1 second");
- return (undef, undef, undef, undef);
- }
- if ( $t1downclose < 1 ) {
- Log3($name,1,"Dooya_getTimingValues: $name time to close needs to be at least 1 second");
- return (undef, undef, undef, undef);
- }
- return ($t1down100, $t1downclose, $t1upopen, $t1up100);
- }
- ######################################################
- # call with hash, translated state
- sub Dooya_CalcCurrentPos($$$$) {
- my ($hash, $move, $pos, $dt) = @_;
- my $name = $hash->{NAME};
- my $newPos = $pos;
-
- # Attributes for calculation
- my ($t1down100, $t1downclose, $t1upopen, $t1up100) = Dooya_getTimingValues($hash);
- if(defined($t1down100) && defined($t1downclose) && defined($t1up100) && defined($t1upopen)) {
- if( ( $t1downclose == $t1down100) && ( $t1up100 == 0 ) ) {
- $pos = min( 100, $pos );
- if($move eq 'on') {
- $newPos = min( 100, $pos );
- if ( $pos < 100 ) {
- # calc remaining time to 100%
- my $remTime = ( 100 - $pos ) * $t1down100 / 100;
- if ( $remTime > $dt ) {
- $newPos = $pos + ( $dt * 100 / $t1down100 );
- }
- }
- } elsif($move eq 'off') {
- $newPos = max( 0, $pos );
- if ( $pos > 0 ) {
- $newPos = $dt * 100 / ( $t1upopen );
- $newPos = max( 0, ($pos - $newPos) );
- }
- } else {
- Log3($name,1,"Dooya_CalcCurrentPos: $name move wrong $move");
- }
- } else {
- if($move eq 'on') {
- if ( $pos >= 100 ) {
- $newPos = $dt * 100 / ( $t1downclose - $t1down100 );
- $newPos = min( 200, $pos + $newPos );
- } else {
- # calc remaining time to 100%
- my $remTime = ( 100 - $pos ) * $t1down100 / 100;
- if ( $remTime > $dt ) {
- $newPos = $pos + ( $dt * 100 / $t1down100 );
- } else {
- $dt = $dt - $remTime;
- $newPos = 100 + ( $dt * 100 / ( $t1downclose - $t1down100 ) );
- }
- }
- } elsif($move eq 'off') {
- if ( $pos <= 100 ) {
- $newPos = $dt * 100 / ( $t1upopen - $t1up100 );
- $newPos = max( 0, $pos - $newPos );
- } else {
- # calc remaining time to 100%
- my $remTime = ( $pos - 100 ) * $t1up100 / 100;
- if ( $remTime > $dt ) {
- $newPos = $pos - ( $dt * 100 / $t1up100 );
- } else {
- $dt = $dt - $remTime;
- $newPos = 100 - ( $dt * 100 / ( $t1upopen - $t1up100 ) );
- }
- }
- } else {
- Log3($name,1,"Dooya_CalcCurrentPos: $name move wrong $move");
- }
- }
- } else {
- ### no timings set so just assume it is always moving
- $newPos = $positions{'moving'};
- }
-
- return $newPos;
- }
- ######################################################
- ######################################################
- ######################################################
- 1;
- =pod
- =item summary Supports rf shutters from dooya
- =item summary_DE Unterstützt dooya Funkrolladen
- =begin html
- <a name="Dooya"></a>
- <h3>Dooya protocol</h3>
- <ul>
- The Dooya protocol is used by a wide range of devices,
- which are either senders or receivers/actuators.
- The RECIVING and SENDING of Dooya commands is implemented in the SIGNALduino, so this module currently supports
- devices like blinds and shutters. The Dooya protocol is used from a lot of different shutter companies in Germanyr. Examples are Rohrmotor24 or Nobily.
- <br><br>
- <pre>
- <code>4: sduino/msg READ: MU;P0=4717;P1=-1577;P2=284;P3=-786;P4=649;P5=-423;D=01232345[......]445232;CP=2;</code>
- <code>4: sduino: Fingerprint for MU Protocol id 16 -> Dooya shutter matches, trying to demodulate</code>
- <code>4: sduino: decoded matched MU Protocol id 16 dmsg u16#370658E133 length 40</code>
- <code>4: SIGNALduino_unknown Protocol: 16</code>
- <code>4: SIGNALduino_unknown converted to bits: 0011011100000110010110001110000100110011</code>
- <code>4: SIGNALduino_unknown / shutter Dooya 0011011100000110010110001110000100110011 received</code>
- <code>4: 00110111000001100101100 1110 0001 0011 0011</code>
- <code>4: SIGNALduino_unknown found shutter from Dooya. id=3606104, remotetype=14, channel=1, direction=down, all_shutters=false</code>
- </pre>
-
- <br> a <a href="#SIGNALduino">SIGNALduino</a> device (must be defined first) <br>
- <br>
- <br>
-
- <a name="Dooyadefine"></a>
- <br>
- <b>Define</b>
- <br>
- <ul>
- <code>define <name> Dooya <id>_<channel> </code>
- <br>
- <br>
- The id is a 28-digit binar code, that uniquely identifies a single remote control.
- <br>
- Pairing is done by setting the shutter in programming mode, either by disconnecting/reconnecting the power,
- and by pressing the program button on an already associated remote.
- <br>
- Once the shutter is in programming mode, send the "prog" command from within FHEM to complete the pairing.
- The shutter will peep shortly to indicate completion.
- <br>
- You are now able to control this blind from FHEM, the receiver thinks it is just another remote control or the real exist remote.
- For the shutter it´s the same.
- <ul>
- <li><code><id></code> is a 28 digit binar number that uniquely identifies FHEM as a new remote control.
- <br>You can use a different one for each device definition, and group them using a structure. You can use the same ID for a couple of shutters
- and you can give every one an other channel. (0 to 15, 0 ist the MASTER and conrols all other channels.)
- If you set one of them, you need to pick the same address as an existing remote. You can create the Device with autocreate with a real remote or manuel without remote control.</li>
- </ul>
- <br>
- Examples:
- <ul>
- <code>define Rollo_Master Dooya 0011011100000110010110001110_0</code><br> Rollo_Master channel 0 controls all shutters (channel 1 - 15) with the same ID, in this case Rollo_1 and Rollo_2 <br>
- <br>
- <code>define Rollo_1 Dooya 0011011100000110010110001110_1</code><br> Rollo_1 channel 1<br>
- <code>define Rollo_2 Dooya 0011011100000110010110101110_2</code><br> Rollo_2 channel 2<br>
- </ul>
- </ul>
- <br>
- <a name="Dooyaset"></a>
- <b>Set </b>
- <ul>
- <code>set <name> <value> [<time>]</code>
- <br><br>
- where <code>value</code> is one of:<br>
- <pre>
- on
- off
- stop
- pos value (0..100) # see note
- prog # Special, see note
- </pre>
- Examples:
- <ul>
- <code>set rollo_1 on</code><br>
- <code>set rollo_1 on,sleep 1,rollo_2 on,sleep 1,rollo_3 on</code><br>
- <code>set rollo_1 off</code><br>
- <code>set rollo_1 pos 50</code><br>
- </ul>
- <br>
- Notes:
- <ul>
- <li>prog is a special command used to pair the receiver to FHEM:
- Set the receiver in programming mode and send the "prog" command from FHEM to finish pairing.<br>
- The shutter will peep shortly to indicate success.
- </li>
- <li>pos value<br>
-
- The position is variying between 0 completely open and 100 for covering the full window.
- The position must be between 0 and 100 and the appropriate
- attributes drive-down-time-to-100, drive-down-time-to-close,
- drive-up-time-to-100 and drive-up-time-to-open must be set.<br>
- </li>
- </ul>
- The position reading distinuishes between multiple cases
- <ul>
- <li>Without timing values set only generic values are used for status and position: <pre>open, closed, moving</pre> are used
- </li>
- <li>With timing values set but drive-down-time-to-close equal to drive-down-time-to-100 and drive-up-time-to-100 equal 0
- the device is considered to only vary between 0 and 100 (100 being completely closed)
- </li>
- <li>With full timing values set the device is considerd a window shutter (Rolladen) with a difference between
- covering the full window (position 100) and being completely closed (position 200)
- </li>
- </ul>
- </ul>
- <br>
- <b>Get</b>
- <ul>N/A</ul><br>
- <a name="Dooyaattr"></a>
- <b>Attributes</b>
- <ul>
- <a name="IODev"></a>
- <li>IODev<br>
- Set the IO or physical device which should be used for sending signals
- for this "logical" device. It must be the SIGNALduino.<br>
- Note: The IODev has to be set, otherwise no commands will be sent!<br>
- </li><br>
- <a name="channel"></a>
- <li>channel<br>
- Set the channel of the remote. You can use 0 (MASTER) to 15.<br>
- Note: The MASTER conrols all remotes with the same ID!!!<br>
- </li><br>
-
- <a name="SignalRepeats"></a>
- <li>SignalRepeats<br>
- Set the repeats for sending signal. You can use 5, 10, 15 and 20.
- </li><br>
-
- <a name="additionalPosReading"></a>
- <li>additionalPosReading<br>
- Position of the shutter will be stored in the reading <code>pos</code> as numeric value.
- Additionally this attribute might specify a name for an additional reading to be updated with the same value than the pos.
- </li><br>
- <a name="eventMap"></a>
- <li>eventMap<br>
- Replace event names and set arguments. The value of this attribute
- consists of a list of space separated values, each value is a colon
- separated pair. The first part specifies the "old" value, the second
- the new/desired value. If the first character is slash(/) or comma(,)
- then split not by space but by this character, enabling to embed spaces.
- Examples:
- <ul><code>
- attr store eventMap on:open off:closed<br>
- attr store eventMap /on-for-timer 10:open/off:closed/<br>
- set store open
- </code>
- </ul>
- </li>
- <br>
- <li><a href="#do_not_notify">do_not_notify</a></li><br>
- <a name="attrdummy"></a>
- <li>dummy<br>
- Set the device attribute dummy to define devices which should not
- output any radio signals. Associated notifys will be executed if
- the signal is received. Used e.g. to react to a code from a sender, but
- it will not emit radio signal if triggered in the web frontend.
- </li><br>
- <li><a href="#loglevel">loglevel</a></li><br>
- <li><a href="#showtime">showtime</a></li><br>
- <a name="model"></a>
-
- <a name="ignore"></a>
- <li>ignore<br>
- Ignore this device, e.g. if it belongs to your neighbour. The device
- won't trigger any FileLogs/notifys, issued commands will silently
- ignored (no RF signal will be sent out, just like for the <a
- href="#attrdummy">dummy</a> attribute). The device won't appear in the
- list command (only if it is explicitely asked for it), nor will it
- appear in commands which use some wildcard/attribute as name specifiers
- (see <a href="#devspec">devspec</a>). You still get them with the
- "ignored=1" special devspec.
- </li><br>
- <a name="drive-down-time-to-100"></a>
- <li>drive-down-time-to-100<br>
- The time the blind needs to drive down from "open" (pos 0) to pos 100.<br>
- In this position, the lower edge touches the window frame, but it is not completely shut.<br>
- For a mid-size window this time is about 12 to 15 seconds.
- </li><br>
- <a name="drive-down-time-to-close"></a>
- <li>drive-down-time-to-close<br>
- The time the blind needs to drive down from "open" (pos 0) to "close", the end position of the blind.<br>
- This is about 3 to 5 seonds more than the "drive-down-time-to-100" value.
- </li><br>
- <a name="drive-up-time-to-100"></a>
- <li>drive-up-time-to-100<br>
- The time the blind needs to drive up from "close" (endposition) to "pos 100".<br>
- This usually takes about 3 to 5 seconds.
- </li><br>
- <a name="drive-up-time-to-open"></a>
- <li>drive-up-time-to-open<br>
- The time the blind needs drive up from "close" (endposition) to "open" (upper endposition).<br>
- This value is usually a bit higher than "drive-down-time-to-close", due to the blind's weight.
- </li><br>
- </ul>
- <br>
- <a name="Dooyaevents"></a>
- <b>Generated events:</b>
- <ul>
- From a Dooya device you can receive one of the following events.
- <li>on</li>
- <li>off</li>
- <li>stop</li>
- Which event is sent is device dependent and can sometimes be configured on
- the device.
- </ul>
- </ul>
- =end html
- =cut
|