mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-04-15 22:26:04 +00:00
99_Venetian: added new module
git-svn-id: https://svn.fhem.de/fhem/trunk@12226 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
parent
b65f56aaf7
commit
620fc65f3d
251
fhem/FHEM/99_Venetian.pm
Normal file
251
fhem/FHEM/99_Venetian.pm
Normal file
@ -0,0 +1,251 @@
|
||||
##############################################
|
||||
#
|
||||
# This is open source software licensed unter the Apache License 2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
##############################################
|
||||
|
||||
# $Id$
|
||||
|
||||
use v5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
use POSIX;
|
||||
use experimental "smartmatch";
|
||||
|
||||
|
||||
#add FHEM/lib to @INC if it's not allready included. Should rather be in fhem.pl than here though...
|
||||
BEGIN {
|
||||
if (!grep(/FHEM\/lib$/,@INC)) {
|
||||
foreach my $inc (grep(/FHEM$/,@INC)) {
|
||||
push @INC,$inc."/lib";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
use VenetianBlinds::VenetianMasterController;
|
||||
use VenetianBlinds::VenetianRoomController;
|
||||
use VenetianBlinds::VenetianBlindController;
|
||||
|
||||
my %valid_types = (
|
||||
"master" =>"VenetianMasterController",
|
||||
"room" => "VenetianRoomController",
|
||||
"blind" => "VenetianBlindController",
|
||||
);
|
||||
|
||||
sub Venetian_Initialize {
|
||||
my ($hash) = @_;
|
||||
$hash->{DefFn} = 'Venetian_Define';
|
||||
#$hash->{UndefFn} = 'Venetian_Undef';
|
||||
$hash->{SetFn} = 'Venetian_Set';
|
||||
#$hash->{GetFn} = 'Venetian_Get';
|
||||
#$hash->{AttrFn} = 'Venetian_Attr';
|
||||
#$hash->{ReadFn} = 'Venetian_Read';
|
||||
$hash->{NotifyFn} = 'Venetian_Notify';
|
||||
$hash->{parseParams} = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
sub Venetian_Define {
|
||||
my ($hash, $a, $h) = @_;
|
||||
$hash->{type} = $valid_types{$h->{type}};
|
||||
if (!defined $hash->{type}) {
|
||||
return "Type $h->{type} is not supported!";
|
||||
}
|
||||
return vbc_call("Define",$hash, $a, $h);
|
||||
}
|
||||
|
||||
sub Venetian_Set {
|
||||
my ( $hash, $a,$h ) = @_;
|
||||
my $result = undef;
|
||||
return vbc_call("Set",$hash, $a, $h);
|
||||
}
|
||||
|
||||
|
||||
sub Venetian_Notify {
|
||||
my ($own_hash, $dev_hash) = @_;
|
||||
my $ownName = $own_hash->{NAME}; # own name / hash
|
||||
return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled
|
||||
|
||||
my $devName = $dev_hash->{NAME}; # Device that created the events
|
||||
my $events = main::deviceEvents($dev_hash,1);
|
||||
return if( !$events );
|
||||
|
||||
return vbc_call("Notify",$own_hash, $devName, $events);
|
||||
}
|
||||
|
||||
sub vbc_call{
|
||||
my ($func,$hash,$a,$h) = @_;
|
||||
$func = "VenetianBlinds::$hash->{type}::$func";
|
||||
my $result;
|
||||
{
|
||||
## no critic (ProhibitNoStrict)
|
||||
no strict 'refs';
|
||||
$result = &$func($hash, $a, $h);
|
||||
## use critic
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
1;
|
||||
|
||||
=pod
|
||||
=item summary automates venetian blinds depending on sun and weather
|
||||
=begin html
|
||||
|
||||
<a name="Venetian"></a>
|
||||
<h3>Venetian</h3>
|
||||
<ul>
|
||||
<i>Venetian</i> implements a fully automated solution to control venetian blinds.
|
||||
It will consinder lots of input data to set the blinds as a human being would do.
|
||||
It use the sun position (from Twilight) the weather (from Weather) and orientation of the blinds.
|
||||
The blinds are opened automatically if the wind speed exceeds a user defined threshold.
|
||||
<br/><br/>
|
||||
<b>Note:</b> Venetian only works with Fibaro FGM-222 controllers!
|
||||
<br/><br/>
|
||||
A minimal set up consists of two devices: one <i>Master</i> and one <i>Blind</i>.
|
||||
The <i>Master</i> will collect global data from your installation while the <i>Blind</i> will control the actual device.
|
||||
For each physical device a <i>Blind</i> device must be defined.
|
||||
<br/><br/>
|
||||
You can add an optional device <i>Room</i> to control all <i>Blinds</i> in one room with one click.
|
||||
For this to work your <i>Blinds</i> must be mapped to rooms with the standard attribute "room".
|
||||
<br/><br/>
|
||||
<a name="Venetian_Define"></a>
|
||||
<b>Define</b>
|
||||
<ul>
|
||||
Master:<br/>
|
||||
<code>define <name> Venetian type=master twilight=<name> weather=<name> wind_speed_threshold=<value></code>
|
||||
<br/><br/>
|
||||
|
||||
Room: <br/>
|
||||
<code>define <name> Venetian type=room [rooms=<name>,<name>,... ]</code>
|
||||
<br/><br/>
|
||||
|
||||
Blind: <br/>
|
||||
<code>define <name> Venetian type=blind master=<name> device=<name> could_index_threshold=<number> azimuth=<start>-<end> elevation=<start>-<end> months=<start>-<end></code>
|
||||
<br/><br/>
|
||||
|
||||
<b>Parameters (common)</b>
|
||||
<ul>
|
||||
<li/>type<br/>
|
||||
is one of the values "master", "room" or "blind".
|
||||
</ul>
|
||||
<b>Parameters (Master)</b>
|
||||
<ul>
|
||||
<li/>twilight<br/>
|
||||
Name of the <a href="#Twilight">Twilight</a> device.
|
||||
This is used to get the current position of the sun.
|
||||
<li/>weather<br/>
|
||||
Name of the <a href="#Weather">Weather</a> device.
|
||||
This is used to get the weather reports
|
||||
<li/>wind_speed_threshold<br/>
|
||||
Maximum wind speed in km/h the venetian blinds are designed for.
|
||||
If the measured wind speed exceed this threshold, the blinds are opened automatically to prevent damages.
|
||||
</ul>
|
||||
<b>Parameters (Blind)</b>
|
||||
<ul>
|
||||
<li/>master</br>
|
||||
name of the Venetian "Master" device.
|
||||
<li/>device</br>
|
||||
name of the blind controller (Fibaro FGM-222) to be controlled.
|
||||
<li/>could_index_threshold</br>
|
||||
Threshold for the cloudiness, as defined in <a href="https://de.wikipedia.org/wiki/Bew%C3%B6lkung">Wikipedia</a>.
|
||||
The scale is 0 (clear sky) to 8 (overcast), where 9 is unknown.
|
||||
The blinds are lowered only the the current cloud index is <i>below</i> this number.
|
||||
<br/>
|
||||
Example: <code>could_index_threshold=5</code> means broken
|
||||
<li/>azimuth</br>
|
||||
The range of the azimuth of the sun, in which the blinds shall be closed.
|
||||
This is measured in degrees.
|
||||
This is defined by the pyhiscal orientation of your window.
|
||||
You can use a compass to measure this for every window.
|
||||
<br/>
|
||||
Example: <code>azimuth=90-120</code> means from 90° to 120°
|
||||
<li/>elevation</br>
|
||||
The range of the elevation of the sun, in which the blinds shall be closed.
|
||||
This is measured in degrees.
|
||||
You can guess this from the pyhsical localtion of your window and obstacles around your building.
|
||||
<br/>
|
||||
Example: <code>azimuth=10-90</code> means from 10° to 90°
|
||||
<li/>months</br>
|
||||
The range of months in which the blinds shall be closed, e.g. summer.
|
||||
<br/>
|
||||
Example: <code>azimuth=5-10</code> means from May to October
|
||||
</ul>
|
||||
<b>Parameters (Room)</b>
|
||||
<ul>
|
||||
<li/>rooms (optional)</br>
|
||||
Comma separated list of rooms in which this device shall control the blinds.
|
||||
If this is not defined, the rooms from the Attribute of this device are used.
|
||||
<br/>
|
||||
Example: <code>rooms=Kitchen,Living</code> means that all blinds in the rooms named "Kitchen" and "Living" are controlled.
|
||||
</ul>
|
||||
</ul>
|
||||
<a name="Venetian_Set"></a>
|
||||
<b>Set</b>
|
||||
<ul>
|
||||
<b>Blind:</b>
|
||||
<ul>
|
||||
<li/><code>set automatic</code></br>
|
||||
Set blinds to automatic mode. This is the normal mode, if blinds should be set accoring to the sun position.
|
||||
<li/><code>set stop</code></br>
|
||||
Stop the blind movement
|
||||
<li/><code>set windalarm</code></br>
|
||||
Trigger the wind alarm. This will cause the blinds to be opened, regardless of the current state.
|
||||
<li/><code>set <scene></code></br>
|
||||
Disable the automatic and set a scene manually. The currently available scenes are:
|
||||
<ul>
|
||||
<li/><code>open</code><br/>
|
||||
Fully open the binds.
|
||||
<li/><code>closed</code> <br/>
|
||||
Fully close the blinds and slats.
|
||||
<li/><code>see_through</code> <br/>
|
||||
Fully close the blinds and set the slats horizontally, so that you can still <i>see through</i>.
|
||||
<li/><code>shaded</code><br/>
|
||||
Close the blinds and slightly close the slats. This is the scene when the automatic closes the blinds.
|
||||
<li/><code>adaptive</code><br/>
|
||||
<b>Experimental!</b> Tries to set the slats so that they are just closed enough so that the sun does not get in.
|
||||
This takes the elevation of the sun and geometry of the slats into consideration.
|
||||
</ul>
|
||||
</ul>
|
||||
<b>Room:</b>
|
||||
<ul>
|
||||
<li/><code>set automatic</code></br>
|
||||
Set all blinds in this room to automatic mode.
|
||||
<li/><code>set stop</code></br>
|
||||
Stop all blinds in this room.
|
||||
<li/><code>set <scene></code></br>
|
||||
Set all blinds in this room to a certain scene.
|
||||
</ul>
|
||||
<b>Master:</b>
|
||||
<ul>
|
||||
<li/><code>set automatic</code></br>
|
||||
Set all blinds to automatic mode.
|
||||
<li/><code>set stop</code></br>
|
||||
Stop all blinds.
|
||||
<li/><code>set trigger_update</code></br>
|
||||
Trigger all blinds in automatic mode to update their scenes to the current sun position.
|
||||
This should not be neccesary in normal operation but is useful when trying our different parameters.
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<!-- there are get operations at the moment
|
||||
<a name="Venetian_Get"></a>
|
||||
<b>Get</b>
|
||||
<ul>
|
||||
TODO
|
||||
</ul>
|
||||
-->
|
||||
|
||||
<!-- there are not attributes at the moment
|
||||
<a name="Venetian_Attributes"></a>
|
||||
<b>Set</b>
|
||||
<ul>
|
||||
TODO
|
||||
</ul>
|
||||
-->
|
||||
|
||||
</ul>
|
||||
|
||||
=end html
|
||||
=cut
|
||||
|
89
fhem/FHEM/lib/VenetianBlinds/Shared.pm
Normal file
89
fhem/FHEM/lib/VenetianBlinds/Shared.pm
Normal file
@ -0,0 +1,89 @@
|
||||
##############################################
|
||||
#
|
||||
# This is open source software licensed unter the Apache License 2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
##############################################
|
||||
|
||||
package VenetianBlinds::Shared;
|
||||
use v5.14;
|
||||
use strict;
|
||||
use warnings;
|
||||
use experimental "smartmatch";
|
||||
use base 'Exporter';
|
||||
|
||||
|
||||
# constants #############################################
|
||||
use constant scenes => {
|
||||
"open" => {
|
||||
"blind" => 99,
|
||||
"slat" => 99,
|
||||
},
|
||||
"closed" => {
|
||||
"blind" => 0,
|
||||
"slat" => 0,
|
||||
},
|
||||
"see_through" => {
|
||||
"blind" => 0,
|
||||
"slat" => 50,
|
||||
},
|
||||
"shaded" => {
|
||||
"blind" => 0,
|
||||
"slat" => 30,
|
||||
},
|
||||
"adaptive" => {
|
||||
"blind" => 0,
|
||||
"slat" => "adaptive",
|
||||
},
|
||||
};
|
||||
|
||||
our @EXPORT_OK = ('scenes');
|
||||
|
||||
# functions #############################################
|
||||
|
||||
sub send_to_all{
|
||||
my ($cmd) = @_;
|
||||
foreach my $device (find_devices()) {
|
||||
main::fhem("set $device $cmd");
|
||||
}
|
||||
}
|
||||
|
||||
sub find_devices_in_room {
|
||||
my ($my_room) = @_;
|
||||
my @result = ();
|
||||
my @devices = find_devices();
|
||||
foreach my $device (@devices){
|
||||
my $rooms = main::AttrVal($device,"room",undef);
|
||||
if (defined $rooms){
|
||||
foreach my $room (split(/,/, $rooms)){
|
||||
if ($my_room eq $room){
|
||||
push(@result,$device);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
main::Log(3,"Blinds '$device' not mapped to a room");
|
||||
}
|
||||
}
|
||||
return @result;
|
||||
}
|
||||
|
||||
|
||||
sub find_devices{
|
||||
my $devstr = main::fhem("list .* type");
|
||||
my @result = ();
|
||||
foreach my $device (split /\n/, $devstr) {
|
||||
$device =~ s/^\s+|\s+$//g; # trim white spaces
|
||||
if( length($device) > 0){
|
||||
$device =~ /^(\S+)\s+(\S+)$/;
|
||||
my $devname = $1;
|
||||
my $model = $2;
|
||||
if ($model eq "VenetianBlindController"){
|
||||
push(@result,$devname);
|
||||
}
|
||||
}
|
||||
}
|
||||
return @result;
|
||||
}
|
||||
|
||||
|
||||
1;
|
283
fhem/FHEM/lib/VenetianBlinds/VenetianBlindController.pm
Normal file
283
fhem/FHEM/lib/VenetianBlinds/VenetianBlindController.pm
Normal file
@ -0,0 +1,283 @@
|
||||
##############################################
|
||||
#
|
||||
# This is open source software licensed unter the Apache License 2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
##############################################
|
||||
package VenetianBlinds::VenetianBlindController;
|
||||
|
||||
use v5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
use experimental "smartmatch";
|
||||
use VenetianBlinds::Shared "scenes";
|
||||
|
||||
|
||||
# constants ########################
|
||||
|
||||
use constant blind_threshold => 5; #percentage points
|
||||
use constant slat_threshold => 5; #percentage points
|
||||
use constant power_threshold => 10; #watts
|
||||
|
||||
# FHEM commands ########################
|
||||
|
||||
sub Define{
|
||||
#TODO: check if $device and $master really exist
|
||||
my ($hash,$a,$h) = @_;
|
||||
|
||||
if (!defined $h->{master}) {
|
||||
return "Mandatory argument 'master=<name>' is missing or undefined";
|
||||
}
|
||||
$hash->{master_controller} = $h->{master};
|
||||
|
||||
if (!defined $h->{device}) {
|
||||
return "Mandatory argument 'device=<name>' is missing or undefined";
|
||||
}
|
||||
$hash->{device} = $h->{device};
|
||||
|
||||
if (!defined $h->{could_index_threshold}) {
|
||||
return "Mandatory argument 'could_index_threshold=<value>' is missing or undefined";
|
||||
}
|
||||
$hash->{could_index_threshold} = $h->{could_index_threshold};
|
||||
|
||||
if (!defined $h->{azimuth} ) {
|
||||
return "Mandatory argument 'azimuth=<start>-<end>' is missing or undefined";
|
||||
}
|
||||
my ($azstart,$azend) = split(/-/, $h->{azimuth});
|
||||
$hash->{azimuth_start} = $azstart;
|
||||
$hash->{azimuth_end} = $azend;
|
||||
|
||||
if (!defined $h->{elevation}) {
|
||||
return "Mandatory argument 'elevation=<start>-<end>' is missing or undefined";
|
||||
}
|
||||
my ($evstart,$evend) = split(/-/, $h->{elevation});
|
||||
$hash->{elevation_start} = $evstart;
|
||||
$hash->{elevation_end} = $evend;
|
||||
|
||||
if (!defined $h->{months} ) {
|
||||
return "Mandatory argument 'months=<start>-<end>' is missing or undefined";
|
||||
} $hash->{azimuth_start} = $azstart;
|
||||
my ($monstart,$monend) = split(/-/, $h->{months});
|
||||
$hash->{month_start} = $monstart;
|
||||
$hash->{month_end} = $monend;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub Set{
|
||||
my ( $hash, $a,$h ) = @_;
|
||||
my $cmd = $a->[1];
|
||||
my @scene_list = keys %{&scenes};
|
||||
if ( $cmd eq "?" ){
|
||||
my $result = "automatic:noArg wind_alarm:noArg stop:noArg";
|
||||
foreach my $scene (@scene_list){
|
||||
$result .= " $scene:noArg";
|
||||
}
|
||||
return $result;
|
||||
} elsif ($cmd eq "automatic") {
|
||||
main::readingsSingleUpdate($hash,"automatic",1,1);
|
||||
update_automatic($hash,1);
|
||||
} elsif ($cmd ~~ @scene_list) {
|
||||
main::readingsSingleUpdate($hash,"automatic",0,1);
|
||||
set_scene($hash, $cmd, 0);
|
||||
} elsif ($cmd eq "scenes") {
|
||||
delete $hash->{scences};
|
||||
} elsif ($cmd eq "wind_alarm") {
|
||||
wind_alarm($hash);
|
||||
} elsif ($cmd eq "stop") {
|
||||
stop($hash);
|
||||
} else {
|
||||
return "unknown command $cmd";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
sub Notify{
|
||||
my ($hash, $devName, $events) = @_;
|
||||
if ($devName eq $hash->{master_controller}){
|
||||
update_automatic($hash,0);
|
||||
} elsif ($devName eq $hash->{device}) {
|
||||
update_STATE($hash);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
# logic for blind control #####################
|
||||
sub update_automatic{
|
||||
my ($hash,$force) = @_;
|
||||
my $master = $hash->{master_controller};
|
||||
my $sun_elevation = main::ReadingsVal($master, "sun_elevation", undef);
|
||||
my $sun_azimuth = main::ReadingsVal($master, "sun_azimuth", undef);
|
||||
my $wind_speed = main::ReadingsVal($master, "wind_speed", undef);
|
||||
my $wind_alarm = main::ReadingsVal($master, "wind_alarm", undef);
|
||||
my $cloud_index = main::ReadingsVal($master, "cloud_index", undef);
|
||||
my $month = main::ReadingsVal($master, "month", undef);
|
||||
my $automatic = main::ReadingsVal($hash->{NAME}, "automatic", undef);
|
||||
my $old_scene = main::ReadingsVal($hash->{NAME}, "scene", undef);
|
||||
my $mechanical_switches = main::ReadingsVal($hash->{device}, "reportedState", undef);
|
||||
|
||||
# reasons to not work in automatic mode
|
||||
if ($wind_alarm
|
||||
or !$automatic
|
||||
or $month < $hash->{month_start}
|
||||
or $month > $hash->{month_end}
|
||||
or $mechanical_switches eq "setOn"
|
||||
or $mechanical_switches eq "setOff" ){
|
||||
return;
|
||||
main::Log(3,"Automatic inactive on $hash->{NAME}");
|
||||
}
|
||||
|
||||
my $new_scene = undef;
|
||||
if ($hash->{elevation_start} <= $sun_elevation and
|
||||
$sun_elevation <= $hash->{elevation_end} and
|
||||
$hash->{azimuth_start} <= $sun_azimuth and
|
||||
$sun_azimuth <= $hash->{azimuth_end} and
|
||||
$cloud_index <= $hash->{could_index_threshold}) {
|
||||
$new_scene ="shaded";
|
||||
} else {
|
||||
$new_scene = "open";
|
||||
}
|
||||
|
||||
if ($force or !($new_scene eq $old_scene) or ($new_scene eq "adaptive") ) {
|
||||
set_scene($hash,$new_scene,0);
|
||||
} else {
|
||||
main::Log(5,"Scene has not changed on $hash->{NAME}, not moving blinds");
|
||||
}
|
||||
}
|
||||
|
||||
# smart slat control #######################
|
||||
#
|
||||
# This equation converts the elevation of the sun to the angle of the slats.
|
||||
# It's an approximation of the trigonometric functions for my slat geometry.
|
||||
#
|
||||
# The goal of the "adaptive" mode is to set the angle of the slats just closed
|
||||
# enough that the sun can't get in. So if the sun elevation is very high
|
||||
# (e.g. 60°) the slats should be vertical so that the sun can't get in, but you
|
||||
# can still look out. If the elevation of the sun is very low (e.g. 10°) the
|
||||
# slats should be fully closed as the sun would otherwise get through the slats.
|
||||
#
|
||||
# So the task is to compute the angle of the slats in a way that the sun does
|
||||
# not get in, but you still can look through.
|
||||
#
|
||||
# For that we need to know the geometry of the slats. For my slats: a=7.2cm
|
||||
# (distance between two slats) and b=8cm (length of the slats).
|
||||
# the geometry is explained here: doc/adaptive_mode/slat_geomertry.jpg
|
||||
#
|
||||
# As it's quite compicated to solve the equation for beta, I created a spread
|
||||
# sheet and used that to get an approximation for the curves. And this
|
||||
# approximation is then implemented in this function. The spread sheet is in
|
||||
# doc/adaptive_mode/approximation_for_slat_geomertry.ods
|
||||
#
|
||||
# TODO: to implement this properly, we need to make the slat geometry configurable
|
||||
|
||||
|
||||
sub get_slats_for_elevation{
|
||||
my ($hash) = @_;
|
||||
my $master = $hash->{master_controller};
|
||||
my $elevation = main::ReadingsVal($master, "sun_elevation", undef);
|
||||
if ($elevation >= 45) {
|
||||
return 50;
|
||||
} elsif ($elevation <= 10) {
|
||||
return 0;
|
||||
}
|
||||
return int($elevation*1.16 + 4);
|
||||
}
|
||||
|
||||
|
||||
# move the blinds ##########################
|
||||
sub set_scene{
|
||||
my ($hash,$scene,$force) = @_;
|
||||
my $automatic = main::ReadingsVal($hash->{NAME}, "automatic", undef);
|
||||
my $old_scene = main::ReadingsVal($hash->{NAME}, "scene", undef);
|
||||
|
||||
if (!defined &scenes->{$scene}){
|
||||
main::Log(1, "undefined scene &scenes->{$scene}");
|
||||
} else {
|
||||
main::readingsSingleUpdate($hash,"scene",$scene,1);
|
||||
main::Log(3,"moving blinds $hash->{device} to scene $scene.");
|
||||
move_blinds($hash, &scenes->{$scene}{blind}, &scenes->{$scene}{slat});
|
||||
}
|
||||
update_STATE($hash);
|
||||
}
|
||||
|
||||
sub update_STATE {
|
||||
my ($hash) = @_;
|
||||
my $automatic = main::ReadingsVal($hash->{NAME}, "automatic", undef);
|
||||
my $scene = main::ReadingsVal($hash->{NAME}, "scene", undef);
|
||||
my $mechanical_switches = main::ReadingsVal($hash->{device}, "reportedState", undef);
|
||||
|
||||
if ($mechanical_switches eq "setOn") {
|
||||
$hash->{STATE} = "mechanical: Up";
|
||||
} elsif ( $mechanical_switches eq "setOff") {
|
||||
$hash->{STATE} = "mechanical: Down";
|
||||
} elsif ($automatic) {
|
||||
$hash->{STATE} = "automatic: $scene";
|
||||
} else {
|
||||
$hash->{STATE} = "manual: $scene";
|
||||
}
|
||||
}
|
||||
|
||||
sub move_blinds{
|
||||
my ($hash, $blind, $slat)= @_;
|
||||
my ($current_blind, $current_slat) = get_position($hash);
|
||||
if (defined $slat and $slat eq "adaptive") {
|
||||
$slat = get_slats_for_elevation($hash);
|
||||
}
|
||||
if ( defined $blind and
|
||||
abs($blind-$current_blind) > &blind_threshold ){
|
||||
main::fhem("set $hash->{device} positionBlinds $blind");
|
||||
count_commands($hash);
|
||||
}
|
||||
if ( defined $slat and
|
||||
abs($slat - $current_slat) > &slat_threshold ){
|
||||
main::fhem("set $hash->{device} positionSlat $slat");
|
||||
}
|
||||
}
|
||||
|
||||
sub wind_alarm{
|
||||
my ($hash) = @_;
|
||||
move_blinds($hash,99,undef);
|
||||
}
|
||||
|
||||
sub stop {
|
||||
my ($hash) = @_;
|
||||
main::fhem("set $hash->{device} stop");
|
||||
count_commands($hash);
|
||||
delete $hash->{queue};
|
||||
}
|
||||
|
||||
sub count_commands{
|
||||
my ($hash) = @_;
|
||||
my $count = main::ReadingsVal($hash->{NAME}, "command_count", 0) +1;
|
||||
main::readingsSingleUpdate($hash,"command_count",$count,0);
|
||||
}
|
||||
|
||||
|
||||
# wrappers around readings #############################
|
||||
sub get_power{
|
||||
my ($hash) = @_;
|
||||
main::fhem("get $hash->{device} smStatus", undef);
|
||||
my $power_reading = main::ReadingsVal($hash->{device}, "power", undef);
|
||||
$power_reading =~ /([\d\.]+)\WW/;
|
||||
if (!defined $1){
|
||||
main::Log(1,"Error reading power level of $hash->{device}:'$power_reading'");
|
||||
}
|
||||
return $1;
|
||||
}
|
||||
|
||||
sub get_position{
|
||||
my ($hash) = @_;
|
||||
main::fhem("get $hash->{device} position");
|
||||
my $device=$hash->{device};
|
||||
#TODO: do we really need a ReadingsVal or does the "get position" also deliver that result?
|
||||
my $position = main::ReadingsVal($device, "position", undef);
|
||||
$position =~ /Blind (\d+) Slat (\d+)/;
|
||||
if (!defined $1 or !defined $2){
|
||||
main::Log( 1, "Error: could not get position of device $hash->{device}: $position");
|
||||
}
|
||||
return ($1,$2);
|
||||
}
|
||||
|
||||
1; # end module
|
185
fhem/FHEM/lib/VenetianBlinds/VenetianMasterController.pm
Normal file
185
fhem/FHEM/lib/VenetianBlinds/VenetianMasterController.pm
Normal file
@ -0,0 +1,185 @@
|
||||
##############################################
|
||||
#
|
||||
# This is open source software licensed unter the Apache License 2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
##############################################
|
||||
|
||||
package VenetianBlinds::VenetianMasterController;
|
||||
use v5.14;
|
||||
use strict;
|
||||
use warnings;
|
||||
use experimental "smartmatch";
|
||||
use VenetianBlinds::Shared;
|
||||
|
||||
# Map the condition codes from yahoo to cloudiness index,
|
||||
# makes it easier to implement thresholds as higher number indicates more clouds
|
||||
# https://developer.yahoo.com/weather/documentation.html
|
||||
# https://de.wikipedia.org/wiki/Bew%C3%B6lkung#Einteilung_des_Flugwetterdienstes
|
||||
|
||||
my $yahoo_code_map = {
|
||||
#TODO: add mapping for more codes
|
||||
23 => 4, # blustery
|
||||
24 => 4, # windy
|
||||
26 => 5, # cloudy
|
||||
28 => 6, # mostly cloudy (day)
|
||||
30 => 3, # partly cloudy (day)
|
||||
32 => 1, # sunny
|
||||
34 => 2, # fair (day)
|
||||
36 => 0, # hot
|
||||
38 => 5, #scattered thunderstorms
|
||||
39 => 5, #scattered thunderstorms
|
||||
};
|
||||
|
||||
sub Define{
|
||||
my ($hash,$a,$h) = @_;
|
||||
|
||||
if (!defined $h->{twilight}) {
|
||||
return "Mandatory argument 'twilight=<name>' is missing or undefined";
|
||||
}
|
||||
$hash->{twilight} = $h->{twilight};
|
||||
|
||||
if (!defined $h->{weather}) {
|
||||
return "Mandatory argument 'weather=<name>' is missing or undefined";
|
||||
}
|
||||
$hash->{weather} = $h->{weather};
|
||||
|
||||
if (!defined $h->{wind_speed_threshold}) {
|
||||
return "Mandatory argument 'wind_speed_threshold=<value>' is missing or undefined";
|
||||
}
|
||||
$hash->{wind_speed_threshold} = $h->{wind_speed_threshold};
|
||||
|
||||
$hash->{STATE} = "OK";
|
||||
return;
|
||||
}
|
||||
|
||||
sub Set{
|
||||
my ($hash,$a,$h) = @_;
|
||||
my $cmd = $a->[1];
|
||||
if ($cmd eq "?"){
|
||||
return "trigger_update:noArg stop:noArg automatic:noArg";
|
||||
} elsif ($cmd eq "trigger_update") {
|
||||
trigger_update($hash);
|
||||
} elsif ($cmd eq "stop") {
|
||||
VenetianBlinds::Shared::send_to_all("stop");
|
||||
} elsif ($cmd eq "automatic") {
|
||||
VenetianBlinds::Shared::send_to_all("automatic");
|
||||
} else {
|
||||
return "unknown command $cmd";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
sub Notify{
|
||||
my ($hash, $devName, $events) = @_;
|
||||
if ($devName eq $hash->{twilight}) {
|
||||
update_twilight($hash);
|
||||
} elsif ($devName eq $hash->{weather}){
|
||||
update_weather($hash);
|
||||
check_wind_alarm($hash);
|
||||
}
|
||||
|
||||
#foreach my $event (@{$events}) {
|
||||
# $event = "" if(!defined($event));
|
||||
# main::Log(3,"Event on device $devName: $event");
|
||||
#}
|
||||
return;
|
||||
}
|
||||
|
||||
sub trigger_update {
|
||||
my ($hash) = @_;
|
||||
update_twilight($hash);
|
||||
update_calendar($hash);
|
||||
update_weather($hash);
|
||||
check_wind_alarm($hash);
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
sub update_twilight{
|
||||
my ($hash) = @_;
|
||||
# TODO: reduce number of events: only trigger event if data has changed
|
||||
main::readingsBeginUpdate($hash);
|
||||
main::readingsBulkUpdate($hash, "sun_elevation",
|
||||
main::ReadingsVal($hash->{twilight}, "elevation", undef) );
|
||||
main::readingsBulkUpdate($hash, "sun_azimuth",
|
||||
main::ReadingsVal($hash->{twilight}, "azimuth", undef) );
|
||||
main::readingsEndUpdate($hash, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
sub update_calendar{
|
||||
my ($hash) = @_;
|
||||
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
|
||||
# Note: months start at 0 = January
|
||||
$mon +=1;
|
||||
my $current = main::ReadingsVal($hash->{NAME}, "month", undef);
|
||||
if ($mon != $current){
|
||||
main::readingsSingleUpdate($hash,"month",$mon,1);
|
||||
}
|
||||
#TODO: do the update exactly at midnight
|
||||
main::InternalTimer(main::gettimeofday()+24*60*60, "VenetianMasterController::update_calendar", $hash, 1);
|
||||
return ;
|
||||
}
|
||||
|
||||
sub update_weather{
|
||||
my ($hash) = @_;
|
||||
my $condition_code = main::ReadingsVal($hash->{weather}, "code", undef);
|
||||
if (!defined $condition_code) {
|
||||
main::Log(1,"could not get Weather condition code from '$hash->{weather}'");
|
||||
}
|
||||
my $cloud_index = undef;
|
||||
$cloud_index = $yahoo_code_map->{$condition_code};
|
||||
if (!defined $cloud_index){
|
||||
$cloud_index = 9;
|
||||
};
|
||||
my $wind_speed = main::ReadingsVal($hash->{weather}, "wind_speed", undef);
|
||||
if (!defined $wind_speed) {
|
||||
main::Log(1,"could not get Weather wind_speed from '$hash->{weather}'");
|
||||
}
|
||||
|
||||
# TODO: reduce number of events: only trigger event if data has changed
|
||||
main::readingsBeginUpdate($hash);
|
||||
main::readingsBulkUpdate($hash, "wind_speed", $wind_speed);
|
||||
main::readingsBulkUpdate($hash, "cloud_index", $cloud_index);
|
||||
main::readingsEndUpdate($hash, 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub check_wind_alarm{
|
||||
my ($hash) = @_;
|
||||
my $windspeed = main::ReadingsVal($hash->{NAME}, "wind_speed", undef);
|
||||
my $windalarm = main::ReadingsVal($hash->{NAME}, "wind_alarm", undef);
|
||||
given ($windalarm) {
|
||||
when (0) {
|
||||
if (($windspeed >= $hash->{wind_speed_threshold})){
|
||||
main::Log(3,"Wind alarm: $windspeed km/h");
|
||||
main::readingsSingleUpdate($hash,"wind_alarm",1,1);
|
||||
VenetianBlinds::Shared::send_to_all("wind_alarm");
|
||||
$hash->{STATE} = "wind alarm";
|
||||
}
|
||||
}
|
||||
|
||||
when (1) {
|
||||
if (($windspeed >= $hash->{wind_speed_threshold})){
|
||||
main::readingsSingleUpdate($hash,"wind_alarm",1,1);
|
||||
} else {
|
||||
if (main::ReadingsAge($hash->{NAME},"wind_speed",undef) > 600) {
|
||||
main::readingsSingleUpdate($hash,"wind_alarm",0,1);
|
||||
main::Log(3,"Wind alarm ended.");
|
||||
$hash->{STATE} = "normal";
|
||||
}
|
||||
}
|
||||
}
|
||||
when (undef) {
|
||||
main::readingsSingleUpdate($hash,"wind_alarm",0,0);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
94
fhem/FHEM/lib/VenetianBlinds/VenetianRoomController.pm
Normal file
94
fhem/FHEM/lib/VenetianBlinds/VenetianRoomController.pm
Normal file
@ -0,0 +1,94 @@
|
||||
##############################################
|
||||
#
|
||||
# This is open source software licensed unter the Apache License 2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
##############################################
|
||||
|
||||
package VenetianBlinds::VenetianRoomController;
|
||||
|
||||
use v5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
use experimental "smartmatch";
|
||||
use VenetianBlinds::Shared;
|
||||
|
||||
|
||||
# fhem interface #############################################
|
||||
sub Define{
|
||||
my ($hash,$a,$h) = @_;
|
||||
$hash->{STATE} = "OK";
|
||||
if (defined $h->{rooms}) {
|
||||
$hash->{rooms} = $h->{rooms};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub Notify {
|
||||
my ($hash, $devName, $events) = @_;
|
||||
return;
|
||||
}
|
||||
|
||||
sub Set{
|
||||
my ($hash,$a,$h) = @_;
|
||||
my $cmd = $a->[1];
|
||||
my @scene_list = keys %{&VenetianBlinds::Shared::scenes};
|
||||
given ($cmd) {
|
||||
when ("?") {
|
||||
my $result = "automatic:noArg stop:noArg automatic:noArg";
|
||||
foreach my $scene (@scene_list){
|
||||
$result .= " $scene:noArg";
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
when ("automatic") {
|
||||
send_to_all_in_my_rooms($hash, "automatic");
|
||||
}
|
||||
when ("stop"){
|
||||
send_to_all_in_my_rooms($hash, "stop");
|
||||
}
|
||||
when (@scene_list){
|
||||
send_to_all_in_my_rooms($hash, $cmd);
|
||||
}
|
||||
default {
|
||||
return "Unkown command $cmd";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
# room logic #############################################
|
||||
|
||||
sub send_to_all_in_my_rooms{
|
||||
my ($hash, $cmd) = @_;
|
||||
foreach my $room ( get_my_rooms($hash) ){
|
||||
foreach my $device (VenetianBlinds::Shared::find_devices_in_room($room)) {
|
||||
main::fhem("set $device $cmd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub send_to_all_in_room{
|
||||
my ($cmd,$room) = @_;
|
||||
foreach my $device (find_devices_in_room($room)) {
|
||||
main::fhem("set $device $cmd");
|
||||
}
|
||||
}
|
||||
|
||||
sub get_my_rooms{
|
||||
my ($hash) = @_;
|
||||
|
||||
my $rooms = undef;
|
||||
if (defined $hash->{rooms}){
|
||||
$rooms = $hash->{rooms};
|
||||
} else {
|
||||
$rooms = main::AttrVal($hash->{NAME},"room",undef);
|
||||
}
|
||||
if (!defined $rooms) {
|
||||
main::Log(1,"Error reading rooms for VenetianRoomController '$hash->{NAME}'");
|
||||
return;
|
||||
}
|
||||
return split(/,/,$rooms);
|
||||
}
|
||||
|
||||
1;
|
@ -692,3 +692,5 @@
|
||||
- Mon Sep 12 2016 (zap)
|
||||
- added modules 88_HMCCU, 88_HMCCUCHN, 88_HMCCUDEV HMCCUConf for interface FHEM - Homematic CCU2
|
||||
|
||||
- Sat Oct 1 2016 (Christian.Kühnel)
|
||||
- added new module 99_Venetian
|
||||
|
@ -384,6 +384,7 @@ FHME/98_weekprofile.pm risiko http://forum.fhem.de Frontends
|
||||
FHEM/98_STOCKQUOTES.pm vbs http://forum.fhem.de Unterstuetzende Dienste
|
||||
FHEM/99_SUNRISE_EL.pm rudolfkoenig http://forum.fhem.de Automatisierung
|
||||
FHEM/99_Utils.pm rudolfkoenig http://forum.fhem.de Automatisierung
|
||||
FHEM/99_Venetian.pm Christian.Kühnel http://forum.fhem.de Automatisierung
|
||||
FHEM/Blocking.pm rudolfkoenig http://forum.fhem.de Automatisierung
|
||||
FHEM/DevIo.pm rudolfkoenig http://forum.fhem.de Sonstiges
|
||||
FHEM/Color.pm justme1968 http://forum.fhem.de Sonstiges
|
||||
|
Loading…
x
Reference in New Issue
Block a user