From 1b3f2d6fc653e2e3b1504f42f5b74b448b0b2eb8 Mon Sep 17 00:00:00 2001 From: thomyd <> Date: Mon, 30 Jun 2014 11:25:12 +0000 Subject: [PATCH] Implemented Somfy RTS support as FHEM module. This module implements the FHEM side of Somfy support, and enables controlling your Somfy blinds from within FHEM. Commands supported are on/off, stop, go-my (goto "my" position"), program (used for pairing) and off-for-timer/on-for-timer to only partially close the blind. Currently only sending of Somfy commands is supported (like Intertechno). git-svn-id: https://svn.fhem.de/fhem/trunk@6183 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/10_SOMFY.pm | 524 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 fhem/FHEM/10_SOMFY.pm diff --git a/fhem/FHEM/10_SOMFY.pm b/fhem/FHEM/10_SOMFY.pm new file mode 100644 index 000000000..4b7159e0f --- /dev/null +++ b/fhem/FHEM/10_SOMFY.pm @@ -0,0 +1,524 @@ +###################################################### +# $Id$ +# +# SOMFY RTS / Simu Hz protocol module for FHEM +# (c) Thomas Dankert +# +# This will only work if you flashed your CUL with +# the newest culfw (support for "Y" command). +# +# Published under GNU GPL License, v2 +###################################################### + +package main; + +use strict; +use warnings; + +my %codes = ( + "10" => "go-my", # goto "my" position + "11" => "stop", # stop the current movement + "20" => "off", # go "up" + "40" => "on", # go "down" + "80" => "prog", # enter programming mode + "100" => "on-for-timer", + "101" => "off-for-timer", +); + +my %somfy_c2b; + +my $somfy_defsymbolwidth = 1240; # Default Somfy frame symbol width +my $somfy_defrepetition = 6; # Default Somfy frame repeat counter + +my %models = ( somfyblinds => 'blinds', ); # supported models (blinds only, as of now) + +############################# +sub SOMFY_Initialize($) { + my ($hash) = @_; + + # map commands from web interface to codes used in Somfy RTS + foreach my $k ( keys %codes ) { + $somfy_c2b{ $codes{$k} } = $k; + } + + # YsKKC0RRRRAAAAAA + # $hash->{Match} = "^YsA..0..........\$"; + $hash->{SetFn} = "SOMFY_Set"; + $hash->{StateFn} = "SOMFY_SetState"; + $hash->{DefFn} = "SOMFY_Define"; + $hash->{UndefFn} = "SOMFY_Undef"; + + # $hash->{ParseFn} = "SOMFY_Parse"; + $hash->{AttrList} = "IODev" + . " symbol-length" + . " enc-key" + . " rolling-code" + . " repetition" + . " switch_rfmode:1,0" + . " do_not_notify:1,0" + . " ignore:0,1" + . " dummy:1,0" + . " model:somfyblinds" + . " loglevel:0,1,2,3,4,5,6"; + +} + +############################# +sub SOMFY_Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + + my $u = "wrong syntax: define SOMFY address " + . "[encryption-key] [rolling-code]"; + + # fail early and display syntax help + if ( int(@a) < 3 ) { + return $u; + } + + # check address format (6 hex digits) + if ( ( $a[2] !~ m/^[a-fA-F0-9]{6}$/i ) ) { + return "Define $a[0]: wrong address format: specify a 6 digit hex value " + } + + my $address = $a[2]; + $hash->{ADDRESS} = uc($address); + + # check optional arguments for device definition + if ( int(@a) > 3 ) { + + # check encryption key (2 hex digits, first must be "A") + if ( ( $a[3] !~ m/^[aA][a-fA-F0-9]{1}$/i ) ) { + return "Define $a[0]: wrong encryption key format:" + . "specify a 2 digits hex value (first nibble = A) " + } + + # store it as attribute, so it is saved in the statefile + $attr{ $a[0] }{"enc-key"} = lc( $a[3] ); + + if ( int(@a) == 5 ) { + # check rolling code (4 hex digits) + if ( ( $a[4] !~ m/^[a-fA-F0-9]{4}$/i ) ) { + return "Define $a[0]: wrong rolling code format:" + . "specify a 4 digits hex value " + } + + # store it + $attr{ $a[0] }{"rolling-code"} = lc( $a[4] ); + } + } + else { + # no values for encryption and rolling code provided, use initial defaults + $attr{ $a[0] }{"enc-key"} = "A0"; + $attr{ $a[0] }{"rolling-code"} = "0000"; + } + + # group devices by their address + my $code = uc($address); + my $ncode = 1; + my $name = $a[0]; + + $hash->{CODE}{ $ncode++ } = $code; + $modules{SOMFY}{defptr}{$code}{$name} = $hash; + + AssignIoPort($hash); +} + +############################# +sub SOMFY_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{SOMFY}{defptr}{$c} } ) { + if ( $modules{SOMFY}{defptr}{$c}{$dname} == $hash ) { + delete( $modules{SOMFY}{defptr}{$c}{$dname} ); + } + } + } + return undef; +} + +##################################### +sub SOMFY_SetState($$$$) { + my ( $hash, $tim, $vt, $val ) = @_; + + if ( $val =~ m/^(.*) \d+$/ ) { + $val = $1; + } + + if ( !defined( $somfy_c2b{$val} ) ) { + return "Undefined value $val"; + } + + return undef; +} + +##################################### +sub SOMFY_Extension_Fn($) +{ + my (undef, $name, $cmd) = split(" ", shift, 3); + return if(!defined($defs{$name})); + + if($cmd eq "on-for-timer") { + DoSet($name, "stop"); # send the stop-command + + } elsif($cmd eq "off-for-timer") { + DoSet($name, "stop"); + + } +} + +############################# +sub SOMFY_Do_For_Timer($@) +{ + my ($hash, $name, $cmd, $param) = @_; + + my $cmd1 = ($cmd =~ m/on.*/ ? "on" : "off"); + + RemoveInternalTimer("SOMFY $name $cmd"); + return "$cmd requires a number as argument" if($param !~ m/^\d*\.?\d*$/); + + if($param) { + # send the on/off command first + DoSet($name, $cmd1); + # schedule the stop command for later + InternalTimer(gettimeofday()+$param,"SOMFY_Extension_Fn","SOMFY $name $cmd",0); + } +} + +################################### +sub SOMFY_Set($@) { + my ( $hash, $name, @args ) = @_; + + my $ret = undef; + my $numberOfArgs = int(@args); + my $message; + + if ( $numberOfArgs < 1 ) { + return "no set value specified" ; + } + + return SOMFY_Do_For_Timer($hash, $name, @args) if($args[0] =~ m/[on|off]-for-timer$/); + return "Bad time spec" if($numberOfArgs == 2 && $args[1] !~ m/^\d*\.?\d+$/); + + my $command = $somfy_c2b{ $args[0] }; + if ( !defined($command) ) { + + return "Unknown argument $args[0], choose one of " + . join( " ", sort keys %somfy_c2b ); + } + + my $io = $hash->{IODev}; + + ## Do we need to change RFMode to SlowRF? + if ( defined( $attr{ $name } ) + && defined( $attr{ $name }{"switch_rfmode"} ) ) + { + if ( $attr{ $name }{"switch_rfmode"} eq "1" ) + { # do we need to change RFMode of IODev + my $ret = + CallFn( $io->{NAME}, "AttrFn", "set", + ( $io->{NAME}, "rfmode", "SlowRF" ) ); + } + } + + ## Do we need to change symbol length? + if ( defined( $attr{ $name } ) + && defined( $attr{ $name }{"symbol-length"} ) ) + { + $message = "Yt" . $attr{ $name }{"symbol-length"}; + CUL_SimpleWrite( $io, $message ); + Log GetLogLevel( $name, 4 ), + "SOMFY set symbol-length: $message for $io->{NAME}"; + } + + + ## Do we need to change frame repetition? + if ( defined( $attr{ $name } ) + && defined( $attr{ $name }{"repetition"} ) ) + { + $message = "Yr" . $attr{ $name }{"repetition"}; + CUL_SimpleWrite( $io, $message ); + Log GetLogLevel( $name, 4 ), + "SOMFY set repetition: $message for $io->{NAME}"; + } + + my $value = $name ." ". join(" ", @args); + + # message looks like this + # Ys_key_ctrl_cks_rollcode_a0_a1_a2 + # Ys ad 20 0ae3 a2 98 42 + + $message = "Ys" + . uc( $attr{ $name }{"enc-key"} ) + . $command + . uc( $attr{ $name }{"rolling-code"} ) + . uc( $hash->{ADDRESS} ); + + ## Log that we are going to switch Somfy + Log GetLogLevel( $name, 2 ), "SOMFY set $value: $message"; + ( undef, $value ) = split( " ", $value, 2 ); # Not interested in the name... + + ## Send Message to IODev and wait for correct answer + my $msg = CallFn( $io->{NAME}, "GetFn", $io, ( " ", "raw", $message ) ); + + my $enckey = uc($attr{$name}{"enc-key"}); + if ( $msg =~ m/raw => Ys$enckey.*/ ) { + Log 4, "Answer from $io->{NAME}: $msg"; + } + else { + Log 2, "SOMFY IODev device didn't answer Ys command correctly: $msg"; + } + + # increment encryption key and rolling code + my $enc_key_increment = hex( $attr{ $name }{"enc-key"} ); + my $rolling_code_increment = hex( $attr{ $name }{"rolling-code"} ); + + $attr{ $name }{"enc-key"} = + sprintf( "%02X", ( ++$enc_key_increment & hex("0xAF") ) ); + $attr{ $name }{"rolling-code"} = + sprintf( "%04X", ( ++$rolling_code_increment ) ); + + ## Do we need to change symbol length back? + if ( defined( $attr{ $name } ) + && defined( $attr{ $name }{"symbol-length"} ) ) + { + $message = "Yt" . $somfy_defsymbolwidth; + CUL_SimpleWrite( $io, $message ); + Log GetLogLevel( $name, 4 ), + "SOMFY set symbol-length back: $message for $io->{NAME}"; + } + + ## Do we need to change repetition back? + if ( defined( $attr{ $name } ) + && defined( $attr{ $name }{"repetition"} ) ) + { + $message = "Yr" . $somfy_defrepetition; + CUL_SimpleWrite( $io, $message ); + Log GetLogLevel( $name, 4 ), + "SOMFY set repetition back: $message for $io->{NAME}"; + } + + ## Do we need to change RFMode back to HomeMatic?? + if ( defined( $attr{ $name } ) + && defined( $attr{ $name }{"switch_rfmode"} ) ) + { + if ( $attr{ $name }{"switch_rfmode"} eq "1" ) + { # do we need to change RFMode of IODev? + my $ret = + CallFn( $io->{NAME}, "AttrFn", "set", + ( $io->{NAME}, "rfmode", "HomeMatic" ) ); + } + } + + ########################## + # Look for all devices with the same address, and set state, timestamp + my $code = "$hash->{ADDRESS}"; + my $tn = TimeNow(); + foreach my $n ( keys %{ $modules{SOMFY}{defptr}{$code} } ) { + + my $lh = $modules{SOMFY}{defptr}{$code}{$n}; + $lh->{CHANGED}[0] = $value; + $lh->{STATE} = $value; + $lh->{READINGS}{state}{TIME} = $tn; + $lh->{READINGS}{state}{VAL} = $value; + } + return $ret; +} + +############################# +sub SOMFY_Parse($$) { + + # not implemented yet, since we only support SENDING of somfy commands +} + +############################# +1; + + +=pod +=begin html + + +

SOMFY - Somfy RTS / Simu Hz protocol

+ + + + +=end html +=cut