fhem-mirror/FHEM/31_MilightDevice.pm
2018-02-04 18:35:23 +00:00

2586 lines
96 KiB
Perl

# $Id$
##############################################
#
# 31_MilightDevice.pm (Based on 32_WifiLight.pm by hermannj)
# FHEM module for MILIGHT lightbulbs. Supports RGB (untested), RGBW and White models.
# Author: Matthew Wire (mattwire)
#
# This file is part of fhem.
#
# Fhem is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Fhem is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
package main;
use strict;
use warnings;
use IO::Handle;
use IO::Socket;
use IO::Select;
use Time::HiRes;
#use Math::Round ();
use Color;
use SetExtensions;
my %dim_values = (
0 => "dim_00",
1 => "dim_10",
2 => "dim_20",
3 => "dim_30",
4 => "dim_40",
5 => "dim_50",
6 => "dim_60",
7 => "dim_70",
8 => "dim_80",
9 => "dim_90",
10 => "dim_100",
);
# RGBW 3 byte commands. 3rd byte not required Bridge V3+
my @RGBWCmdsOn = ("\x45", "\x47", "\x49", "\x4B", "\x42"); # Byte 1 for setting On
my @RGBWCmdsOff = ("\x46", "\x48", "\x4A", "\x4C", "\x41"); # Byte 1 for setting Off
my @RGBWCmdsWT = ("\xC5", "\xC7", "\xC9", "\xCB", "\xC2"); # Byte 1 for setting WhiteMode
my @RGBWCmdsNt = ("\xC6", "\xC8", "\xCA", "\xCC", "\xC1"); # Byte 1 for setting NightMode
my $RGBWCmdBri = "\x4E"; # Byte 1 for setting brightness (Byte 2 specifies level (0x02-0x1B 25 steps)
my $RGBWCmdCol = "\x40"; # Byte 1 for setting color (Byte 2 specifies color value (0x00-0xFF (255 steps))
my $RGBWCmdDiscoUp = "\x4D"; # Byte 1 for setting discoMode Up
my $RGBWCmdDiscoInc = "\x44"; # Byte 1 for setting discoMode speed +
my $RGBWCmdDiscoDec = "\x43"; # Byte 1 for setting discoMode speed -
my $RGBWCmdEnd = "\x55"; # Byte 3
# White 3 byte commands.
my @WhiteCmdsOn = ("\x38", "\x3D", "\x37", "\x32", "\x35"); # Byte 1 for setting On
my @WhiteCmdsOff = ("\x3B", "\x33", "\x3A", "\x36", "\x39"); # Byte 1 for setting Off
my @WhiteCmdsOnFull = ("\xB8", "\xBD", "\xB7", "\xB2", "\xB5"); # Byte 1 for setting full brightness
my @WhiteCmdsNt = ("\xBB", "\xB3", "\xBA", "\xB6", "\xB9"); # Byte 1 for setting NightMode
my @WhiteCmdBriDn = ("\x34", "\x34", "\x34", "\x34", "\xB4"); # Byte 1 for setting Brightness down (11 steps, no direct setting)
my @WhiteCmdBriUp = ("\x3C", "\x3C", "\x3C", "\x3C", "\xBC"); # Byte 1 for setting Brightness up (11 steps, no direct setting)
my @WhiteCmdColDn = ("\x3F", "\x3F", "\x3F", "\x3F", "\xBF"); # Byte 1 for setting colour temp down
my @WhiteCmdColUp = ("\x3E", "\x3E", "\x3E", "\x3E", "\xBE"); # Byte 1 for setting colour temp up
my $WhiteCmdEnd = "\x55"; # Byte 3
sub MilightDevice_Initialize($)
{
my ($hash) = @_;
$hash->{DefFn} = "MilightDevice_Define";
$hash->{UndefFn} = "MilightDevice_Undef";
$hash->{ShutdownFn} = "MilightDevice_Undef";
$hash->{SetFn} = "MilightDevice_Set";
$hash->{GetFn} = "MilightDevice_Get";
$hash->{AttrFn} = "MilightDevice_Attr";
$hash->{NotifyFn} = "MilightDevice_Notify";
$hash->{AttrList} = "IODev dimStep defaultBrightness defaultRampOn " .
"defaultRampOff presets dimOffWhite:1,0 updateGroupDevices:1,0 " .
"restoreAtStart:1,0 colorCast gamma lightSceneParamsToSave " .
$readingFnAttributes;
FHEM_colorpickerInit();
}
#####################################
# Device State Icon for FHEMWEB: Shows a colour changing icon with dim level
sub MilightDevice_devStateIcon($)
{
my($hash) = @_;
$hash = $defs{$hash} if(ref($hash) ne 'HASH');
return undef if(!$hash);
return undef if($hash->{helper}->{group});
my $name = $hash->{NAME};
my $percent = ReadingsVal($name,"brightness","100");
my $s = $dim_values{MilightDevice_roundfunc($percent/10)};
# Return SVG coloured icon with toggle as default action
return ".*:light_light_$s@#".ReadingsVal($name, "rgb", "FFFFFF").":toggle"
if (($hash->{LEDTYPE} eq 'RGBW') || ($hash->{LEDTYPE} eq 'RGB'));
# Return SVG icon with toggle as default action (for White bulbs)
return ".*:light_light_$s:toggle";
}
#####################################
# Define Milight device
sub MilightDevice_Define($$)
{
my ($hash, $def) = @_;
my @args = split("[ \t][ \t]*", $def);
my ($name, $type, $ledtype, $iodev, $slot) = @args;
$hash->{INIT} = 0; # Set to 1 when lamp initialised (MilightDevice_Restore)
$hash->{LEDTYPE} = $ledtype;
$hash->{SLOT} = $slot;
$hash->{SLOTID} = $slot;
if($slot eq 'A') {
$hash->{SLOTID} = 9 if ($hash->{LEDTYPE} eq 'RGBW');
$hash->{SLOTID} = 5 if ($hash->{LEDTYPE} eq 'White');
$hash->{SLOTID} = 0 if ($hash->{LEDTYPE} eq 'RGB');
}
# Validate parameters
return "wrong syntax: define <name> MilightDevice <devType(RGB|RGBW|White)> <IODev> <slot>" if(@args < 5);
return "unknown LED type ($hash->{LEDTYPE}): choose one of RGB, RGBW, White" if ($hash->{LEDTYPE} !~ /RGBW|White|RGB/);
return "Invalid slot: Select one of 1..4 / A for White" if (($hash->{SLOTID} !~ /^\d*$/) || (($hash->{SLOT} ne 'A') && (($hash->{SLOT} < 1) || ($hash->{SLOT} > 4))) && ($hash->{LEDTYPE} eq 'White'));
return "Invalid slot: Select one of 5..8 / A for RGBW" if (($hash->{SLOTID} !~ /^\d*$/) || (($hash->{SLOT} ne 'A') && (($hash->{SLOT} < 5) || ($hash->{SLOT} > 8))) && ($hash->{LEDTYPE} eq 'RGBW'));
return "Invalid slot: Select 0 for RGB" if (($hash->{SLOTID} !~ /^\d*$/) || ($hash->{SLOTID} != 0 && $hash->{LEDTYPE} eq 'RGB'));
Log3 ($hash, 4, $name."_Define: $name $type $hash->{LEDTYPE} $iodev $hash->{SLOT}");
# Verify IODev is valid
AssignIoPort($hash, $iodev);
if(defined($hash->{IODev}->{NAME})) {
Log3 $name, 4, $name."_Define: I/O device is " . $hash->{IODev}->{NAME};
} else {
Log3 $name, 1, $name."_Define: no I/O device";
}
# Look for already defined device on IODev
if ($hash->{SLOT} ne 'A' && defined($hash->{IODev}->{$hash->{SLOT}}->{NAME}))
{
# If defined slot does not match current device name don't allow new definition. Redefining the same device is ok though.
#return "Slot $hash->{SLOT} already defined as $hash->{IODev}->{$hash->{SLOT}}->{NAME}" if ($hash->{IODev}->{$hash->{SLOT}}->{NAME} ne $name);
}
# Define device on IODev
if ($hash->{SLOT} ne 'A')
{
$hash->{IODev}->{$hash->{SLOT}}->{NAME} = $name;
#$hash->{IODev}->{$hash->{SLOT}}->{DEVNAME} = $name;
}
# Define Command Queue
my @cmdQueue = [];
$hash->{helper}->{cmdQueue} = \@cmdQueue;
my $baseCmds = "on off toggle dimup dimdown";
my $sharedCmds = "pair unpair restorePreviousState:noArg saveState:noArg restoreState:noArg";
my $rgbCmds = "hsv rgb:colorpicker,RGB hue:colorpicker,HUE,0,1,360 saturation:slider,0,100,100 preset";
$hash->{helper}->{COMMANDSET} = "$baseCmds discoModeUp:noArg discoSpeedUp:noArg discoSpeedDown:noArg night:noArg white:noArg toggleWhite:noArg $sharedCmds $rgbCmds"
if ($hash->{LEDTYPE} eq 'RGBW');
$hash->{helper}->{COMMANDSET} = "$baseCmds discoModeUp:noArg discoModeDown:noArg discoSpeedUp:noArg discoSpeedDown:noArg $sharedCmds $rgbCmds"
if ($hash->{LEDTYPE} eq 'RGB');
$hash->{helper}->{COMMANDSET} = "$baseCmds hsv ct:colorpicker,CT,3000,350,6500 night:noArg $sharedCmds"
if ($hash->{LEDTYPE} eq 'White');
my $defaultcommandset = $hash->{helper}->{COMMANDSET};
$hash->{helper}->{COMMANDSET} .= " dim:slider,0,".MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)).",100 brightness:slider,0,".MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)).",100";
# webCmds
if (!defined($attr{$name}{webCmd}))
{
$attr{$name}{webCmd} = 'on:off:dim:hue:night:rgb ffffff:rgb ff0000:rgb 00ff00:rgb 0000ff:rgb ffff00' if ($hash->{LEDTYPE} eq 'RGBW');
$attr{$name}{webCmd} = 'on:off:dim:hue:rgb ffffff:rgb ff0000:rgb 00ff00:rgb 0000ff:rgb ffff00' if ($hash->{LEDTYPE} eq 'RGB');
$attr{$name}{webCmd} = 'on:off:dim:ct:night' if ($hash->{LEDTYPE} eq 'White');
}
$hash->{helper}->{GAMMAMAP} = MilightDevice_CreateGammaMapping($hash, 1.0);
$hash->{helper}->{COLORMAP} = MilightDevice_ColorConverter($hash, split(',', "0,0,0,0,0,0"));
# Define devStateIcon
$attr{$name}{devStateIcon} = '{(MilightDevice_devStateIcon($name),"toggle")}' if(!defined($attr{$name}{devStateIcon}));
# Event on change reading
$attr{$name}{"event-on-change-reading"} = "state,transitionInProgress" if (!defined($attr{$name}{"event-on-change-reading"}));
# lightScene
if(!defined($attr{$name}{"lightSceneParamsToSave"}))
{
$attr{$name}{"lightSceneParamsToSave"} = "hsv" if (($hash->{LEDTYPE} eq 'RGBW')|| ($hash->{LEDTYPE} eq 'RGB'));
$attr{$name}{"lightSceneParamsToSave"} = "brightness" if ($hash->{LEDTYPE} eq 'White');
}
# IODev
$attr{$name}{IODev} = $hash->{IODev} if (!defined($attr{$name}{IODev}));
# restoreAtStart
if($slot eq 'A') {
$attr{$name}{"restoreAtStart"} = 0 if (!defined($attr{$name}{"restoreAtStart"}));
} else {
$attr{$name}{"restoreAtStart"} = 1 if (!defined($attr{$name}{"restoreAtStart"}));
}
return undef;
}
sub MilightDevice_Init($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
if( AttrVal($hash->{NAME}, "gamma", "1.0") eq "1.0")
{
Log3 ($name, 5, $name." dimstep ".MilightDevice_roundfunc(100 / MilightDevice_DimSteps($hash))." / gamma 1.0");
} else {
$hash->{helper}->{COMMANDSET} =~ s/dim:slider,0,.*,100/dim:slider,0,1,100/g;
$hash->{helper}->{COMMANDSET} =~ s/brightness:slider,0,.*,100/brightness:slider,0,1,100/g;
Log3 $name, 5, $name." dimstep 1 / gamma ".AttrVal($hash->{NAME}, "gamma", "1.0");
$hash->{helper}->{GAMMAMAP} = MilightDevice_CreateGammaMapping($hash, AttrVal($hash->{NAME}, "gamma", "1.0"));
}
# Colormap / Commandsets
if (($hash->{LEDTYPE} eq 'RGBW') || ($hash->{LEDTYPE} eq 'RGB'))
{
my @a = split(',', "0,0,0,0,0,0");
if ( defined( $attr{$name}{colorCast} ) )
{
@a = split(',', AttrVal($hash->{NAME}, "colorCast", "0,0,0,0,0,0"));
@a = split(',', "0,0,0,0,0,0") unless (@a == 6);
foreach my $tc (@a)
{
@a = split(',', "0,0,0,0,0,0") unless ($tc =~ m/^\s*[\-]{0,1}[0-9]+[\.]{0,1}[0-9]*\s*$/g);
@a = split(',', "0,0,0,0,0,0") if (abs($tc) >= 30);
}
}
$hash->{helper}->{COLORMAP} = MilightDevice_ColorConverter($hash, @a);
}
return undef;
}
#####################################
# Undefine device
sub MilightDevice_Undef(@)
{
my ($hash,$args) = @_;
RemoveInternalTimer($hash);
# Remove slot on bridge
delete ($hash->{IODev}->{$hash->{SLOT}}->{NAME}) if ($hash->{SLOT} ne 'A');
return undef;
}
#####################################
# Set functions
sub MilightDevice_Set(@)
{
my ($hash, $name, $cmd, @args) = @_;
my $cnt = @args;
my $ramp = 0;
my $flags = "";
my $event = undef;
my $usage = "set $name ...";
if ($hash->{IODev}->{STATE} ne "ok" && $hash->{IODev}->{STATE} ne "Initialized") {
readingsSingleUpdate($hash, "state", "error", 1);
$flags = "q";
$args[2] = "" if(!defined($args[2]));
$args[2] .= "q" if ($args[2] !~ m/.*[qQ].*/);
# return SetExtensions($hash, $hash->{helper}->{COMMANDSET}, $name, $cmd, @args);
# IO error, we need to keep our current state settings!
}
# Commands that map to other commands
if ($cmd eq "toggle")
{
$cmd = ReadingsVal($name,"state","on") ne "off" ? "off" :"on";
}
elsif ($cmd eq "white")
{
$cmd = "saturation";
$args[0] = 0;
}
elsif ($cmd eq "toggleWhite")
{
$cmd = "saturation";
$args[0] = (ReadingsVal($name,"saturation",100) > 0) ? 0 : 100;
}
# Commands
if ($cmd eq 'on')
{
if (defined($args[0]))
{
return "Usage: set $name on [seconds(0..X)]" if ($args[0] !~ /^\d+$/);
$ramp = $args[0];
}
elsif (defined($attr{$name}{defaultRampOn}))
{
$ramp = $attr{$name}{defaultRampOn};
}
return MilightDevice_RGBW_On($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_DimOn($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White' && AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1);
return MilightDevice_White_On($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_On($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB');
}
elsif ($cmd eq 'off')
{
if (defined($args[0]))
{
return "Usage: set $name off [seconds(0..X)]" if ($args[0] !~ /^\d+$/);
$ramp = $args[0];
}
elsif (defined($attr{$name}{defaultRampOff}))
{
$ramp = $attr{$name}{defaultRampOff};
}
return MilightDevice_RGBW_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_DimOff($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White' && AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1);
return MilightDevice_White_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB');
}
# Set HSV value
elsif ($cmd eq 'hsv')
{
$usage = "Usage: set $name hsv <h(0..360)>,<s(0..100)>,<v(0..100)> [seconds(0..x)] [flags(l=long path|q=don't clear queue)]";
$usage = "Usage: set $name hsv <h(3000=Warm..6500=Cool)>,<s(-)>,<v(0..100)> [seconds(0..x)] [flags(q=don't clear queue)]" if ($hash->{LEDTYPE} eq 'White');
return $usage if ($args[0] !~ /^(\d{1,4}),(\d{1,3}),(\d{1,3})$/);
my ($h, $s, $v) = ($1, $2, $3);
return "Invalid hue ($h): valid range 0..360" if (!(($h >= 0) && ($h <= 360)) && ($hash->{LEDTYPE} ne 'White'));
return "Invalid color temperature ($h): valid range 3000..6500" if (!(($h >= 3000) && ($h <= 6500)) && ($hash->{LEDTYPE} eq 'White'));
return "Invalid saturation ($s): valid range 0..100" if (!(($s >= 0) && ($s <= 100)) && ($hash->{LEDTYPE} ne 'White'));
return "Invalid brightness ($v): valid range 0..100" if !(($v >= 0) && ($v <= 100));
if (defined($args[1]))
{
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0
$ramp = $args[1];
}
if (defined($args[2]))
{
return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end)
$flags = $args[2];
}
return MilightDevice_White_Transition($hash, $h, 0, $v, $ramp, $flags) if($hash->{LEDTYPE} eq 'White');
return MilightDevice_HSV_Transition($hash, $h, $s, $v, $ramp, $flags);
}
# Dim to a fixed percentage with transition if requested
elsif ($cmd eq 'dim' || $cmd eq 'brightness')
{
$usage = "Usage: set $name dim <percent(0..100)> [seconds(0..x)] [flags(l=long path|q=don't clear queue)]";
return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); # Decimal value for percent between 0..100
if (defined($args[1]))
{
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0
$ramp = $args[1];
}
if (defined($args[2]))
{
return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end)
$flags = $args[2];
}
return MilightDevice_RGBW_Dim($hash, $args[0], $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_Dim($hash, $args[0], $ramp, $flags) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_Dim($hash, $args[0], $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB');
}
# Set night mode
elsif ($cmd eq 'night')
{
if (defined($args[0]))
{
return "Usage: set $name night";
}
return MilightDevice_RGBW_Night($hash) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_Night($hash) if ($hash->{LEDTYPE} eq 'White');
}
# Set hue
elsif ($cmd eq 'hue')
{
$usage = "Usage: set $name hue <h(0..360)> [seconds(0..x)] [flags(l=long path|q=don't clear queue)]";
return $usage if (($args[0] !~ /^(\d+)$/) || ($args[0] < 0) || ($args[0] > 360));
if (defined($args[1]))
{
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0
$ramp = $args[1];
}
if (defined($args[2]))
{
return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end)
$flags = $args[2];
}
my $sat = ReadingsVal($hash->{NAME}, "saturation", 100);
$sat = 100 if(ReadingsVal($hash->{NAME}, "saturation", 0) == 0);
return MilightDevice_HSV_Transition($hash, $args[0], $sat, ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), $ramp, $flags);
}
# Set color temperature
elsif ($cmd eq 'ct')
{
if (defined($args[0]))
{
return "Usage: set $name ct <3000=Warm..6500=Cool>" if (($args[0] !~ /^\d+$/) || ($args[0] < 2500 || $args[0] > 7000));
}
if (defined($args[1]))
{
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0
$ramp = $args[1];
}
if (defined($args[2]))
{
return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end)
$flags = $args[2];
}
return MilightDevice_White_SetColourTemp($hash, $args[0], $ramp, $flags);
}
# Set RGB value
elsif( $cmd eq "rgb")
{
$usage = "Usage: set $name rgb RRGGBB [seconds(0..x)] [flags(l=long path|q=don't clear queue)]";
return $usage if ($args[0] !~ /^([0-9A-Fa-f]{1,2})([0-9A-Fa-f]{1,2})([0-9A-Fa-f]{1,2})$/);
my( $r, $g, $b ) = (hex($1), hex($2), hex($3)); #change to color.pm?
my( $h, $s, $v ) = Color::rgb2hsv($r/255.0,$g/255.0,$b/255.0);
$h = MilightDevice_roundfunc($h * 360);
$s = MilightDevice_roundfunc($s * 100);
$v = MilightDevice_roundfunc($v * 100);
if (defined($args[1]))
{
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0
$ramp = $args[1];
}
if (defined($args[2]))
{
return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end)
$flags = $args[2];
}
return MilightDevice_HSV_Transition($hash, $h, $s, $v, $ramp, $flags);
}
# Dim up by 1 "dimStep" or by a percentage with transition if requested
elsif ($cmd eq 'dimup')
{
$usage = "Usage: set $name dimup [percent change(0..100)] [seconds(0..x)]";
my $percentChange = MilightDevice_roundfunc(100 / MilightDevice_DimSteps($hash)); # Default one dimStep
if (defined($args[0]))
{ # Percent change (0..100%)
return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); # Decimal value for percent between 0..100
$percentChange = $args[0]; # Percentage to change, will be converted in dev specific function
}
if (defined($args[1]))
{ # Seconds for transition (0..x)
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] >= 0)); # Decimal value for ramp > 0
$ramp = $args[1];
# Special case, if percent=100 adjust the ramp so it matches the actual amount required.
# Eg. start: 80%. ramp 5seconds. Amount change: 100-80=20. Ramp time req: 20/100*5 = 1second.
if ($percentChange == 100)
{
my $difference = $percentChange - ReadingsVal($hash->{NAME}, "brightness", 0);
$ramp = ($difference/100) * $ramp;
Log3 ($hash, 5, "$hash->{NAME}_Set: dimdown. Adjusted ramp to $ramp");
}
}
my $newBrightness = ReadingsVal($hash->{NAME}, "brightness", 0) + $percentChange;
$newBrightness = 100 if $newBrightness > 100;
return MilightDevice_RGBW_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB');
}
# Dim down by 1 "dimStep" or by a percentage with transition if requested
elsif ($cmd eq 'dimdown')
{
$usage = "Usage: set $name dimdown [percent change(0..100)] [seconds(0..x)]";
my $percentChange = MilightDevice_roundfunc(100 / MilightDevice_DimSteps($hash)); # Default one dimStep
if (defined($args[0]))
{ # Percent change (0..100%)
return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); # Decimal value for percent between 0..100
$percentChange = $args[0]; # Percentage to change, will be converted in dev specific function
}
if (defined($args[1]))
{ # Seconds for transition (0..x)
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] >= 0)); # Decimal value for ramp > 0
$ramp = $args[1];
# Special case, if percent=100 adjust the ramp so it matches the actual amount required.
# Eg. start: 80%. ramp 5seconds. Amount change: 80. Ramp time req: 80/100*5 = 4second.
if ($percentChange == 100)
{
my $difference = ReadingsVal($hash->{NAME}, "brightness", 0);
$ramp = ($difference/100) * $ramp;
Log3 ($hash, 5, "$hash->{NAME}_Set: dimdown. Adjusted ramp to $ramp");
}
}
my $newBrightness = ReadingsVal($hash->{NAME}, "brightness", 0) - $percentChange;
$newBrightness = 0 if $newBrightness < 0;
return MilightDevice_RGBW_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB');
}
elsif ($cmd eq 'saturation')
{
$usage = "Usage: set $name saturation <h(0..100)> [seconds(0..x)] [flags(q=don't clear queue)]";
return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100));
if (defined($args[1]))
{
return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0
$ramp = $args[1];
}
if (defined($args[2]))
{
return $usage if ($args[2] !~ m/.*[qQ].*/); # Flags q=don't clear queue (add to end)
$flags = $args[2];
}
return MilightDevice_HSV_Transition($hash, ReadingsVal($hash->{NAME}, "hue", 0), $args[0], ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), $ramp, $flags);
}
elsif ($cmd eq 'discoModeUp')
{
return MilightDevice_RGBW_DiscoModeStep($hash, 1);
}
elsif ($cmd eq 'discoModeDown')
{
return MilightDevice_RGBW_DiscoModeStep($hash, 0);
}
elsif ($cmd eq 'discoSpeedUp')
{
return MilightDevice_RGBW_DiscoModeSpeed($hash, 1);
}
elsif ($cmd eq 'discoSpeedDown')
{
return MilightDevice_RGBW_DiscoModeSpeed($hash, 0);
}
elsif ($cmd eq 'restorePreviousState')
{
# Restore the previous state (as store in previous* readings)
my ($h, $s, $v) = MilightDevice_HSVFromStr($hash, ReadingsVal($hash->{NAME}, "previousState", MilightDevice_HSVToStr($hash, 0, 0, 0)));
if($v eq 0)
{
if (defined($attr{$name}{defaultRampOff}))
{
$ramp = $attr{$name}{defaultRampOff};
}
return MilightDevice_RGBW_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_DimOff($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White' && AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1);
return MilightDevice_White_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB');
}
MilightDevice_HSV_Transition($hash, $h, $s, $v, 0, '');
return undef;
}
elsif ($cmd eq 'saveState')
{
# Save the hsv state as a string
readingsSingleUpdate($hash, "savedState", MilightDevice_HSVToStr($hash, ReadingsVal($hash->{NAME}, "hue", 0), ReadingsVal($hash->{NAME}, "saturation", 0), ReadingsVal($hash->{NAME}, "brightness", 0)), 1);
return undef;
}
elsif ($cmd eq 'restoreState')
{
my ($h, $s, $v) = MilightDevice_HSVFromStr($hash, ReadingsVal($hash->{NAME}, "savedState", MilightDevice_HSVToStr($hash, 0, 0, 0)));
return MilightDevice_HSV_Transition($hash, $h, $s, $v, 0, '');
}
elsif ($cmd eq 'preset')
{
my $preset = "+";
# Default to "preset +" if no args defined
if (defined($args[0]))
{
return "Usage: set $name preset <0..X|+>" if ($args[0] !~ /^(\d+|\+)$/);
$preset = $args[0];
}
# Get presets, if not defined default to 1 preset 0,0,100.
my @presets = split(/ /, AttrVal($hash->{NAME}, "presets", MilightDevice_HSVToStr($hash, 0, 0, 100)));
# Load the next preset (and loop back to the first) if "+" specified.
if ("$preset" eq "+")
{
$preset = (ReadingsVal($hash->{NAME}, "lastPreset", -1) + 1);
if ($#presets < $preset) { $preset = 0; }
}
return "No preset defined at index $preset" if ($#presets < $preset);
# Update reading and load preset
readingsSingleUpdate($hash, "lastPreset", $preset, 1);
my ($h, $s, $v) = MilightDevice_HSVFromStr($hash, $presets[$preset]);
return MilightDevice_HSV_Transition($hash, $h, $s, $v, 0, '');
}
elsif ($cmd eq 'pair')
{
if (defined($args[0]))
{
return "Usage: set $name pair [seconds(0..X)(default 3)]" if ($args[0] !~ /^\d+$/);
$ramp = $args[0];
}
else { $ramp = 3; } # Default pair for 3 seconds
MilightDevice_CmdQueue_Clear($hash);
return MilightDevice_RGBW_Pair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_Pair($hash, $ramp) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_Pair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGB');
}
elsif ($cmd eq 'unpair')
{
if (defined($args[0]))
{
return "Usage: set $name unpair [seconds(0..X)(default 3)]" if ($args[0] !~ /^\d+$/);
$ramp = $args[0];
}
else { $ramp = 3; } # Default unpair for 3 seconds
MilightDevice_CmdQueue_Clear($hash);
return MilightDevice_RGBW_UnPair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_UnPair($hash, $ramp) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_UnPair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGB');
}
return SetExtensions($hash, $hash->{helper}->{COMMANDSET}, $name, $cmd, @args);
}
#####################################
# Get functions
sub MilightDevice_Get(@)
{
my ($hash, @args) = @_;
my $name = $args[0];
return "$name: get needs at least one parameter" if(@args < 2);
my $cmd= $args[1];
if($cmd eq "rgb" || $cmd eq "RGB") {
return ReadingsVal($name, "rgb", "FFFFFF");
}
elsif($cmd eq "hsv") {
return MilightDevice_HSVToStr($hash, ReadingsVal($hash->{NAME}, "ct", 3000), 0, ReadingsVal($hash->{NAME}, "brightness", 0)) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_HSVToStr($hash, ReadingsVal($hash->{NAME}, "hue", 0), ReadingsVal($hash->{NAME}, "saturation", 0), ReadingsVal($hash->{NAME}, "brightness", 0));
}
return "Unknown argument $cmd, choose one of rgb:noArg hsv:noArg";
}
#####################################
# Attribute functions
sub MilightDevice_Attr(@)
{
my ($cmd, $device, $attribName, $attribVal) = @_;
my $hash = $defs{$device};
$attribVal = "" if (!defined($attribVal));
Log3 ($hash, 4, "$hash->{NAME}_Attr: Cmd: $cmd; Attribute: $attribName; Value: $attribVal");
if ($cmd eq 'set' && $attribName eq 'gamma')
{
return "gamma is required as numerical value with one decimal (eg. 0.5 or 2.2)" if ($attribVal !~ /^\d*\.\d*$/);
$hash->{helper}->{GAMMAMAP} = MilightDevice_CreateGammaMapping($hash, $attribVal);
if($attribVal ne "1.0")
{
$hash->{helper}->{COMMANDSET} =~ s/dim:slider,0,.*,100/dim:slider,0,1,100/g;
$hash->{helper}->{COMMANDSET} =~ s/brightness:slider,0,.*,100/brightness:slider,0,1,100/g;
}
}
# Allows you to modify the default number of dimSteps for a device
elsif ($cmd eq 'set' && $attribName eq 'dimStep')
{
return "dimStep is required as numerical value [1..100]" if ($attribVal !~ /^\d*$/) || (($attribVal < 1) || ($attribVal > 100));
}
# Allows you to set a default transition time for on/off
elsif ($cmd eq 'set' && (($attribName eq 'defaultRampOn') || ($attribName eq 'defaultRampOff')))
{
return "defaultRampOn/Off is required as numerical value [0..100]" if ($attribVal !~ /^[0-9]*\.?[0-9]*$/) || (($attribVal < 0) || ($attribVal > 100));
}
# List of presets in hsv separated by space. Loaded by set command preset X
elsif ($cmd eq 'set' && ($attribName eq 'presets'))
{
return "presets is required as space separated list of hsv(h,s,v) (eg. 0,0,100, 0,100,50)" if ($attribVal !~ /^[(\d{1,3}),(\d{1,3}),(\d{1,3})(?:$|\s)]*$/);
}
elsif ($cmd eq 'set' && $attribName eq 'colorCast')
{
return "colorCast: only works with RGB(W) devices" if ($hash->{LEDTYPE} eq 'White');
my @a = split(',', $attribVal);
my $msg = "colorCast: correction requires red, yellow, green ,cyan, blue, magenta (each in a range of -29 .. 29)";
return $msg unless (@a == 6);
foreach my $tc (@a)
{
return $msg unless ($tc =~ m/^\s*[\-]{0,1}[0-9]+[\.]{0,1}[0-9]*\s*$/g);
return $msg if (abs($tc) >= 30);
}
$hash->{helper}->{COLORMAP} = MilightDevice_ColorConverter($hash, @a);
#MilightDevice_RGB_ColorConverter($hash, @a);
if ($init_done && !(@{$hash->{helper}->{cmdQueue}} > 0))
{
my $hue = $hash->{READINGS}->{hue}->{VAL};
my $sat = $hash->{READINGS}->{saturation}->{VAL};
my $val = $hash->{READINGS}->{brightness}->{VAL};
return MilightDevice_RGBW_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_RGB_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGB');
}
}
elsif ($cmd eq 'set' && $attribName eq 'defaultBrightness')
{
return "defaultBrighness: has to be between ".MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))." and 100" if ($attribVal < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)) || $attribVal > 100);
}
return undef;
}
#####################################
# Notify functions
sub MilightDevice_Notify(@)
{
my ($hash,$dev) = @_;
return MilightDevice_Restore($hash);
}
#####################################
# Restore HSV settings from readings.
# Called after initialization to synchronise lamp state with fhem.
sub MilightDevice_Restore(@)
{
my ($hash) = @_;
return if ($hash->{INIT});
if ($init_done)
{
return if (AttrVal($hash->{NAME}, "restoreAtStart", 0) == 0);
Log3 ($hash, 4, "$hash->{NAME}_Restore: Restoring saved HSV values");
$hash->{INIT} = 1;
# Initialize device
MilightDevice_Init($hash);
# Clear inProgress flag: MJW Do we still need to do this?
readingsSingleUpdate($hash, "transitionInProgress", 0, 1);
# Default to OFF if not defined
my ($hue, $sat, $val);
$hue = ReadingsVal($hash->{NAME}, "hue", 0);
$sat = ReadingsVal($hash->{NAME}, "saturation", 0);
$val = ReadingsVal($hash->{NAME}, "brightness", 0);
# Restore state
return MilightDevice_RGBW_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGBW');
return MilightDevice_White_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'White');
return MilightDevice_RGB_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGB');
}
}
###############################################################################
# device specific controller functions RGB
# LED Strip or bulb, no white, controller V2+. No longer manufactured Jan2014
###############################################################################
sub MilightDevice_RGB_Pair(@)
{
my ($hash, $numSeconds) = @_;
$numSeconds = 3 if (($numSeconds || 0) == 0);
Log3 ($hash, 4, "$hash->{NAME}_RGB_Pair: RGB LED slot $hash->{SLOT} pair $numSeconds s");
# DISCO SPEED FASTER 0x25 (SYNC/PAIR RGB Bulb within 2 seconds of Wall Switch Power being turned ON)
my $ctrl = "\x25\x00\x55";
for (my $i = 0; $i < $numSeconds; $i++)
{
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 1000, undef);
}
return undef;
}
#####################################
sub MilightDevice_RGB_UnPair(@)
{
my ($hash) = @_;
my $numSeconds = 8;
Log3 ($hash, 4, "$hash->{NAME}_RGB_UnPair: RGB LED slot $hash->{SLOT} unpair $numSeconds s");
# DISCO SPEED FASTER 0x25 (SYNC/PAIR RGB Bulb within 2 seconds of Wall Switch Power being turned ON)
my $ctrl = "\x25\x00\x55";
for (my $i = 0; $i < $numSeconds; $i++)
{
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
}
return undef;
}
#####################################
sub MilightDevice_RGB_On(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36);
Log3 ($hash, 4, "$hash->{NAME}_RGB_On: RGB slot $hash->{SLOT} set on $ramp");
# Switch on with same brightness it was switched off with, or max if undefined.
if (ReadingsVal($hash->{NAME}, "state", "off") eq "off")
{
$v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
else
{
$v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
# When turning on, make sure we request at least minimum dim step.
if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)))
{
$v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash));
}
return MilightDevice_RGB_Dim($hash, $v, $ramp, $flags);
}
#####################################
sub MilightDevice_RGB_Off(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_RGB_Off: RGB slot $hash->{SLOT} set off $ramp");
# Store value of brightness before turning off
# "on" will be of the form "on 50" where 50 is current dimlevel
if (ReadingsVal($hash->{NAME}, "state", "off") ne "off")
{
readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1);
MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
# Dim down to min brightness then send off command (avoid flicker on turn on)
MilightDevice_RGB_Dim($hash, MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)), $ramp, $flags);
return MilightDevice_RGB_Dim($hash, 0, 0, 'qP');
}
else
{
# If we are already off just send the off command again
return MilightDevice_RGB_Dim($hash, 0, 0, 'P');
}
}
#####################################
sub MilightDevice_RGB_Dim(@)
{
my ($hash, $level, $ramp, $flags) = @_;
my $h = ReadingsVal($hash->{NAME}, "hue", 0);
my $s = ReadingsVal($hash->{NAME}, "saturation", 0);
Log3 ($hash, 4, "$hash->{NAME}_RGB_Dim: RGB slot $hash->{SLOT} dim $level $ramp $flags");
return MilightDevice_HSV_Transition($hash, $h, $s, $level, $ramp, $flags);
}
#####################################
sub MilightDevice_RGB_SetHSV(@)
{
my ($hash, $hue, $sat, $val, $repeat) = @_;
Log3 ($hash, 4, "$hash->{NAME}_RGB_setHSV: RGB slot $hash->{SLOT} set h:$hue, s:$sat, v:$val");
$sat = 100;
MilightDevice_SetHSV_Readings($hash, $hue, $sat, $val);
# apply gamma correction
my $gammaVal = $hash->{helper}->{GAMMAMAP}[$val];
# convert to device specs
my ($cv, $cl, $wl) = MilightDevice_RGB_ColorConverter($hash, $hue, $sat, $gammaVal);
Log3 ($hash, 4, "$hash->{NAME}_RGB_setHSV: RGB slot $hash->{SLOT} set levels: $cv, $cl, $wl");
$repeat = 1 if (!defined($repeat));
# On first load, colorLevel won't be defined, define it.
$hash->{helper}->{colorLevel} = $cl if (!defined($hash->{helper}->{colorLevel}));
# NOTE: All commands sent twice for reliability (it's udp with no feedback)
if (($wl < 1) && ($cl < 1)) # off
{
# if no white or colour switch off
IOWrite($hash, "\x21\x00\x55"); # switch off
$hash->{helper}->{colorLevel} = 0;
}
else # on
{
if (($wl > 0) || ($cl > 0)) # Colour/White on
{
IOWrite($hash, "\x22\x00\x55"); # switch on
IOWrite($hash, "\x20".chr($cv)."\x55"); # set color
if ($repeat eq 1) {
IOWrite($hash, "\x22\x00\x55"); # switch on
IOWrite($hash, "\x20".chr($cv)."\x55"); # set color
}
# cl decrease
if ($hash->{helper}->{colorLevel} > $cl)
{
for (my $i=$hash->{helper}->{colorLevel}; $i > $cl; $i--)
{
IOWrite($hash, "\x24\x00\x55"); # brightness down
$hash->{helper}->{colorLevel} = $i - 1;
}
}
# cl increase
if ($hash->{helper}->{colorLevel} < $cl)
{
for (my $i=$hash->{helper}->{colorLevel}; $i < $cl; $i++)
{
IOWrite($hash, "\x23\x00\x55"); # brightness up
$hash->{helper}->{colorLevel} = $i + 1;
}
}
}
}
return undef;
}
#####################################
sub MilightDevice_RGB_ColorConverter(@)
{
my ($hash, $h, $s, $v) = @_;
my $color = $hash->{helper}->{COLORMAP}[$h % 360];
# there are 0..9 dim level, setup correction
my $valueSpread = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash));
my $totalVal = MilightDevice_roundfunc($v / $valueSpread);
# saturation 100..50: color full, white increase. 50..0 white full, color decrease
my $colorVal = ($s >= 50) ? $totalVal : int(($s / 50 * $totalVal) +0.5);
my $whiteVal = ($s >= 50) ? int(((100-$s) / 50 * $totalVal) +0.5) : $totalVal;
return ($color, $colorVal, $whiteVal);
}
###############################################################################
# RGBW device specific: Bridge V3+ only.
# Available as GU10, E14, E27, B22, led strip controller...
###############################################################################
sub MilightDevice_RGBW_Pair(@)
{
my ($hash, $numSeconds) = @_;
$numSeconds = 3 if (($numSeconds || 0) == 0);
Log3 ($hash, 4, "$hash->{NAME}_RGBW_Pair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: pair $numSeconds");
# find my slot and get my group-all-on cmd
my $ctrl = @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd;
# Send on command once a second
for (my $i = 0; $i < $numSeconds; $i++)
{
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 1000, undef);
}
return undef;
}
#####################################
sub MilightDevice_RGBW_UnPair(@)
{
my ($hash, $numSeconds, $releaseFromSlot) = @_;
$numSeconds = 3 if (($numSeconds || 0) == 0);
Log3 ($hash, 4, "$hash->{NAME}_RGBW_UnPair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: unpair $numSeconds");
# find my slot and get my group-all-on cmd
my $ctrl = @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd;
# Send on command every 200ms
for (my $i = 0; $i < $numSeconds; $i++)
{
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
}
return undef;
}
#####################################
sub MilightDevice_RGBW_On(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36);
Log3 ($hash, 4, "$hash->{NAME}_RGBW_On: Set ON; Ramp: $ramp");
# Switch on with same brightness it was switched off with, or max if undefined.
if (ReadingsVal($hash->{NAME}, "state", "off") eq "off" || ReadingsVal($hash->{NAME}, "state", "off") eq "night")
{
$v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
else
{
$v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
# When turning on, make sure we request at least minimum dim step.
if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)))
{
$v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash));
}
return MilightDevice_RGBW_Dim($hash, $v, $ramp, $flags);
}
#####################################
sub MilightDevice_RGBW_Off(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_RGBW_Off: Set OFF; Ramp: $ramp");
# Store value of brightness before turning off
# "on" will be of the form "on 50" where 50 is current dimlevel
if (ReadingsVal($hash->{NAME}, "state", "off") ne "off" && ReadingsVal($hash->{NAME}, "state", "off") ne "night")
{
readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", 0), 1);
MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
# Dim down to min brightness then send off command (avoid flicker on turn on)
MilightDevice_RGBW_Dim($hash, MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)), $ramp, $flags);
return MilightDevice_RGBW_Dim($hash, 0, 0, 'qP');
}
else
{
# If we are already off just send the off command again
return MilightDevice_RGBW_Dim($hash, 0, 0, 'P');
}
}
#####################################
sub MilightDevice_RGBW_Night(@)
{
my ($hash) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_RGBW_Night: Set NIGHTMODE");
if(ReadingsVal($hash->{NAME}, "state", "off") ne "night") {
if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0)
{
readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", 4), 1);
MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
}
IOWrite($hash, @RGBWCmdsOff[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # off
}
IOWrite($hash, @RGBWCmdsNt[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # night
readingsSingleUpdate($hash, "state", "night", 1);
MilightDevice_BridgeDevices_Update($hash, "state") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
return undef;
}
#####################################
sub MilightDevice_RGBW_Dim(@)
{
my ($hash, $v, $ramp, $flags) = @_;
my $h = ReadingsVal($hash->{NAME}, "hue", 0);
my $s = ReadingsVal($hash->{NAME}, "saturation", 0);
Log3 ($hash, 4, "$hash->{NAME}_RGBW_Dim: Brightness: $v; Ramp: $ramp; Flags: ". $flags || '');
return MilightDevice_HSV_Transition($hash, $h, $s, $v, $ramp, $flags);
}
#####################################
sub MilightDevice_RGBW_SetHSV(@)
{
my ($hash, $hue, $sat, $val, $repeat) = @_;
my ($cl, $wl);
$repeat = 1 if (!defined($repeat));
my $cv = $hash->{helper}->{COLORMAP}[$hue % 360];
#check dim levels to decide wether to change color or brightness first
my $dimup = 0;
$dimup = 1 if($val > ReadingsVal($hash->{NAME}, "brightness", 100));
# apply gamma correction
my $gammaVal = $hash->{helper}->{GAMMAMAP}[$val];
# brightness 2..27 (x02..x1b) | 25 dim levels
my $cf = MilightDevice_roundfunc((($gammaVal / 100) * MilightDevice_DimSteps($hash)) + 1);
if ($sat < 20)
{
$wl = $cf;
$cl = 0;
$sat = 0;
}
else
{
$cl = $cf;
$wl = 0;
$sat = 100;
}
Log3 ($hash, 5, "MilightDevice_RGBW_SetHSV: h:$hue s:$sat v:$val / cv:$cv wl:$wl cl:$cl ");
# Set readings in FHEM
MilightDevice_SetHSV_Readings($hash, $hue, $sat, $val);
# NOTE: All commands sent twice for reliability (it's udp with no feedback)
# Off is shifted to "2" above so check for < 2
if (($wl < 2) && ($cl < 2)) # off
{
IOWrite($hash, @RGBWCmdsOff[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # group off
IOWrite($hash, @RGBWCmdsOff[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if ($repeat eq 1); # group off
$hash->{helper}->{whiteLevel} = 0;
$hash->{helper}->{colorLevel} = 0;
}
else # on
{
if ($wl > 0) # white
{
IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on
IOWrite($hash, @RGBWCmdsWT[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # white
IOWrite($hash, $RGBWCmdBri.chr($wl).$RGBWCmdEnd); # brightness
if ($repeat eq 1) {
IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on
IOWrite($hash, @RGBWCmdsWT[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # white
IOWrite($hash, $RGBWCmdBri.chr($wl).$RGBWCmdEnd); # brightness
}
}
elsif ($cl > 0) # color
{
IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on
if($dimup)
{
IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color
IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness
} else {
IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness
IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color
}
if ($repeat eq 1) {
IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on
if($dimup)
{
IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color
IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness
} else {
IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness
IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color
}
}
}
$hash->{helper}->{colorValue} = $cv;
$hash->{helper}->{colorLevel} = $cl;
$hash->{helper}->{whiteLevel} = $wl;
}
return undef;
}
####################################
# RGB and RGBW types
sub MilightDevice_RGBW_DiscoModeStep(@)
{
my ($hash, $step) = @_;
MilightDevice_CmdQueue_Clear($hash);
$step = 0 if ($step < 0);
$step = 1 if ($step > 1);
# Set readings in FHEM
MilightDevice_SetDisco_Readings($hash, $step, ReadingsVal($hash->{NAME}, 'discoSpeed', 5));
# NOTE: Only sending commands once, because it makes changes on each successive command
IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($hash->{LEDTYPE} eq 'RGBW')); # group on
IOWrite($hash, "\x22\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # switch on
if ($step == 1)
{
IOWrite($hash, $RGBWCmdDiscoUp."\x00".$RGBWCmdEnd) if (($hash->{LEDTYPE} eq 'RGBW')); # discoMode step up
IOWrite($hash, "\x27\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # discoMode step up
}
elsif ($step == 0)
{
IOWrite($hash, "\x28\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # discoMode step down
# There is no discoMode step down for RGBW
}
return undef;
}
#####################################
# RGB and RGBW types
sub MilightDevice_RGBW_DiscoModeSpeed(@)
{
my ($hash, $speed) = @_;
MilightDevice_CmdQueue_Clear($hash);
$speed = 0 if ($speed < 0);
$speed = 1 if ($speed > 1);
# Set readings in FHEM
MilightDevice_SetDisco_Readings($hash, ReadingsVal($hash->{NAME}, 'discoMode', 1), $speed);
# NOTE: Only sending commands once, because it makes changes on each successive command
IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($hash->{LEDTYPE} eq 'RGBW')); # group on
IOWrite($hash, "\x22\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # switch on
if ($speed == 1)
{
IOWrite($hash, $RGBWCmdDiscoInc."\x00".$RGBWCmdEnd) if ($hash->{LEDTYPE} eq 'RGBW'); # discoMode speed up
IOWrite($hash, "\x25\x00\x55") if ($hash->{LEDTYPE} eq 'RGB'); # discoMode speed up
}
elsif ($speed == 0)
{
IOWrite($hash, $RGBWCmdDiscoDec."\x00".$RGBWCmdEnd) if ($hash->{LEDTYPE} eq 'RGBW'); # discoMode speed down
IOWrite($hash, "\x26\x00\x55") if ($hash->{LEDTYPE} eq 'RGB'); # discoMode speed down
}
return undef;
}
###############################################################################
# White device specific: Warm/Cold White with Dim - Bridge V2+
###############################################################################
sub MilightDevice_White_Pair(@)
{
my ($hash, $numSeconds) = @_;
$numSeconds = 3 if (($numSeconds || 0) == 0);
Log3 ($hash, 4, "$hash->{NAME}_White_Pair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: pair $numSeconds");
# find my slot and get my group-all-on cmd
my $ctrl = @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd;
# Send on command once a second
for (my $i = 0; $i < $numSeconds; $i++)
{
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 1000, undef);
}
return undef;
}
#####################################
sub MilightDevice_White_UnPair(@)
{
my ($hash, $numSeconds, $releaseFromSlot) = @_;
$numSeconds = 3 if (($numSeconds || 0) == 0);
Log3 ($hash, 4, "$hash->{NAME}_White_UnPair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: unpair $numSeconds");
# find my slot and get my group-all-on cmd
my $ctrl = @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd;
for (my $i = 0; $i < $numSeconds; $i++)
{
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
}
return undef;
}
#####################################
sub MilightDevice_White_On(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36);
Log3 ($hash, 4, "$hash->{NAME}_White_On: Set ON: Ramp: $ramp");
# Switch on with same brightness it was switched off with, or max if undefined.
if (ReadingsVal($hash->{NAME}, "state", "off") eq "off" || ReadingsVal($hash->{NAME}, "state", "off") eq "night")
{
$v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
else
{
$v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
# When turning on, make sure we request at least minimum dim step.
if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)))
{
$v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash));
}
return MilightDevice_White_Dim($hash, $v, $ramp, $flags);
}
#####################################
sub MilightDevice_White_Off(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_White_Off: Set OFF; Ramp: $ramp");
# Store value of brightness before turning off
# "on" will be of the form "on 50" where 50 is current dimlevel
if (ReadingsVal($hash->{NAME}, "state", "off") ne "off" && ReadingsVal($hash->{NAME}, "state", "off") ne "night")
{
if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0)
{
readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1);
MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
}
# Dim down to min brightness then send off command (avoid flicker on turn on)
MilightDevice_White_Dim($hash, MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)), $ramp, $flags);
return MilightDevice_White_Dim($hash, 0, 0, 'q');
}
else
{
# If we are already off just send the off command again
return MilightDevice_White_Dim($hash, 0, 0, 'P');
}
}
#####################################
sub MilightDevice_White_DimOff(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_White_DimOff: Set OFF; Ramp: $ramp");
if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0)
{
readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1);
MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
}
for (my $i = 0; $i < 12; $i++)
{
IOWrite($hash, @WhiteCmdBriDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
}
IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
return MilightDevice_White_Dim($hash, 0, 0, 'q');
}
#####################################
sub MilightDevice_White_DimOn(@)
{
my ($hash, $ramp, $flags) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_White_DimOn: Set ON; Ramp: $ramp");
my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36);
if (ReadingsVal($hash->{NAME}, "state", "off") eq "off" || ReadingsVal($hash->{NAME}, "state", "off") eq "night")
{
$v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
else
{
$v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36));
}
# When turning on, make sure we request at least minimum dim step.
if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)))
{
$v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash));
}
MilightDevice_White_Dim($hash, $v, $ramp, $flags);
for (my $i = 0; $i < ($v/(100/MilightDevice_DimSteps($hash))); $i++)
{
#IOWrite($hash, @WhiteCmdBriUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
}
#$ctrl = @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd;
#MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef);
#return MilightDevice_White_Dim($hash, 0, 0, 'q');
}
#####################################
sub MilightDevice_White_Night(@)
{
my ($hash) = @_;
my $name = $hash->{NAME};
Log3 ($hash, 4, "$hash->{NAME}_White_NIGHT: Set NIGHTMODE");
if(ReadingsVal($hash->{NAME}, "state", "off") ne "night")
{
if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0)
{
readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1);
MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
}
IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # off
}
IOWrite($hash, @WhiteCmdsNt[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # night
readingsSingleUpdate($hash, "state", "night", 1);
MilightDevice_BridgeDevices_Update($hash, "state") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
return undef;
}
#####################################
sub MilightDevice_White_Dim(@)
{
my ($hash, $level, $ramp, $flags) = @_;
Log3 ($hash, 4, "$hash->{NAME}_White_Dim: Brightness: $level; Ramp: $ramp; Flags: $flags");
return MilightDevice_HSV_Transition($hash, ReadingsVal($hash->{NAME}, "ct", 3000), 0, $level, $ramp, $flags);
}
#####################################
# $hue is colourTemperature, $val is brightness
sub MilightDevice_White_SetHSV(@)
{
my ($hash, $hue, $sat, $val, $repeat) = @_;
my $name = $hash->{NAME};
$repeat = 1 if (!defined($repeat));
# Validate brightness
$val = 100 if ($val > 100);
$val = 0 if ($val < 0);
# Validate colour temperature
$hue = 6500 if ($hue > 6500);
$hue = 3000 if ($hue < 3000);
my $oldHueStep = MilightDevice_White_ct_hwValue($hash, ReadingsVal($hash->{NAME}, "ct", 6500));
my $newHueStep = MilightDevice_White_ct_hwValue($hash, $hue);
$hue = MilightDevice_White_ct_hwValue($hash, $newHueStep);
# Set colour temperature
if ($oldHueStep != $newHueStep)
{
IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on
if ($oldHueStep > $newHueStep)
{
Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Decrease from $oldHueStep to $newHueStep");
for (my $i=$oldHueStep; $i > $newHueStep; $i--)
{
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Cooler (colourtemp up)
}
}
elsif ($oldHueStep < $newHueStep)
{
Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Increase from $oldHueStep to $newHueStep");
for (my $i=$oldHueStep; $i < $newHueStep; $i++)
{
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Warmer (colourtemp down)
}
}
if(AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1)
{
if($newHueStep == 1)
{
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
}
elsif($newHueStep == 11)
{
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
}
}
}
# apply gamma correction
my $gammaVal = $hash->{helper}->{GAMMAMAP}[$val];
# Calculate brightness hardware value (10 steps / 11 positions for white)
my $maxWl = (100 / MilightDevice_DimSteps($hash));
my $wl = MilightDevice_roundfunc($gammaVal / $maxWl);
# On first load, whiteLevel won't be defined, define it.
$hash->{helper}->{whiteLevel} = $wl if (!defined($hash->{helper}->{whiteLevel}));
if (ReadingsVal($hash, "brightness", 0) > 0)
{
# We are transitioning from on to off so store new value of wl and stop brightness up/down being triggered below
$hash->{helper}->{whiteLevel} = $wl;
}
# Store new values for colourTemperature and Brightness
MilightDevice_SetHSV_Readings($hash, $hue, 0, $val);
# Make sure we actually send off command if we should be off
if ($wl == 0)
{
IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group off
IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd) if ($repeat eq 1); # group off
Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: OFF");
}
elsif ($wl == $maxWl)
{
IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on
IOWrite($hash, @WhiteCmdsOnFull[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on full
if ($repeat eq 1) {
IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on
IOWrite($hash, @WhiteCmdsOnFull[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on full
}
Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: Full Brightness");
}
else
{
# Not off or MAX brightness, so make sure we are on
IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on
IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd) if ($repeat eq 1); # group on
if ($hash->{helper}->{whiteLevel} > $wl)
{
# Brightness level should be decreased
Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: Brightness decrease from $hash->{helper}->{whiteLevel} to $wl");
for (my $i=$hash->{helper}->{whiteLevel}; $i > $wl; $i--)
{
IOWrite($hash, @WhiteCmdBriDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # brightness down
$hash->{helper}->{whiteLevel} = $i - 1;
}
}
elsif ($hash->{helper}->{whiteLevel} < $wl)
{
# Brightness level should be increased
$hash->{helper}->{whiteLevel} = 1 if ($hash->{helper}->{whiteLevel} == 0);
Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: Brightness increase from $hash->{helper}->{whiteLevel} to $wl");
for (my $i=$hash->{helper}->{whiteLevel}; $i < $wl; $i++)
{
IOWrite($hash, @WhiteCmdBriUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # brightness up
$hash->{helper}->{whiteLevel} = $i + 1;
}
}
else
{
Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: ON");
}
}
$hash->{helper}->{whiteLevel} = $wl;
return undef;
}
#####################################
sub MilightDevice_White_SetColourTemp(@)
{
# $hue is colourTemperature (1-11), $val is brightness (0-100%)
my ($hash, $hue) = @_;
my $name = $hash->{NAME};
MilightDevice_CmdQueue_Clear($hash);
# Save old value of ct
my $oldHue = MilightDevice_White_ct_hwValue($hash, ReadingsVal($hash->{NAME}, "ct", 6500));
# Store new values for colourTemperature and Brightness
MilightDevice_SetHSV_Readings($hash, $hue, 0, ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36) ) );
# Validate colourTemperature (11 steps)
# 3000-6500 (350 per step) Warm-White to Cool White
# Maps backwards 1=6500 11=3000
$hue = MilightDevice_White_ct_hwValue($hash, $hue);
Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: $oldHue to $hue");
# Set colour temperature
if ($oldHue != $hue)
{
IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on
if ($oldHue > $hue)
{
Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Decrease from $oldHue to $hue");
for (my $i=$oldHue; $i > $hue; $i--)
{
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Cooler (colourtemp up)
}
}
elsif ($oldHue < $hue)
{
Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Increase from $oldHue to $hue");
for (my $i=$oldHue; $i < $hue; $i++)
{
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Warmer (colourtemp down)
}
}
}
if(AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1)
{
if($hue == 1)
{
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
}
elsif($hue == 11)
{
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd);
}
}
return undef;
}
# Convert from 3000-6500 colourtemperature to hardware value
sub MilightDevice_White_ct_hwValue(@)
{
my ($hash, $ct) = @_;
# Couldn't get switch statement to work so using if
if ($ct == 11) { return 3000; }
elsif ($ct == 10) { return 3350; }
elsif ($ct == 9) { return 3700; }
elsif ($ct == 8) { return 4050; }
elsif ($ct == 7) { return 4400; }
elsif ($ct == 6) { return 4750; }
elsif ($ct == 5) { return 5100; }
elsif ($ct == 4) { return 5450; }
elsif ($ct == 3) { return 5800; }
elsif ($ct == 2) { return 6150; }
elsif ($ct == 1) { return 6500; }
elsif ($ct < 3350) { return 11; }
elsif ($ct < 3700) { return 10; }
elsif ($ct < 4050) { return 9; }
elsif ($ct < 4400) { return 8; }
elsif ($ct < 4750) { return 7; }
elsif ($ct < 5100) { return 6; }
elsif ($ct < 5450) { return 5; }
elsif ($ct < 5800) { return 4; }
elsif ($ct < 6150) { return 3; }
elsif ($ct < 6500) { return 2; }
return 1;
}
###############################################################################
# Device independent routines
###############################################################################
sub MilightDevice_HSVFromStr(@)
{
# Convert HSV values from string in format "h,s,v"
my ($hash, @args) = @_;
if ((!defined($args[0])) || ($args[0] !~ /^(\d{1,4}),(\d{1,3}),(\d{1,3})$/))
{
Log3 ($hash, 3, "MilightDevice_HSVFromStr: Could not parse h,s,v values from $args[0]");
return (0, 0, 0);
}
Log3 ($hash, 5, "MilightDevice_HSVFromStr: Parsed hsv string: h:$1,s:$2,v:$3");
return ($1, $2, $3);
}
#####################################
sub MilightDevice_HSVToStr(@)
{
# Convert HSV values to string in format "h,s,v"
my ($hash, $h, $s, $v) = @_;
$h=0 if (!defined($h));
$s=0 if (!defined($s));
$v=0 if (!defined($v));
Log3 ($hash, 5, "MilightDevice_HSVToStr: h:$h,s:$s,v:$v");
return "$h,$s,$v";
}
#####################################
sub MilightDevice_ValidateHSV(@)
{
# Validate and return valid values for HSV
my ($hash, $h, $s, $v) = @_;
$h = 0 if ($h < 0);
$h = 360 if ($h > 360 && $hash->{LEDTYPE} ne 'White');
$h = 3000 if ($h < 3000 && $hash->{LEDTYPE} eq 'White');
$h = 6500 if ($h > 6500);
$s = 0 if ($s < 0);
$s = 100 if ($s > 100);
$v = 0 if ($v < 0);
$v = 100 if ($v > 100);
return ($h, $s, $v);
}
#####################################
# Return number of steps for each type of bulb
# White: 11 steps (step = 9.1)
# RGB: 9 steps (step = 11)
# RGBW: 25 steps (step = 4)
sub MilightDevice_DimSteps(@)
{
my ($hash) = @_;
return AttrVal($hash->{NAME}, "dimStep", 25) if ($hash->{LEDTYPE} eq 'RGBW');
return AttrVal($hash->{NAME}, "dimStep", 11) if ($hash->{LEDTYPE} eq 'White');
return AttrVal($hash->{NAME}, "dimStep", 9) if ($hash->{LEDTYPE} eq 'RGB');
}
#####################################
# Return number of colour steps for each type of bulb
# White: 11 steps (this is colour temperature)
# RGB: 255 steps (not mentioned in API?)
# RGBW: 255 steps
sub MilightDevice_ColourSteps(@)
{
my ($hash) = @_;
return 255 if ($hash->{LEDTYPE} eq 'RGBW');
return 11 if ($hash->{LEDTYPE} eq 'White');
return 255 if ($hash->{LEDTYPE} eq 'RGB');
}
#####################################
# dispatcher
sub MilightDevice_SetHSV(@)
{
my ($hash, $hue, $sat, $val, $repeat) = @_;
MilightDevice_RGBW_SetHSV($hash, $hue, $sat, $val, $repeat) if ($hash->{LEDTYPE} eq 'RGBW');
MilightDevice_White_SetHSV($hash, $hue, $sat, $val, $repeat) if ($hash->{LEDTYPE} eq 'White');
MilightDevice_RGB_SetHSV($hash, $hue, $sat, $val, $repeat) if ($hash->{LEDTYPE} eq 'RGB');
return undef;
}
#####################################
sub MilightDevice_HSV_Transition(@)
{
my ($hash, $hue, $sat, $val, $ramp, $flags) = @_;
my ($hueFrom, $satFrom, $valFrom, $timeFrom)=0;
# Clear command queue if flag "q" not specified
MilightDevice_CmdQueue_Clear($hash) if ($flags !~ m/.*[qQ].*/);
# if queue in progress set start vals to last cached hsv target, else set start to actual hsv
if (@{$hash->{helper}->{cmdQueue}} > 0)
{
$hueFrom = $hash->{helper}->{targetHue};
$satFrom = $hash->{helper}->{targetSat};
$valFrom = $hash->{helper}->{targetVal};
$timeFrom = $hash->{helper}->{targetTime};
$hueFrom = 0 if(!defined($hueFrom));
$satFrom = 100 if(!defined($satFrom));
$valFrom = 0 if(!defined($valFrom));
$timeFrom = 0 if(!defined($timeFrom));
Log3 ($hash, 5, "$hash->{NAME}_HSV_Transition: Prepare Start (cached): $hueFrom,$satFrom,$valFrom@".$timeFrom);
}
else
{
$hueFrom = ReadingsVal($hash->{NAME}, "hue", 0);
$satFrom = ReadingsVal($hash->{NAME}, "saturation", 0);
$valFrom = ReadingsVal($hash->{NAME}, "brightness", 0);
$timeFrom = gettimeofday();
Log3 ($hash, 5, "$hash->{NAME}_HSV_Transition: Prepare Start (actual): $hueFrom,$satFrom,$valFrom@".$timeFrom);
if ($flags !~ m/.*[pP].*/ and ($hash->{LEDTYPE} eq 'RGB') || ($hash->{LEDTYPE} eq 'RGBW'))
{
# Store previous state if different to requested state
if (($hueFrom != $hue) || ($satFrom != $sat) || ($valFrom != $val))
{
readingsSingleUpdate($hash, "previousState", MilightDevice_HSVToStr($hash, $hueFrom, $satFrom, $valFrom),1);
}
}
}
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Current: $hueFrom,$satFrom,$valFrom");
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Set: $hue,$sat,$val; Ramp: $ramp; Flags: ". $flags);
# Store target vales
$hash->{helper}->{targetHue} = $hue;
$hash->{helper}->{targetSat} = $sat;
$hash->{helper}->{targetVal} = $val;
# if there is no ramp we don't need transition
if (($ramp || 0) == 0)
{
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Set: $hue,$sat,$val; No Ramp");
$hash->{helper}->{targetTime} = $timeFrom;
return MilightDevice_CmdQueue_Add($hash, $hue, $sat, $val, undef, 0, undef);
}
# calculate the left and right turn length based
# startAngle +360 -endAngle % 360 = counter clock
# endAngle +360 -startAngle % 360 = clockwise
my $hueTo = ($hue == 0) ? 1 : ($hue == 360) ? 359 : $hue;
my $fadeLeft = ($hueFrom + 360 - $hue) % 360;
my $fadeRight = ($hue + 360 - $hueFrom) % 360;
my $direction = ($fadeLeft <=> $fadeRight); # -1 = counterclock, +1 = clockwise
$direction = ($direction == 0)?1:$direction; # in dupt cw
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Colour rotation: cc(-1): $fadeLeft, cw(+1): $fadeRight; Shortest: $direction;");
$direction *= -1 if ($flags =~ m/.*[lL].*/); # reverse if long path desired (flag l or L is set)
my $rotation = ($direction == 1)?$fadeRight:$fadeLeft; # angle of hue rotation in based on flags
my $sFade = abs($sat - $satFrom);
my $vFade = abs($val - $valFrom);
# No transition, so set immediately and ignore ramp setting
if ($rotation == 0 && $sFade == 0 && $vFade == 0)
{
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Unchanged. Set: $hue,$sat,$val; Ignoring Ramp");
$hash->{helper}->{targetTime} = $timeFrom;
return MilightDevice_CmdQueue_Add($hash, $hue, $sat, $val, undef, 0, undef);
}
my ($stepWidth, $steps, $maxSteps, $hueToSet, $hueStep, $satToSet, $satStep, $valToSet, $valStep);
# Calculate stepWidth
if ($rotation >= ($sFade || $vFade))
{
# Transition based on Hue, so max steps = colourSteps
$stepWidth = ($ramp * 1000 / $rotation); # how long is one step (set hsv) in ms based on hue
$maxSteps = MilightDevice_ColourSteps($hash);
}
elsif ($sFade >= ($rotation || $vFade))
{
# Transition based on Saturation, so max steps = 2 (devices don't support sat, so set to 0 or 100 mostly)
$stepWidth = ($ramp * 1000 / $sFade); # how long is one step (set hsv) in ms based on sat
$maxSteps = 2;
}
else
{
# Transition based on Brightness, so max steps = dimSteps
$stepWidth = ($ramp * 1000 / $vFade); # how long is one step (set hsv) in ms based on val
$maxSteps = MilightDevice_DimSteps($hash);
}
# Calculate number of steps, limit to max number (no point running more if they are the same)
$steps = int($ramp * 1000 / $stepWidth);
if ($steps > $maxSteps)
{
$stepWidth *= ($steps/$maxSteps);
$steps = $maxSteps;
}
# Calculate number of steps, limit to max number (no point running more if they are the same)
$steps = int($ramp * 1000 / $stepWidth);
if ($steps > $maxSteps)
{
$stepWidth *= ($steps/$maxSteps);
$steps = $maxSteps;
}
# Calculate minimum stepWidth
# Min bridge delay as specified by Bridge * 3 (eg. 100*3=300ms).
# On average min 3 commands need to be sent per step (eg. Group On; Mode; Brightness;) so this gets it approximately right
my $minStepWidth = $hash->{IODev}->{INTERVAL} * 3;
$stepWidth = $minStepWidth if ($stepWidth < $minStepWidth); # Make sure we have min stepWidth
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Steps: $steps; Step Interval(ms): $stepWidth");
# Calculate hue step
$hueToSet = $hueFrom; # Start at current hue
$hueStep = $rotation / $steps * $direction;
# Calculate saturation step
$satToSet = $satFrom; # Start at current saturation
$satStep = ($sat - $satFrom) / $steps;
# Calculate brightness step
$valToSet = $valFrom; # Start at current brightness
$valStep = ($val - $valFrom) / $steps;
for (my $i=1; $i <= $steps; $i++)
{
$hueToSet += $hueStep; # Increment new hue by step (negative step decrements)
$hueToSet -= 360 if ($hueToSet > 360); #handle turn over zero
$hueToSet += 360 if ($hueToSet < 0);
$satToSet += $satStep; # Increment new saturation by step (negative step decrements)
$valToSet += $valStep; # Increment new brightness by step (negative step decrements)
Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Add to Queue: h:".($hueToSet).", s:".($satToSet).", v:".($valToSet)." ($i/$steps)");
MilightDevice_CmdQueue_Add($hash, MilightDevice_roundfunc($hueToSet), MilightDevice_roundfunc($satToSet), MilightDevice_roundfunc($valToSet), undef, $stepWidth, $timeFrom + (($i-1) * $stepWidth / 1000) );
}
# Set target time for completion of sequence.
# This may be slightly higher than what was requested since $stepWidth > minDelay (($steps * $stepWidth) > $ramp)
$hash->{helper}->{targetTime} = $timeFrom + ($steps * $stepWidth / 1000);
Log3 ($hash, 5, "$hash->{NAME}_HSV_Transition: TargetTime: $hash->{helper}->{targetTime}");
return undef;
}
#####################################
sub MilightDevice_White_Transition(@)
{
my ($hash, $ct, $sat, $val, $ramp, $flags) = @_;
my ($ctFrom, $valFrom, $timeFrom)=0;
# Clear command queue if flag "q" not specified
MilightDevice_CmdQueue_Clear($hash) if ($flags !~ m/.*[qQ].*/);
# if queue in progress set start vals to last cached hsv target, else set start to actual hsv
if (@{$hash->{helper}->{cmdQueue}} > 0)
{
$ctFrom = $hash->{helper}->{targetCt};
$valFrom = $hash->{helper}->{targetVal};
$timeFrom = $hash->{helper}->{targetTime};
$ctFrom = 3000 if(!defined($ctFrom));
$valFrom = 0 if(!defined($valFrom));
$timeFrom = 0 if(!defined($timeFrom));
Log3 ($hash, 5, "$hash->{NAME}_White_Transition: Prepare Start (cached): $ctFrom,$valFrom@".$timeFrom);
}
else
{
$ctFrom = ReadingsVal($hash->{NAME}, "ct", 3000);
$valFrom = ReadingsVal($hash->{NAME}, "brightness", 0);
$timeFrom = gettimeofday();
Log3 ($hash, 5, "$hash->{NAME}_White_Transition: Prepare Start (actual): $ctFrom,$valFrom@".$timeFrom);
if ($flags !~ m/.*[pP].*/)
{
# Store previous state if different to requested state
if (($ctFrom != $ct) || ($valFrom != $val))
{
readingsSingleUpdate($hash, "previousState", MilightDevice_HSVToStr($hash, $ctFrom, 0, $valFrom),1);
}
}
}
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Current: $ctFrom,$valFrom");
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Set: $ct,$val; Ramp: $ramp; Flags: ". $flags);
# Store target vales
$hash->{helper}->{targetCt} = $ct;
$hash->{helper}->{targetVal} = $val;
# if there is no ramp we don't need transition
if (($ramp || 0) == 0)
{
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Set: $ct,$val; No Ramp");
$hash->{helper}->{targetTime} = $timeFrom;
return MilightDevice_CmdQueue_Add($hash, $ct, 0, $val, undef, 0, undef);
}
my $vFade = abs($val - $valFrom);
my $ctFade = abs($ct - $ctFrom);
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Colour temp: $ctFade, Brightness: $vFade;");
# No transition, so set immediately and ignore ramp setting
if ($ctFade == 0 && $vFade == 0)
{
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Unchanged. Set: $ct,0,$val; Ignoring Ramp");
$hash->{helper}->{targetTime} = $timeFrom;
return MilightDevice_CmdQueue_Add($hash, $ct, 0, $val, undef, 0, undef);
}
my ($stepWidth, $steps, $maxSteps, $ctToSet, $ctStep, $valToSet, $valStep);
# Calculate stepWidth
if ($ctFade >= $vFade)
{
# Transition based on ct, so max steps = colourSteps
$stepWidth = ($ramp * 1000 / $ctFade /100); # how long is one step (set hsv) in ms based on ct
$maxSteps = MilightDevice_ColourSteps($hash);
}
else
{
# Transition based on Brightness, so max steps = dimSteps
$stepWidth = ($ramp * 1000 / $vFade); # how long is one step (set hsv) in ms based on val
$maxSteps = MilightDevice_DimSteps($hash);
}
# Calculate number of steps, limit to max number (no point running more if they are the same)
$steps = int($ramp * 1000 / $stepWidth);
if ($steps > $maxSteps)
{
$stepWidth *= ($steps/$maxSteps);
$steps = $maxSteps;
}
# Calculate number of steps, limit to max number (no point running more if they are the same)
$steps = int($ramp * 1000 / $stepWidth);
if ($steps > $maxSteps)
{
$stepWidth *= ($steps/$maxSteps);
$steps = $maxSteps;
}
# Calculate minimum stepWidth
# Min bridge delay as specified by Bridge * 3 (eg. 100*3=300ms).
# On average min 3 commands need to be sent per step (eg. Group On; Mode; Brightness;) so this gets it approximately right
my $minStepWidth = $hash->{IODev}->{INTERVAL} * 3;
$stepWidth = $minStepWidth if ($stepWidth < $minStepWidth); # Make sure we have min stepWidth
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Steps: $steps; Step Interval(ms): $stepWidth");
# Calculate hue step
$ctToSet = $ctFrom; # Start at current hue
$ctStep = ($ct - $ctFrom) / $steps;
# Calculate brightness step
$valToSet = $valFrom; # Start at current brightness
$valStep = ($val - $valFrom) / $steps;
for (my $i=1; $i <= $steps; $i++)
{
$ctToSet += $ctStep; # Increment new hue by step (negative step decrements)
$valToSet += $valStep; # Increment new brightness by step (negative step decrements)
Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Add to Queue: ct:".(int($ctToSet)).", s:0, v:".(int($valToSet))." ($i/$steps)");
MilightDevice_CmdQueue_Add($hash, MilightDevice_roundfunc($ctToSet), 0, MilightDevice_roundfunc($valToSet), undef, $stepWidth, $timeFrom + (($i-1) * $stepWidth / 1000) );
}
# Set target time for completion of sequence.
# This may be slightly higher than what was requested since $stepWidth > minDelay (($steps * $stepWidth) > $ramp)
$hash->{helper}->{targetTime} = $timeFrom + ($steps * $stepWidth / 1000);
Log3 ($hash, 5, "$hash->{NAME}_White_Transition: TargetTime: $hash->{helper}->{targetTime}");
return undef;
}
#####################################
sub MilightDevice_SetHSV_Readings(@)
{
my ($hash, $hue, $sat, $val, $val_on) = @_;
my $name = $hash->{NAME};
readingsBeginUpdate($hash); # Start update readings
# Store requested values
readingsBulkUpdate($hash, "brightness", $val);
# Store on brightness so we can turn on at a set brightness
readingsBulkUpdate($hash, "brightness_on", $val_on);
if (($hash->{LEDTYPE} eq 'RGB') || ($hash->{LEDTYPE} eq 'RGBW'))
{
# Store previous state if different to requested state
my $prevHue = ReadingsVal($hash->{NAME}, "hue", 0);
my $prevSat = ReadingsVal($hash->{NAME}, "saturation", 0);
my $prevVal = ReadingsVal($hash->{NAME}, "brightness", 0);
if (($prevHue != $hue) || ($prevSat != $sat) || ($prevVal != $val))
{
readingsBulkUpdate($hash, "previousState", MilightDevice_HSVToStr($hash, $prevHue, $prevSat, $prevVal)) if ReadingsVal($hash->{NAME}, "transitionInProgress", 1) eq 0;
}
readingsBulkUpdate($hash, "saturation", $sat);
readingsBulkUpdate($hash, "hue", $hue);
readingsBulkUpdate($hash, "hsv", MilightDevice_HSVToStr($hash, $hue,$sat,$val));
# Calc RGB values from HSV
my ($r,$g,$b) = Color::hsv2rgb($hue/360.0,$sat/100.0,$val/100.0);
$r *=255; $g *=255; $b*=255;
# Store values
readingsBulkUpdate($hash, "rgb", sprintf("%02X%02X%02X",$r,$g,$b)); # Int to Hex convert
readingsBulkUpdate($hash, "discoMode", 0);
readingsBulkUpdate($hash, "discoSpeed", 0);
}
elsif ($hash->{LEDTYPE} eq 'White')
{
readingsBulkUpdate($hash, "ct", $hue);
readingsBulkUpdate($hash, "hsv", MilightDevice_HSVToStr($hash, $hue,0,$val));
}
readingsBulkUpdate($hash, "state", "on $val") if ($val > 1);
readingsBulkUpdate($hash, "state", "off") if ($val < 2);
readingsEndUpdate($hash, 1);
MilightDevice_BridgeDevices_Update($hash, "bulk") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1);
}
#####################################
sub MilightDevice_SetDisco_Readings(@)
{
# Step/Speed can be "1" or "0" when active
my ($hash, $step, $speed) = @_;
my $name = $hash->{NAME};
if (($hash->{LEDTYPE} eq 'RGBW') || ($hash->{LEDTYPE} eq 'RGB'))
{
my $discoMode = ReadingsVal($hash->{NAME}, "discoMode", 0);
$discoMode = "on";
my $discoSpeed = ReadingsVal($hash->{NAME}, "discoSpeed", 5);
$discoSpeed = "-" if ($speed == 0);
$discoSpeed = "+" if ($speed == 1);
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "discoMode", $step);
readingsBulkUpdate($hash, "discoSpeed", $speed);
readingsEndUpdate($hash, 1);
if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1)
{
MilightDevice_BridgeDevices_Update($hash, "discoMode");
MilightDevice_BridgeDevices_Update($hash, "discoSpeed");
}
}
}
#####################################
sub MilightDevice_ColorConverter(@)
{
my ($hash, $cr, $cy, $cg, $cc, $cb, $cm) = @_;
my @colorMap;
my $adjRed = 0 + $cr;
my $adjYellow = 60 + $cy;
my $adjGreen = 120 + $cg;
my $adjCyan = 180 + $cc;
my $adjBlue = 240 + $cb;
my $adjLilac = 300 + $cm;
my $devRed = 176; # (0xB0)
#my $devYellow = 128; # (0x80)
my $devYellow = 144;
my $devGreen = 96; # (0x60)
#my $devCyan = 48; # (0x30)
my $devCyan = 56;
my $devBlue = 16; # (0x10)
my $devLilac = 224; # (0xE0)
my $i= 360;
# red to yellow
$adjRed += 360 if ($adjRed < 0); # in case of negative adjustment
$devRed += 256 if ($devRed < $devYellow);
$adjYellow += 360 if ($adjYellow < $adjRed);
for ($i = $adjRed; $i <= $adjYellow; $i++)
{
$colorMap[$i % 360] = ($devRed - int((($devRed - $devYellow) / ($adjYellow - $adjRed) * ($i - $adjRed)) +0.5)) % 255;
Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]);
}
#yellow to green
$devYellow += 256 if ($devYellow < $devGreen);
$adjGreen += 360 if ($adjGreen < $adjYellow);
for ($i = $adjYellow; $i <= $adjGreen; $i++)
{
$colorMap[$i % 360] = ($devYellow - int((($devYellow - $devGreen) / ($adjGreen - $adjYellow) * ($i - $adjYellow)) +0.5)) % 255;
Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]);
}
#green to cyan
$devGreen += 256 if ($devGreen < $devCyan);
$adjCyan += 360 if ($adjCyan < $adjGreen);
for ($i = $adjGreen; $i <= $adjCyan; $i++)
{
$colorMap[$i % 360] = ($devGreen - int((($devGreen - $devCyan) / ($adjCyan - $adjGreen) * ($i - $adjGreen)) +0.5)) % 255;
Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]);
}
#cyan to blue
$devCyan += 256 if ($devCyan < $devCyan);
$adjBlue += 360 if ($adjBlue < $adjCyan);
for ($i = $adjCyan; $i <= $adjBlue; $i++)
{
$colorMap[$i % 360] = ($devCyan - int((($devCyan - $devBlue) / ($adjBlue - $adjCyan) * ($i - $adjCyan)) +0.5)) % 255;
Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]);
}
#blue to lilac
$devBlue += 256 if ($devBlue < $devLilac);
$adjLilac += 360 if ($adjLilac < $adjBlue);
for ($i = $adjBlue; $i <= $adjLilac; $i++)
{
$colorMap[$i % 360] = ($devBlue - int((($devBlue - $devLilac) / ($adjLilac - $adjBlue) * ($i- $adjBlue)) +0.5)) % 255;
Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]);
}
#lilac to red
$devLilac += 256 if ($devLilac < $devRed);
$adjRed += 360 if ($adjRed < $adjLilac);
for ($i = $adjLilac; $i <= $adjRed; $i++)
{
$colorMap[$i % 360] = ($devLilac - int((($devLilac - $devRed) / ($adjRed - $adjLilac) * ($i - $adjLilac)) +0.5)) % 255;
Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]);
}
return \@colorMap;
}
#####################################
sub MilightDevice_CreateGammaMapping(@)
{
my ($hash, $gamma) = @_;
#original wifilight gamma was inverted
$gamma = 1/$gamma;
my @gammaMap;
$gammaMap[0] = 0;
for (my $i = 1; $i <= 100; $i += 1)
{
my $correction = ($i / 100) ** (1 / $gamma);
$gammaMap[$i] = $correction * 100;
$gammaMap[$i] = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)) if($gammaMap[$i] < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)));
Log3 ($hash, 5, "$hash->{NAME} create gammamap v-in: ".$i.", v-out: $gammaMap[$i]");
}
return \@gammaMap;
}
###############################################################################
# Device Command Queue
# Triggers commands for long running transitions for a device
###############################################################################
sub MilightDevice_CmdQueue_Add(@)
{
my ($hash, $hue, $sat, $val, $ctrl, $delay, $targetTime) = @_;
my $cmd;
# Validate input
($hue, $sat, $val) = MilightDevice_ValidateHSV($hash, $hue, $sat, $val);
$cmd->{hue} = $hue;
$cmd->{sat} = $sat;
$cmd->{val} = $val;
$cmd->{ctrl} = $ctrl;
$cmd->{delay} = $delay;
$cmd->{targetTime} = $targetTime;
$cmd->{inProgess} = 0;
push @{$hash->{helper}->{cmdQueue}}, $cmd;
my $hexStr = defined($cmd->{ctrl})? unpack("H*", $cmd->{ctrl} || '') : "";
Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Add: h: ".(defined($cmd->{hue})? $cmd->{hue}: "")."; s: ".(defined($cmd->{sat})? $cmd->{sat}: "")."; v: ".(defined($cmd->{val})? $cmd->{val}: "")."; Ctrl $hexStr; TargetTime: ".(defined($cmd->{targetTime})? $cmd->{targetTime}: "")."; QLen: ".@{$hash->{helper}->{cmdQueue}});
my $actualCmd = @{$hash->{helper}->{cmdQueue}}[0];
# sender busy ?
if(defined($actualCmd))
{
return undef if (ref($actualCmd) ne 'HASH');
return undef if (!defined($actualCmd->{inProgess}));
return undef if (($actualCmd->{inProgess} || 0) == 1);
}
return MilightDevice_CmdQueue_Exec($hash);
}
#####################################
sub MilightDevice_CmdQueue_Exec(@)
{
my ($hash) = @_;
RemoveInternalTimer($hash);
#if ($hash->{IODev}->{STATE} ne "ok" && $hash->{IODev}->{STATE} ne "Initialized") {
# InternalTimer(gettimeofday() + 60, "MilightDevice_CmdQueue_Exec", $hash, 0);
# return undef;
#}
my $actualCmd = @{$hash->{helper}->{cmdQueue}}[0];
# transmission complete, remove
shift @{$hash->{helper}->{cmdQueue}} if ($actualCmd->{inProgess});
# next in queue
$actualCmd = @{$hash->{helper}->{cmdQueue}}[0];
my $nextCmd = @{$hash->{helper}->{cmdQueue}}[1];
# return if no more elements in queue
if (!defined($actualCmd->{inProgess}))
{
readingsSingleUpdate($hash, "transitionInProgress", 0, 1); # Clear transitionInProgress flag
return undef;
}
readingsSingleUpdate($hash, "transitionInProgress", 1, 1); # Set transitionInProgress flag
# drop frames if next frame is already scheduled for given time. do not drop if it is the last frame or if it is a control command
while (defined($nextCmd->{targetTime}) && ($nextCmd->{targetTime} < gettimeofday()) && !$actualCmd->{ctrl})
{
shift @{$hash->{helper}->{cmdQueue}};
$actualCmd = @{$hash->{helper}->{cmdQueue}}[0];
$nextCmd = @{$hash->{helper}->{cmdQueue}}[1];
Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Exec: Drop Frame. Queue Length: ".@{$hash->{helper}->{cmdQueue}});
}
Log3 ($hash, 5, "$hash->{NAME}_CmdQueue_Exec: Dropper Delay: ".($actualCmd->{targetTime} - gettimeofday())) if (defined($actualCmd->{targetTime}));
# set hsv or if a device ctrl command is scheduled: send it and ignore hsv
if ($actualCmd->{ctrl})
{
my $dbgStr = unpack("H*", $actualCmd->{ctrl});
Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Exec: Send ctrl: $dbgStr; Queue Length: ".@{$hash->{helper}->{cmdQueue}});
IOWrite($hash, $actualCmd->{ctrl});
}
else
{
# Send an HSV Command.
my $repeat = 0;
# If queue length < 2 (ie. 1) we are last command so repeat sending (takes twice as long...)
$repeat = 1 if (@{$hash->{helper}->{cmdQueue}} < 2);
MilightDevice_SetHSV($hash, $actualCmd->{hue}, $actualCmd->{sat}, $actualCmd->{val}, $repeat);
}
$actualCmd->{inProgess} = 1;
my $next = defined($nextCmd->{targetTime})?$nextCmd->{targetTime}:gettimeofday() + ($actualCmd->{delay} / 1000);
Log3 ($hash, 5, "$hash->{NAME}_CmdQueue_Exec: Next Exec: $next");
InternalTimer($next, "MilightDevice_CmdQueue_Exec", $hash, 0);
return undef;
}
#####################################
sub MilightDevice_CmdQueue_Clear(@)
{
my ($hash) = @_;
Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Clear");
RemoveInternalTimer($hash);
#if ($hash->{IODev}->{STATE} ne "ok" && $hash->{IODev}->{STATE} ne "Initialized") {
# InternalTimer(gettimeofday() + 60, "MilightDevice_CmdQueue_Exec", $hash, 0);
# return undef;
#}
readingsSingleUpdate($hash, "transitionInProgress", 0, 1); # Clear inProgress flag
#foreach my $args (keys %intAt)
#{
# if (($intAt{$args}{ARG} eq $hash) && ($intAt{$args}{FN} eq 'MilightDevice_CmdQueue_Exec'))
# {
# Log3 ($hash, 5, "$hash->{NAME}_CmdQueue_Clear: Remove timer at: ".$intAt{$args}{TRIGGERTIME});
# delete($intAt{$args});
# }
#}
$hash->{helper}->{cmdQueue} = [];
return undef;
}
#####################################
sub MilightDevice_BridgeDevices_Update(@)
{
my ($hash, $attr) = @_;
my @rdlist = ($attr);
if($attr eq 'bulk')
{
@rdlist = ("state","brightness","brightness_on","hue", "saturation", "hsv", "rgb", "discoMode", "discoSpeed")if ($hash->{LEDTYPE} eq 'RGBW');
@rdlist = ("state","brightness","brightness_on","ct")if ($hash->{LEDTYPE} eq 'White');
}
my $sl = 5;
$sl = 1 if ($hash->{LEDTYPE} eq 'White');
for (my $i = 0; $i < 4; $i++)
{
my $devname = $hash->{IODev}->{$sl+$i}->{NAME};
next if (!defined($defs{$devname}));
my $device = $defs{$devname};
$devname = "?" if(!defined($devname));
readingsSingleUpdate($device, "transitionInProgress", 1, 1);
readingsBeginUpdate($device);
foreach my $rdname (@rdlist)
{
if (exists ($device->{READINGS}{$rdname}))
{
readingsBulkUpdate($device, $rdname, $hash->{READINGS}{$rdname}{VAL}, 1);
Log3 ($hash, 4, $rdname.": ".$device->{READINGS}{$rdname}{VAL}." for ".$devname);
}
}
readingsEndUpdate($device, 1);
readingsSingleUpdate($device, "transitionInProgress", 0, 1);
}
return undef;
}
sub MilightDevice_roundfunc($) {
my ($number) = @_;
return sprintf("%.0f", $number);
#return Math::Round::round($number);
}
1;
=pod
=item device
=item summary This module represents a Milight LED Bulb or LED strip controller
=begin html
<a name="MilightDevice"></a>
<h3>MilightDevice</h3>
<ul>
<p>This module represents a Milight LED Bulb or LED strip controller. It is controlled by a <a href="#MilightBridge">MilightBridge</a>.</p>
<p>The Milight system is sold under various brands around the world including "LimitlessLED, EasyBulb, AppLamp"</p>
<p>The API documentation is available here: <a href="http://www.limitlessled.com/dev/">http://www.limitlessled.com/dev/</a></p>
<p>Requires perl module Math::Round</p>
<a name="MilightDevice_define"></a>
<p><b>Define</b></p>
<ul>
<p><code>define &lt;name&gt; MilightDevice &lt;devType(RGB|RGBW|White)&gt; &lt;IODev&gt; &lt;slot&gt;</code></p>
<p>Specifies the Milight device.<br/>
&lt;devType&gt; One of RGB, RGBW, White depending on your device.<br/>
&lt;IODev&gt; The <a href="#MilightBridge">MilightBridge</a> which the device is paired with.<br/>
&lt;slot&gt; The slot on the <a href="#MilightBridge">MilightBridge</a> that the device is paired with or 'A' to group all slots.</p>
</ul>
<a name="MilightDevice_readings"></a>
<p><b>Readings</b></p>
<ul>
<li>
<b>state</b><br/>
[on xxx|off|night]: Current state of the device / night mode (xxx = 0-100%).
</li>
<li>
<b>brightness</b><br/>
[0-100]: Current brightness level in %.
</li>
<li>
<b>brightness_on</b><br/>
[0-100]: The brightness level before the off command was sent. This allows the light to turn back on to the last brightness level.
</li>
<li>
<b>rgb</b><br/>
[FFFFFF]: HEX value for RGB.
</li>
<li>
<b>previousState</b><br/>
[hsv]: hsv value before last change. Can be used with <b>restorePreviousState</b> set command.
</li>
<li>
<b>savedState</b><br/>
[hsv]: hsv value that was saved using <b>saveState</b> set function
</li>
<li>
<b>hue</b><br/>
[0-360]: Current hue value.
</li>
<li>
<b>saturation</b><br/>
[0-100]: Current saturation value.
</li>
<li>
<b>transitionInProgress</b><br/>
[0|1]: Set to 1 if a transition is currently in progress for this device (eg. fade).
</li>
<li>
<b>discoMode</b><br/>
[0|1]: 1 if discoMode is enabled, 0 otherwise.
</li>
<li>
<b>discoSpeed</b><br/>
[0|1]: 1 if discoSpeed is increased, 0 if decreased. Does not mean much for RGBW
</li>
<li>
<b>lastPreset</b><br/>
[0..X]: Last selected preset.
</li>
<li>
<b>ct</b><br/>
[1-10]: Current colour temperature (3000=Warm,6500=Cold) for White devices.
</li>
</ul>
<a name="MilightDevice_set"></a>
<p><b>Set</b></p>
<ul>
<li>
<b>on &lt;ramp_time (seconds)></b>
</li>
<li>
<b>off &lt;ramp_time (seconds)></b>
</li>
<li>
<b>toggle</b>
</li>
<li>
<b>night</b>
</li>
<li>
<b>dim &lt;percent(0..100)&gt; [seconds(0..x)] [flags(l=long path|q=don't clear queue)]</b><br/>
Will be replaced by <i>brightness</i> at some point
</li>
<li>
<b>dimup &lt;percent change(0..100)&gt; [seconds(0..x)]</b><br/>
Special case: If percent change=100, seconds will be adjusted for actual change to go from current brightness.
</li>
<li>
<b>dimdown &lt;percent change(0..100)&gt; [seconds(0..x)]</b><br/>
Special case: If percent change=100, seconds will be adjusted for actual change to go from current brightness.
</li>
<li>
<b>pair</b><br/>
May not work properly. Sometimes it is necessary to use a remote to clear pairing first.
</li>
<li>
<b>unpair</b><br/>
May not work properly. Sometimes it is necessary to use a remote to clear pairing first.
</li>
<li>
<b>restorePreviousState</b><br/>
Set device to previous hsv state as stored in <b>previousState</b> reading.
</li>
<li>
<b>saveState</b><br/>
Save current hsv state to <b>savedState</b> reading.
</li>
<li>
<b>restoreState</b><br/>
Set device to saved hsv state as stored in <b>savedState</b> reading.
</li>
<li>
<b>preset (0..X|+)</b><br/>
Load preset (+ for next preset).
</li>
<li>
<b>hsv &lt;h(0..360)&gt;,&lt;s(0..100)&gt;,&lt;v(0..100)&gt; [seconds(0..x)] [flags(l=long path|q=don't clear queue)]</b><br/>
Set hsv value directly
</li>
<li>
<b>rgb RRGGBB [seconds(0..x)] [flags(l=long path|q=don't clear queue)]</b><br/>
Set rgb value directly or using colorpicker.
</li>
<li>
<b>hue &lt;(0..360)&gt; [seconds(0..x)] [flags(l=long path|q=don't clear queue)]</b><br/>
Set hue value.
</li>
<li>
<b>saturation &lt;s(0..100)&gt; [seconds(0..x)] [flags(q=don't clear queue)]</b><br/>
Set saturation value directly
</li>
<li>
<b>discoModeUp</b><br/>
Next disco Mode setting (for RGB and RGBW).
</li>
<li>
<b>discoModeDown</b><br/>
Previous disco Mode setting (for RGB).
</li>
<li>
<b>discoSpeedUp</b><br/>
Increase speed of disco mode (for RGB and RGBW).
</li>
<li>
<b>discoSpeedDown</b><br/>
Decrease speed of disco mode (for RGB and RGBW).
</li>
<li>
<b>ct &lt;3000-6500&gt;</b><br/>
Colour temperature 3000=Warm White,6500=Cold White (10 steps) (for White devices only).
</li>
<li>
<a href="#setExtensions"> set extensions</a> are supported.
</li>
</ul>
<a name="MilightDevice_get"></a>
<p><b>Get</b></p>
<ul>
<li>
<b>rgb</b>
</li>
<li>
<b>hsv</b>
</li>
</ul>
<a name="MilightDevice_attr"></a>
<p><b>Attributes</b></p>
<ul>
<li>
<b>dimStep</b><br/>
Allows you to modify the default dimStep if required.
</li>
<li>
<b>defaultRampOn</b><br/>
Set the default ramp time if not specified for on command.
</li>
<li>
<b>defaultRampOff</b><br/>
Set the default ramp time if not specified for off command.
</li>
<li>
<b>presets</b><br/>
List of hsv presets separated by spaces (eg 0,0,100 9,0,50).
</li>
<li>
<b>colorCast</b><br/>
Color shift values for red,yellow,green,cyan,blue,magenta (-29..29) for HSV color correction (eg 0,5,10,-5,0,0)
</li>
<li>
<b>gamma</b><br/>
Set gamma correction value for device (eg 0.8)
</li>
<li>
<b>dimOffWhite</b><br/>
Use a different switching logic for White bulbs to better handle packet loss.
</li>
<li>
<b>updateGroupDevices</b><br/>
Update the state of single devices switched with slot 'A'.
</li>
<li>
<b>restoreAtStart</b><br/>
Restore the state of devices at startup. Default 0 for slot 'A', 1 otherwise.
</li>
<li>
<b>defaultBrightness</b><br/>
Set the default brightness if not known. (Default: 36)
</li>
</ul>
</ul>
=end html
=cut