# $Id$ # Änderungen Viegener - 2020-05-04 # Umstellung verzögerte berechnung der Pixel erst beim upload # blocktext (ESPEInk_FormatBlockText) korrigiert für Behandlung von \\n # text height ($th) is potentially wrong if text is wrapped to multiple lines so text height shoul dbe calced from one char only (addobjects) # # Änderungen eki - 2021-10-07 # Zusammenstellunge aller Änderungen aus dem Forumschat # - neues Device mit 5.65 inch und 6 Farben # - neues Attribut mininterval (es wird mindestens mininterval sekunden gewartet, bis ein neues Bild hochgeladen werden kann) # - neues Attribut uploadTimeout (timeout bevor abgebrochen und ein Fehler bezüglich der upload Kommunikation gemeldet wird) # - Korrektur zum Setzen von Farben in icons # - Korrektur bezüglich Warnungen beim Start package main; use strict; use warnings; use Time::HiRes qw( time ); use POSIX qw(strftime); use Encode qw(decode encode); use GD; use Scalar::Util; use File::Basename; use File::Copy; use File::Spec::Functions; use File::Find; use HttpUtils; my $ESPEInk_has_SVG = 0; eval {require Image::LibRSVG}; if ($@) { Log3 undef, 2, "ESPEInk: Error loading LibRSVG. Result of checking LibRSVG is: '$@'"; } else { $ESPEInk_has_SVG = 1; Image::LibRSVG->import(); } #--------------------------------------------------------------------------------------------------- # define all needed palettes (i.e. arrays with rgb values fitting to each of the eInk color values) # needed to map real color values in the picture to respective color values of the eInk display my @ESPEInk_palettes = ( [2,[0,0,0],[255,255,255]], [3,[0,0,0],[255,255,255],[127,0,0]], [3,[0,0,0],[255,255,255],[127,127,127]], [4,[0,0,0],[255,255,255],[127,127,127],[127,0,0]], [2,[0,0,0],[255,255,255]], [3,[0,0,0],[255,255,255],[220,180,0]], [7,[0,0,0],[255,255,255],[0,255,0],[0,0,255],[255,0,0],[255,255,0],[255,128,0]] ); #--------------------------------------------------------------------------------------------------- # define all possible device types and thier parameters (width, height, palette index and id) my %ESPEInk_devices = ( # "4.3inch_e-Paper_UART_Module" => {width => 200, height => 200, pindex => 10, id => 26}, # "10.3inch_e-Paper_HAT_(D)" => {width => 200, height => 200, pindex => 10, id => 25}, # "9.7inch_e-Paper_HAT" => {width => 200, height => 200, pindex => 10, id => 24}, # "7.8inch_e-Paper_HAT" => {width => 200, height => 200, pindex => 10, id => 23}, # "6inch_e-Paper_HAT" => {width => 200, height => 200, pindex => 10, id => 22}, "5.65inch_e-Paper_HAT_(F)" => {width => 600, height => 448, pindex => 6, updateint => 35, id => 25}, "7.5inch_e-Paper_HAT_(B)_HD" => {width => 880, height => 528, pindex => 1, updateint => 21, id => 24}, "7.5inch_e-Paper_HAT_V2_(B)" => {width => 800, height => 480, pindex => 1, updateint => 16, id => 23}, "7.5inch_e-Paper_HAT_V2" => {width => 800, height => 480, pindex => 0, updateint => 5, id => 22}, "7.5inch_e-Paper_HAT_(C)" => {width => 640, height => 384, pindex => 5, updateint => 21, id => 21}, "7.5inch_e-Paper_HAT_(B)" => {width => 640, height => 384, pindex => 1, updateint => 16, id => 20}, "7.5inch_e-Paper_HAT" => {width => 640, height => 384, pindex => 0, updateint => 5, id => 19}, "5.83inch_e-Paper_HAT_(C)" => {width => 600, height => 448, pindex => 5, updateint => 26, id => 18}, "5.83inch_e-Paper_HAT_(B)" => {width => 600, height => 448, pindex => 1, updateint => 20, id => 17}, "5.83inch_e-Paper_HAT" => {width => 600, height => 448, pindex => 0, updateint => 5, id => 16}, "4.2inch_e-Paper_Module_(C)" => {width => 400, height => 300, pindex => 5, updateint => 15, id => 15}, "4.2inch_e-Paper_Module_(B)" => {width => 400, height => 300, pindex => 1, updateint => 15, id => 14}, "4.2inch_e-Paper_Module" => {width => 400, height => 300, pindex => 0, updateint => 4, id => 13}, "2.9inch_e-Paper_Module_(D)" => {width => 128, height => 296, pindex => 0, updateint => 2, id => 12}, "2.9inch_e-Paper_Module_(C)" => {width => 128, height => 296, pindex => 5, updateint => 15, id => 11}, "2.9inch_e-Paper_Module_(B)" => {width => 128, height => 296, pindex => 1, updateint => 15, id => 10}, "2.9inch_e-Paper_Module" => {width => 128, height => 296, pindex => 0, updateint => 2, id => 9}, "2.7inch_e-Paper_HAT_(B)" => {width => 176, height => 264, pindex => 1, updateint => 15, id => 8}, "2.7inch_e-Paper_HAT" => {width => 176, height => 264, pindex => 0, updateint => 6, id => 7}, "2.13inch_e-Paper_HAT_(D)" => {width => 104, height => 212, pindex => 0, updateint => 2, id => 6}, "2.13inch_e-Paper_HAT_(C)" => {width => 104, height => 212, pindex => 5, updateint => 15, id => 5}, "2.13inch_e-Paper_HAT_(B)" => {width => 104, height => 212, pindex => 1, updateint => 15, id => 4}, "2.13inch_e-Paper_HAT" => {width => 122, height => 250, pindex => 0, updateint => 2, id => 3}, "1.54inch_e-Paper_Module_(C)" => {width => 152, height => 152, pindex => 5, updateint => 28, id => 2}, "1.54inch_e-Paper_Module_(B)" => {width => 200, height => 200, pindex => 3, updateint => 8, id => 1}, "1.54inch_e-Paper_Module" => {width => 200, height => 200, pindex => 0, updateint => 2, id => 0} ); #--------------------------------------------------------------------------------------------------- # Default values for upload control (e.g. number of maximum retries in case of communication problems) my %ESPEInk_uploadcontrol = ( "retries" => 0, "maxretries" => 3, "timeout" => 10, "srcindex" => 0, "stepindex" => 0 ); my %ESPEInk_sets = ( "convert" => "noArg", # run conversion of input picture and possible additional objects (text) "upload" => "noArg", # perform upload of converted picture to EInk Display via WLAN "addtext" => "textFieldNL", # add text to picture at given position (see set for details) "addicon" => "textFieldNL", # add icon to picture at given position (see set for details) "addsymbol" => "textFieldNL", # add symbol (line, rectangle, ellipse) to picture at given position (see set for details) "iconreading" => "textFieldNL", # add text to picture at given position (see set for details) "textreading" => "textFieldNL" # add text to picture at given position (see set for details) ); my %ESPEInk_gets = ( "devices" => ":noArg" # get list of all supported devices ); my $ESPEInk_InitializationDone = 0; #--------------------------------------------------------------------------------------------------- # Initialize Module sub ESPEInk_Initialize($) { my ($hash) = @_; $hash->{DefFn} = 'ESPEInk_Define'; $hash->{UndefFn} = 'ESPEInk_Undef'; $hash->{ShutdownFn} = 'ESPEInk_Shutdown'; $hash->{SetFn} = 'ESPEInk_Set'; $hash->{GetFn} = 'ESPEInk_Get'; $hash->{AttrFn} = 'ESPEInk_Attr'; $hash->{NotifyFn} = 'ESPEInk_Notify'; $hash->{UploadFn} = 'ESPEInk_Upload'; $hash->{ConvertFn} = 'ESPEInk_Convert'; my $devs = ""; for my $key ( sort keys %ESPEInk_devices ) { $devs .= $key.","; } $devs = substr($devs,0,length($devs)-1); $hash->{AttrList} = "picturefile " . "url " . "interval " . "devicetype:".$devs." " . "boardtype:ESP8266,ESP32 " . "convertmode:level,dithering " . "colormode:monochrome,color " . "width " . "height " . "x0 " . "y0 " . "placement:top-left,top-right,bottom-left,bottom-right " . "scale2fit:0,1 " . "coloroffset " . "maxretries " . "timeout " . "disable:0,1 " . "definition:textField-long " . "definitionFile " . "mininterval " . "uploadTimeout " . $readingFnAttributes; $hash->{STATE} = "Initialized"; } #--------------------------------------------------------------------------------------------------- # Define new device of type ESPEInk sub ESPEInk_Define($$) { my ($hash, $def) = @_; my @param = split('[ \t]+', $def); if(int(@param) < 3) { return "too few parameters: define ESPEInk "; } if (!open(IMAGE, $param[2])) { return "ESPEInk: Invalid filename $param[2]. Must point to a readable file."; } else { close IMAGE; } $hash->{NAME} = $param[0]; $hash->{PICTUREFILE} = $param[2]; $hash->{SUBFOLDER} = "images"; $hash->{INTERVAL} = 300; $hash->{COLORMODE} = "monochrome"; $hash->{CONVERTMODE} = "level"; $hash->{DEVICETYPE} = "1.54inch_e-Paper_Module"; $hash->{BOARDTYPE} = "ESP8266"; $hash->{URL} = ""; if(int(@param) > 7) { $hash->{CONVERTMODE} = $param[7]; } elsif (int(@param) > 6) { $hash->{COLORMODE} = $param[6]; } elsif (int(@param) > 5) { $hash->{INTERVAL} = $param[5]; } elsif (int(@param) > 4) { $hash->{URL} = $param[4]; } elsif (int(@param) > 3) { $hash->{SUBFOLDER} = $param[3]; } my $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root mkdir $rootname."/".$hash->{SUBFOLDER}."/".$param[0]; copy($param[2],$rootname."/".$hash->{SUBFOLDER}."/".$param[0]); # local copy of the file for usage in FHEMWEB (display) ESPEInk_MakePictureHTML($param[0],"source_picture",basename($param[2])); ESPEInk_ResetNotifies({hash=>$hash}); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "ESPEInk_FullUpdate", $hash, 0) if ($hash->{INTERVAL} > 0); $hash->{STATE} = "Added as new device"; return undef; } #--------------------------------------------------------------------------------------------------- # shutdown device sub ESPEInk_Shutdown($$) { my ($hash, $arg) = @_; # clean up directories and files created ESPEInk_Cleanup($hash); RemoveInternalTimer ($hash); BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); Log3 $hash, 5, "Prepared shutown for ".$hash->{NAME}; return undef; } #--------------------------------------------------------------------------------------------------- # remove device sub ESPEInk_Undef($$) { my ($hash, $arg) = @_; # clean up directories and files created ESPEInk_Cleanup($hash); RemoveInternalTimer ($hash); BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); Log3 $hash, 5, "Removed device for ".$hash->{NAME}; return undef; } #--------------------------------------------------------------------------------------------------- # Get function, currently only display of supported devices is implemented sub ESPEInk_Get($@) { my ($hash, @param) = @_; return '"get ESPEInk" needs at least one argument' if (int(@param) < 2); my $name = shift @param; my $opt = shift @param; if(!$ESPEInk_gets{$opt}) { return "Unknown argument $opt, choose one of " . join(" ", map{ "$_".$ESPEInk_gets{$_}} keys %ESPEInk_gets); } if ($opt eq 'devices') { my $result = ""; $result .= "Name Width Height\n"; $result .= "----------------------------------------------\n"; foreach my $name (sort keys %ESPEInk_devices) { my $fill = ' ' x (30-length($name)); $result .= $name.$fill." ".$ESPEInk_devices{$name}{"width"}." ".$ESPEInk_devices{$name}{"height"}."\n"; } return $result; } return $ESPEInk_gets{$opt}; } #--------------------------------------------------------------------------------------------------- # Set function, convert/upload/addText if text is existing also removeText sub ESPEInk_Set($@) { my ($hash, @param) = @_; return '"set ESPEInk" needs at least one argument' if (int(@param) < 2); my $name = shift @param; my $opt = shift @param; my $value = join(" ", @param); my $itext; if(!defined($ESPEInk_sets{$opt})) { return "Unknown argument $opt, choose one of " . join(" ", map{ "$_:".$ESPEInk_sets{$_}} keys %ESPEInk_sets); } if ($opt eq 'addtext' || $opt eq 'textreading' || $opt eq 'addicon' || $opt eq 'iconreading' || $opt eq 'addsymbol') { my ($text, $x, $y, $size, $angle, $color, $font, $linegap, $blockwidth) = split("#",$value); return "No text defined, use format: 'addtext text#x#y#size#angle#color#font'" if (!$text && ($opt eq 'addtext')); return "No reading defined, use format: 'textreading device:reading#x#y#size#angle#color#font'" if (!$text && ($opt eq 'textreading')); return "No icon defined, use format: 'addicon icon#x#y#size#angle#color'" if (!$text && ($opt eq 'addicon')); return "No reading defined, use format: 'iconreading device:reading#x#y#size#angle#color'" if (!$text && ($opt eq 'iconreading')); return "No symbol defined, use format: 'addsymbol symbol#x#y#size#angle#color#width#height#arc'" if (!$text && ($opt eq 'addsymbol')); my($texts,$eval) = split("{",$text); my ($device,$reading) = split(':',$texts); $reading = "state" if (!$reading); readingsBeginUpdate($hash); $itext = ReadingsVal($name,"deftexts",0)+1; readingsBulkUpdate($hash,"deftexts",$itext); if ($opt eq 'textreading' or $opt eq 'iconreading') { readingsBulkUpdate($hash,$itext."-trigger",$text); $text = ReadingsVal($device,$reading,''); if ($eval) { $eval =~ s/\}//g; $text = sprintf($eval,$text); } readingsBulkUpdate($hash,$itext."-text",$text) if ($opt eq 'textreading'); ESPEInk_ResetNotifies({hash=>$hash}); } if ($opt eq 'iconreading' || $opt eq 'addicon') { readingsBulkUpdate($hash,$itext."-icon",$text) if ($opt eq 'iconreading'); readingsBulkUpdate($hash,$itext."-isIcon",1); readingsBulkUpdate($hash,$itext."-isSymbol",0); } elsif ($opt eq 'textreading' || $opt eq 'addtext' || $opt eq 'addsymbol') { readingsBulkUpdate($hash,$itext."-isIcon",0); if ($opt eq 'addsymbol') { readingsBulkUpdate($hash,$itext."-isSymbol",1); } else { readingsBulkUpdate($hash,$itext."-isSymbol",0); } } $x = 0 if (!$x || (($x !~ '-?\d+')&&($x !~ '(left|mid|right)'))); $y = 0 if (!$y || (($y !~ '-?\d+')&&($y !~ '(top|mid|bottom)'))); $size = 10 if (!$size || ($size !~ '\d+')); $angle = 0 if (!$angle || ($angle !~ '-?\d+')); $color = "000000" if (!$color || !ESPEInk_CheckColorString($color)); $font = "medium" if (!ESPEInk_CheckFontString($font) and ($opt ne 'addsymbol')); $linegap = 0 if (!$linegap || ($linegap !~ '\d+')); $blockwidth = 0 if (!$blockwidth || ($blockwidth !~ '\d+')); readingsBulkUpdate($hash,$itext."-def",$opt."#".$value); readingsBulkUpdate($hash,$itext."-text",$text) if ($opt eq 'addtext'); readingsBulkUpdate($hash,$itext."-symbol",$text) if ($opt eq 'addsymbol'); readingsBulkUpdate($hash,$itext."-icon",$text) if ($opt eq 'addicon'); readingsBulkUpdate($hash,$itext."-x",$x); readingsBulkUpdate($hash,$itext."-y",$y); readingsBulkUpdate($hash,$itext."-size",$size); readingsBulkUpdate($hash,$itext."-angle",$angle); readingsBulkUpdate($hash,$itext."-color",$color); readingsBulkUpdate($hash,$itext."-font",$font) if (($opt eq 'addtext' || $opt eq 'textreading') and $opt ne 'addsymbol'); readingsBulkUpdate($hash,$itext."-linegap",$linegap) if ($opt ne 'addsymbol');; readingsBulkUpdate($hash,$itext."-blockwidth",$blockwidth) if ($opt ne 'addsymbol');; readingsBulkUpdate($hash,$itext."-width",$font) if ($opt eq 'addsymbol'); # parameter number 7 is width instead of font for symbols readingsBulkUpdate($hash,$itext."-height",$linegap) if ($opt eq 'addsymbol'); # parameter number 8 is height instead of linegap for symbols readingsBulkUpdate($hash,$itext."-arc",$blockwidth) if ($opt eq 'addsymbol'); # parameter number 9 is height instead of linegap for symbols ESPEInk_AddTextAttributes($name,$itext); delete $ESPEInk_sets{'removeobject'}; my $list = ""; for (my $i=1; $i<=ReadingsVal($name,"deftexts",0); $i++) { $list .= ",".$i; } $ESPEInk_sets{'removeobject'} = "multiple-strict".$list if ($list ne ""); readingsEndUpdate($hash,1); $hash->{STATE} = "Added text $text at ($x,$y) for display"; return undef; } if ($opt eq 'removeobject') { my @args = split(",",$value); foreach (reverse sort @args) { ESPEInk_RemoveTextReadings($hash,$_) } delete $ESPEInk_sets{'removeobject'}; my $list = ""; for (my $i=1; $i<=ReadingsVal($name,"deftexts",0); $i++) { $list .= ",".$i; } $ESPEInk_sets{'removeobject'} = "multiple-strict".$list if ($list ne "");; ESPEInk_ResetNotifies({hash=>$hash}); $hash->{STATE} = "Removed ".@args." texts from display"; return undef; } if ($opt eq 'upload') { $hash->{STATE} = ESPEInk_Upload($hash); return undef; } if ($opt eq 'convert') { $hash->{STATE} = ESPEInk_Convert($hash,0); return undef; } } #--------------------------------------------------------------------------------------------------- # Attr function, check attribute setting, conversion to readings and error handling sub ESPEInk_FetchReadings(@) { my @values = @_; my @outvalues; my $outvalue; foreach my $value (@values) { if ($value =~ m/\[/) { $value =~ s/[\[\]]//g; my ($dev,$reading) = split(":",$value); $value = ReadingsVal($dev,$reading,''); } push(@outvalues,$value); $outvalue = $value; } if ((scalar @outvalues) > 1) { return @outvalues; } else { return $outvalue; } } #--------------------------------------------------------------------------------------------------- # Attr function, check attribute setting, conversion to readings and error handling sub ESPEInk_Attr(@) { my ($cmd,$name,$attr_name,$attr_value) = @_; my $err; my $hash = $defs{$name}; if($cmd eq "set") { if($attr_name eq "interval") { if(!looks_like_number($attr_value)) { $err = "Invalid argument $attr_value to $attr_name. Must be a number."; return $err; } $hash->{INTERVAL} = $attr_value; RemoveInternalTimer ($hash); InternalTimer(gettimeofday()+$attr_value, "ESPEInk_FullUpdate", $hash, 0) if ($attr_value > 0); } elsif($attr_name eq "url") { $hash->{URL} = $attr_value; } elsif($attr_name eq "picturefile") { $err = ESPEInk_UpdatePicture($hash,$attr_value); return $err if (defined $err); $hash->{PICTUREFILE} = $attr_value; } elsif($attr_name eq "devicetype") { my $colormode = ESPEInk_GetSetting($name,"colormode"); my $ipal = $ESPEInk_devices{$attr_value}{"pindex"}; $hash->{DEVICETYPE} = $attr_value; if (($colormode eq "color") && ($ipal==0 or $ipal==4) && $ESPEInk_InitializationDone) { $err = "Inconsistent setting for device type $attr_value. Device does not support color mode, setting colormode attribute to monochrome."; fhem("attr $name colormode monochrome"); fhem("attr $name devicetype $attr_value"); return $err; } } elsif($attr_name eq "boardtype") { $hash->{BOARDTYPE} = $attr_value; } elsif($attr_name eq "colormode") { my $devtype = ESPEInk_GetSetting($name,"devicetype"); my $ipal = $ESPEInk_devices{$devtype}{"pindex"}; if (($attr_value eq "color") && ($ipal==0 or $ipal==4) && $ESPEInk_InitializationDone) { $err = "Invalid argument $attr_value to $attr_name. Device does not support color mode."; return $err; } $hash->{COLORMODE} = $attr_value; } elsif($attr_name =~ '^\d+-.*') { if ($attr_name =~ '(x$|y$|size$)') { return "Invalid argument $attr_value to $attr_name must be an interger number" if ($attr_value !~ '-?\d+' && $attr_value !~ '^\[.*'); } elsif ($attr_name =~ '(size)') { return "Invalid argument $attr_value to $attr_name must be a positive interger number" if ($attr_value !~ '\d+' && $attr_value !~ '^\[.*'); } elsif ($attr_name =~ '(angle)') { return "Invalid argument $attr_value to $attr_name must be an integer number between -180 and +180" if (($attr_value !~ '-?\d+' && $attr_value !~ '^\[.*') || int($attr_value) < -180 || int($attr_value) > 180); } elsif ($attr_name =~ '(color)') { return "Invalid argument $attr_value to $attr_name must be a valid rgb hex string" if (!ESPEInk_CheckColorString($attr_value)); } elsif ($attr_name =~ '(font)') { return "Invalid argument $attr_value to $attr_name either one of small/medium/large/giant or a path to a valid TTF file" if (!ESPEInk_CheckFontString($attr_value)); } elsif ($attr_name =~ '(icon)') { my ($ret,$path) = ESPEInk_CheckIconString($attr_value); return "Invalid argument $attr_value to $attr_name must be a valid icon" if (!$ret); } elsif ($attr_name =~ '(trigger)') { readingsSingleUpdate( $hash,$attr_name,$attr_value,1); ESPEInk_ResetNotifies({hash=>$hash}); } readingsSingleUpdate( $hash,$attr_name,$attr_value,1); } elsif($attr_name eq "convertmode") { $hash->{CONVERTMODE} = $attr_value; } elsif($attr_name eq "width") { } elsif($attr_name eq "height") { } elsif($attr_name eq "x0") { } elsif($attr_name eq "y0") { } elsif($attr_name eq "placement") { } elsif($attr_name eq "scale2fit") { } elsif($attr_name eq "coloroffset") { } elsif($attr_name eq "maxretries") { } elsif($attr_name eq "timeout") { } elsif($attr_name eq "mininterval") { } elsif($attr_name eq "uploadTimeout") { } elsif($attr_name eq "definition") { ESPEInk_ResetNotifies({hash=>$hash,definition=>$attr_value}); } elsif($attr_name eq "definitionFile") { ESPEInk_ResetNotifies({hash=>$hash,definitionfile=>$attr_value}); } elsif(IsDisabled($name) && $attr_value eq "1") { Log3 $hash, 5, "$name: disable attribute set, stop timer"; RemoveInternalTimer ($hash); } elsif(IsDisabled($name) && $attr_value eq "0") { Log3 $hash, 5, "$name: disable attribute unset, restart timer"; RemoveInternalTimer ($hash); InternalTimer(gettimeofday()+ESPEInk_GetSetting($name,"interval"), "ESPEInk_FullUpdate", $hash, 0) if (ESPEInk_GetSetting($name,"interval") > 0); } } if($cmd eq "del") { if(IsDisabled($name)) { Log3 $hash, 5, "$name: disable attribute removed, restart timer"; RemoveInternalTimer ($hash); InternalTimer(gettimeofday()+ESPEInk_GetSetting($name,"interval"), "ESPEInk_FullUpdate", $hash, 0) if (ESPEInk_GetSetting($name,"interval") > 0); } elsif($attr_name =~ '.*-trigger') { my ($ind,$cmd) = split("-",$attr_name); my ($type,$trigger) = split("#",ReadingsVal($name,$ind."-def","")); readingsDelete($hash,"$ind-trigger") if ($type eq 'addtext' || $type eq 'addicon' || $type eq 'addsymbol'); readingsSingleUpdate( $hash, "$ind-icon", $trigger, 1 ) if ($trigger && ReadingsVal($name,"$ind-isIcon",0)); readingsSingleUpdate( $hash, "$ind-text", $trigger, 1 ) if ($trigger && !ReadingsVal($name,"$ind-isIcon",0) && $type eq 'addtext'); readingsSingleUpdate( $hash, "$ind-sybmol", $trigger, 1 ) if ($trigger && !ReadingsVal($name,"$ind-isIcon",0) && $type eq 'addsymbol'); Log3 $hash, 5, "$name: deleted attribute $attr_name for triggering reset to initial definition"; } elsif($attr_name =~ '^\d+-.*') { my ($ind,$cmd) = split("-",$attr_name); my ($type, $text, $x, $y, $size, $angle, $color, $font, $linegap, $blockwidth) = split("#",ReadingsVal($name,$ind."-def","")); Log3 $hash, 5, "$name: $ind, $cmd, $text, $x, $y, $size, $angle, $color, $font, $linegap, $blockwidth"; if ($cmd =~ "text") { readingsSingleUpdate( $hash, "$ind-text", $text, 1 ) if ($text); } elsif ($cmd =~ "sybmol") { readingsSingleUpdate( $hash, "$ind-symbol", $text, 1 ) if ($text); } elsif ($cmd =~ "icon") { readingsSingleUpdate( $hash, "$ind-icon", $text, 1 ) if ($text); } elsif ($cmd =~ "x") { readingsSingleUpdate( $hash, "$ind-x", $x, 1 ) if ($x); readingsSingleUpdate( $hash, "$ind-x", 0, 1 ) if (!$x); } elsif ($cmd =~ "y") { readingsSingleUpdate( $hash, "$ind-y", $y, 1 ) if ($y); readingsSingleUpdate( $hash, "$ind-y", 0, 1 ) if (!$y); } elsif ($cmd =~ "size") { readingsSingleUpdate( $hash, "$ind-size", $size, 1 ) if ($size); readingsSingleUpdate( $hash, "$ind-size", 10, 1 ) if (!$size); } elsif ($cmd =~ "angle") { readingsSingleUpdate( $hash, "$ind-angle", $angle, 1 ) if ($angle); readingsSingleUpdate( $hash, "$ind-angle", 0, 1 ) if (!$angle); } elsif ($cmd =~ "color") { readingsSingleUpdate( $hash, "$ind-color", $color, 1 ) if ($color); readingsSingleUpdate( $hash, "$ind-color", "000000", 1 ) if (!$color); } elsif ($cmd =~ "font" && !ReadingsVal($name,"$ind-isIcon",0)) { readingsSingleUpdate( $hash, "$ind-font", $font, 1 ) if ($font); readingsSingleUpdate( $hash, "$ind-font", "medium", 1 ) if (!$font); } elsif ($cmd =~ "linegap" && !ReadingsVal($name,"$ind-isIcon",0)) { readingsSingleUpdate( $hash, "$ind-linegap", $linegap, 1 ) if ($linegap); readingsSingleUpdate( $hash, "$ind-linegap", 0, 1 ) if (!$linegap); } elsif ($cmd =~ "blockwidth" && !ReadingsVal($name,"$ind-isIcon",0)) { readingsSingleUpdate( $hash, "$ind-blockwidth", $blockwidth, 1 ) if ($blockwidth); readingsSingleUpdate( $hash, "$ind-blockwidth", 0, 1 ) if (!$blockwidth); } elsif ($cmd =~ "width" && !ReadingsVal($name,"$ind-isIcon",0)) { readingsSingleUpdate( $hash, "$ind-width", $font, 1 ) if ($font); readingsSingleUpdate( $hash, "$ind-width", 0, 1 ) if (!$font); } elsif ($cmd =~ "height" && !ReadingsVal($name,"$ind-isIcon",0)) { readingsSingleUpdate( $hash, "$ind-height", $linegap, 1 ) if ($linegap); readingsSingleUpdate( $hash, "$ind-height", 0, 1 ) if (!$linegap); } elsif ($cmd =~ "arc" && !ReadingsVal($name,"$ind-isIcon",0)) { readingsSingleUpdate( $hash, "$ind-arc", $blockwidth, 1 ) if ($blockwidth); readingsSingleUpdate( $hash, "$ind-arc", 0, 1 ) if (!$blockwidth); } Log3 $hash, 5, "$name: deleted attribute $attr_name reset to default if in initial definition (set ".$name." ".$type.")"; } } return undef; } #--------------------------------------------------------------------------------------------------- # Notify for INITIALIZED and check settings (e.g. when reload is called) sub ESPEInk_Notify($$) { my ($own, $source) = @_; my $name = $own->{NAME}; my $hash = $defs{$name}; my $sname = $source->{NAME}; my $doupdate = 0; Log3 $own, 5, "$name: Event received from device $sname (events are: ".join("; ",@{deviceEvents($source, 1)}).")"; if (grep(m/^INITIALIZED|REREADCFG$/, @{$source->{CHANGED}})) { Log3 $own, 5, "Making sure that settings are correct when initializing or rereading config"; $ESPEInk_InitializationDone = 1; ESPEInk_ChecksWhenInitialized(); } return if (IsDisabled($name)); my $events = deviceEvents($source, 1); return if(!$events); my ($reading,$device,$sreading,$value); foreach my $event (@{$events}) { $event = "" if(!defined($event)); ($sreading,$value) = split(':',$event,2); my $definition = AttrVal($name,"definition",undef); my $definitionFile = AttrVal($name,"definitionFile",undef); if ($definitionFile) { my ($error, @content) = FileRead({FileName=>$definitionFile, ForceType=>"file"}); if (!$error) { $definition .= "\n" . (join("\n", @content)); } else { Log3 $own, 1, "Error ($error) reading definition from file $definitionFile"; } } if ($definition) { # work on all definitions if definition attribute is defined foreach my $line (split(/\n/,$definition)) { # go through the definition line by line next if ($line =~ /^\s*\#.*/); # check for comment lines my ($type, $text, $x, $y, $size, $ang, $col, $fnt) = split("#",$line); my $eval=0; ($text,$eval) = split('{',$text); ($device,$reading) = split(':',$text,2); $reading = "state" if (!$reading); if (($device) && ($device eq $sname) && ($reading eq $sreading)) { if (ESPEInk_GetSetting($name,"interval") == 0) { $doupdate = 1; # there has been an update to one of the readings in the definition, do update if interval is 0 } } } } for (my $i = 1; $i <= ReadingsVal($name,"deftexts",0); $i++) { my ($text,$eval) = split('{',ReadingsVal($name,$i."-trigger",""),2); ($device,$reading) = split(':',$text,2); $reading = "state" if (!$reading); ESPEInk_ResetNotifies({hash=>$own}) if ($name eq $sname && $sreading =~ '.*trigger'); # make sure that changes in trigger setting are reflected in NOTIFYDEV if ($name eq $sname && $reading =~ 'interval') { # take into account changes in interval settings } if (($device) && ($device eq $sname) && ($reading eq $sreading)) { if ($eval) { $eval =~ s/\}//g; $value = sprintf($eval,$value); } Log3 $own, 5, "Setting new text or icon to $value for device $name"; readingsSingleUpdate( $hash,$i."-text",$value,1) if (!ReadingsVal($name,"$i-isIcon",0)); if (ReadingsVal($name,"$i-isIcon",0)) { my $dsi = AttrVal($name,"devStateIcon",undef); if ($dsi) { my @list = split(" ", $dsi); foreach my $l (@list) { my ($re, $iconName, $link) = split(":", $l, 3); if(defined($re) && $sreading =~ m/^$re$/) { $value = $iconName; } } } readingsSingleUpdate( $hash,$i."-icon",$value,1); } if (ESPEInk_GetSetting($name,"interval") == 0) { $doupdate = 1; } } } } ESPEInk_FullUpdate($own) if ($doupdate); return; } #--------------------------------------------------------------------------------------------------- # function for doing a conversion and upload with the latest settings for the device (called when interval is > 0 or after trigger from external reading) sub ESPEInk_FullUpdate($){ my ($hash) = @_; my $name = $hash->{NAME}; ESPEInk_Convert($hash, 1); RemoveInternalTimer ($hash); InternalTimer(gettimeofday()+ESPEInk_GetSetting($name,"interval"), "ESPEInk_FullUpdate", $hash, 0) if (ESPEInk_GetSetting($name,"interval") > 0); } #--------------------------------------------------------------------------------------------------- # Check color setting for validity sub ESPEInk_ResetNotifies($) { my ($args) = @_; my $hash = $args->{hash}; my $valdef = $args->{definition}; my $valdeffile = $args->{definitionfile}; my $name = $hash->{NAME}; my ($device,$reading); my $notifies; $notifies = ""; my $definition = (defined($valdef))?$valdef:AttrVal($name,"definition",undef); my $definitionFile = (defined($valdeffile))?$valdeffile:AttrVal($name,"definitionFile",undef); if ($definitionFile) { my ($error, @content) = FileRead({FileName=>$definitionFile, ForceType=>"file"}); if (!$error) { $definition .= "\n" . (join("\n", @content)); } else { Log3 $hash, 1, "Error ($error) reading definition from file $definitionFile"; } } if ($definition) { # work on all definitions if definition attribute is defined foreach my $line (split(/\n/,$definition)) { # go through the definition line by line next if ($line =~ /^\s*\#.*/); # check for comment lines my ($type, $text, $x, $y, $size, $ang, $col, $fnt) = split("#",$line); if ($type eq "iconreading" || $type eq "textreading") { my $eval; ($text,$eval) = split("{",$text); ($device,$reading) = split(':',$text); next if (($type eq 'iconreading') && (length($text) == 0)); # nothing to do, just skip - Hajo 2 if ($device) { $reading = "state" if (!$reading); $notifies .= $device.":".$reading."|" if ($defs{$device}); $reading = undef; } } } } for (my $i = 1; $i <= ReadingsVal($name,"deftexts",0); $i++) { my ($text,$eval) = split("{",ReadingsVal($name,$i."-trigger","")); ($device,$reading) = split(':',$text); if ($device) { $reading = "state" if (!$reading); $notifies .= $device.":".$reading."|" if ($defs{$device}); $reading = undef; } } $notifies .= $hash->{NAME}.":.*-trigger.*"; # make sure that changes in trigger are going to NotifyFn $notifies .= '|global'; notifyRegexpChanged($hash, $notifies); Log3 $hash, 5, "Notify definition: ".($hash->{NOTIFYDEV}?$hash->{NOTIFYDEV}:"")." from $notifies"; } #--------------------------------------------------------------------------------------------------- # cleanup sub ESPEInk_Cleanup($) { my ($hash) = @_; # clean up directories and files created my $name = $hash->{NAME}; my $fname = basename(ESPEInk_GetSetting($name,"picturefile")); my $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root unlink $rootname."/".$hash->{SUBFOLDER}."/".$name."/".$fname; # remove local copy of input file in directory with device name unlink $rootname."/".$hash->{SUBFOLDER}."/".$name."/tmp_png.png"; # remove local copy of eventually existing temporary file in directory with device name unlink $rootname."/".$hash->{SUBFOLDER}."/".$name."/result.png"; # remove local copy of conversion result file in directory with device name rmdir $rootname."/".$hash->{SUBFOLDER}."/".$name; } #--------------------------------------------------------------------------------------------------- # Check color setting for validity sub ESPEInk_CheckColorString($) { my ($color) = @_; return ($color =~ '^(?:[0-9a-fA-F]{3}){1,2}$') || ($color =~ '^\['); } #--------------------------------------------------------------------------------------------------- # Get width and height of painted string sub ESPEInk_GetStringPixelWidth($$$$$) { my ($name,$string,$font,$angle,$size) = @_; my $cw = 0; my $ch = 0; my $slength = length($string); my $wpixels = 0; my $hpixels = 0; ($cw,$ch) = (6,12) if ($font eq 'small'); ($cw,$ch) = (7,13) if ($font eq 'medium'); ($cw,$ch) = (8,16) if ($font eq 'large'); ($cw,$ch) = (9,15) if ($font eq 'giant'); if ($cw == 0) { # no standard font, use font from path my $dw = AttrVal($name,"width",$ESPEInk_devices{ESPEInk_GetSetting($name,"devicetype")}{"width"}); my $dh = AttrVal($name,"height",$ESPEInk_devices{ESPEInk_GetSetting($name,"devicetype")}{"height"}); my $image = GD::Image->new($dw,$dh,1); my @bounds = GD::Image->stringFT($image->colorAllocate(0,0,0),$font,$size,$angle/180*(4*atan2(1,1)),0,$size,$string); $wpixels = $bounds[4] - $bounds[0] - 1.5; $hpixels = $bounds[1] - $bounds[5]; } else { $wpixels = $cw*$slength*cos($angle)+$ch*sin($angle); $hpixels = $ch*cos($angle)+$cw*$slength*sin($angle); } return ($wpixels,$hpixels); } #--------------------------------------------------------------------------------------------------- # Helper function to correct x and y according to setting (left/right/mid) sub ESPEInk_CorrectXY($$$$$$$$$$) { my ($name,$type,$text,$font,$angle,$size,$w,$h,$xo,$yo) = @_; my ($wt,$ht); my ($x,$y); ($wt,$ht) = ESPEInk_GetStringPixelWidth($name,$text,$font,$angle,$size) if ($type =~ 'text'); ($wt,$ht) = split("#",$text) if ($type =~ 'icon'); my $xn = $xo; $xn =~ s/^(\<|\||\>)//g; $xn =~ s/^(left|mid|right)//g; $xn = 0 if (!$xn); my $yn = $yo; $yn =~ s/^(\<|\||\>)//g; $yn =~ s/^(top|mid|bottom)//g; $yn = 0 if (!$yn); $xn = 0 if (!$xn || ($xn !~ '-?\d+')); $yn = 0 if (!$yn || ($yn !~ '-?\d+')); $x = $xn; $x += ($w - $wt) if ($xo =~ 'right'); $x += ($w/2 - $wt/2) - $xn if ($xo =~ 'mid'); $x -= $wt if ($xo =~ '\>'); $x -= $wt/2 if ($xo =~ '\|'); $y = $yn; $y += ($h - $ht) if ($yo =~ 'bottom'); $y += ($h/2 - $ht/2) if ($yo =~ 'mid'); $y -= $ht if ($yo =~ '\>'); $y -= $ht/2 if ($yo =~ '\|'); return ($x,$y); } #--------------------------------------------------------------------------------------------------- # Check font setting for validity sub ESPEInk_CheckFontString($) { my ($font) = @_; my $ret = 0; $ret = 1 if ($font =~ '^\['); # reading:value format $ret = 1 if ($font && $font =~ '(small|medium|large|giant)'); $ret = ($font && -e $font) if (!$ret); return $ret; } #--------------------------------------------------------------------------------------------------- # Check icon setting for validity sub ESPEInk_CheckIconString($) { my ($icon) = @_; my $ret = 0; my $picdata = undef; if ($icon =~ '^\[') { return($ret,$icon) if ($ret); } if ($icon =~ /http(?:s)\:\/\//) { # Web link, check if it can be downloaded ($ret, $picdata) = HttpUtils_BlockingGet({url=>$icon,timeout=>30}); $ret = defined $picdata; return($ret,$icon) if ($ret); } my $modpath = AttrVal("global","modpath","."); my $iconpath = AttrVal("WEB","iconPath","default:fhemSVG:fontawesome:openautomation"); my @iconpaths = split(":",$iconpath); no warnings 'File::Find'; foreach my $path (@iconpaths) { #$path .= "/regular" if ($path eq "fontawesome" && -d $modpath."/"."www/images/".$path."/regular"); foreach my $extension ("gif","jpg","png","svg") { my $foundfile = undef; my $search = ".*".$icon.".".$extension; find(sub {$foundfile = $File::Find::name if ($_ =~ /$search/);}, $modpath."/"."www/images/".$path); $ret = ($icon && defined $foundfile); return ($ret,$foundfile) if ($ret); } } return ($ret,"./www/images/default/fhemicon.png"); # nothing found return default fhem icon. } #--------------------------------------------------------------------------------------------------- # Add all new attributes (text, position, size, angle, color, font) for a text object sub ESPEInk_AddTextAttributes($$) { my ($name,$itext) = @_; addToDevAttrList($name, "$itext-text") if (!ReadingsVal($name,"$itext-isIcon",0) and !ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-symbol") if (!ReadingsVal($name,"$itext-isIcon",0) and ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-icon") if (ReadingsVal($name,"$itext-isIcon",0)); addToDevAttrList($name, "$itext-trigger"); addToDevAttrList($name, "$itext-x"); addToDevAttrList($name, "$itext-y"); addToDevAttrList($name, "$itext-size"); addToDevAttrList($name, "$itext-angle"); addToDevAttrList($name, "$itext-color:colorpicker,RGB"); addToDevAttrList($name, "$itext-font") if (!ReadingsVal($name,"$itext-isIcon",0) && !ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-linegap") if (!ReadingsVal($name,"$itext-isIcon",0) && !ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-blockwidth") if (!ReadingsVal($name,"$itext-isIcon",0) && !ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-width") if (!ReadingsVal($name,"$itext-isIcon",0) && ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-height") if (!ReadingsVal($name,"$itext-isIcon",0) && ReadingsVal($name,"$itext-isSymbol",0)); addToDevAttrList($name, "$itext-arc") if (!ReadingsVal($name,"$itext-isIcon",0) && ReadingsVal($name,"$itext-isSymbol",0)); } #--------------------------------------------------------------------------------------------------- # Check setting consistency if initialization is done sub ESPEInk_ChecksWhenInitialized() { my @ESPEInkModules = devspec2array("TYPE=ESPEInk"); foreach (@ESPEInkModules) { my ($hash) = $defs{$_}; my $name = $hash->{NAME}; my $tcount = 0; foreach my $reading ( keys %{$hash->{READINGS}}){ if ($reading =~ '^\d+-text*' || $reading =~ '^\d+-icon*') { # there seems to be a text defined my $itext = $reading =~ /^\d+/g; ESPEInk_AddTextAttributes($name,$itext); $tcount++; }; if ($reading =~ '^\d+-trigger*') { # there seems to be a triger for the text or icon defined }; } Log3 $hash, 5, "found $tcount text/icon objects in module $name"; readingsSingleUpdate( $hash,"deftexts",$tcount,1); if ($tcount > 0) { delete $ESPEInk_sets{'removeobject'}; my $list = ""; for (my $i=1; $i<=$tcount; $i++) { $list .= ",".$i; } $ESPEInk_sets{'removeobject'} = "multiple-strict".$list if ($list ne ""); } ESPEInk_ResetNotifies({hash=>$hash}); RemoveInternalTimer ($hash); InternalTimer(gettimeofday()+ESPEInk_GetSetting($name,"interval"), "ESPEInk_FullUpdate", $hash, 0) if (ESPEInk_GetSetting($name,"interval") > 0); } } #--------------------------------------------------------------------------------------------------- # allocate the colors defined for the selected EInk display to color table of destination image sub ESPEInk_AllocateColors($$$){ my ($hash,$image,$pi) = @_; my $length = $ESPEInk_palettes[$pi][0]; my $index; Log3 $hash, 5, "Deallocate ".$image->colorsTotal." color entries"; for (my $i=0;$i<$image->colorsTotal;$i++) # remove all previously existing color entries of the image { $image->colorDeallocate($i) } for (my $i=1;$i<=$length;$i++) # allocate new colors according to EInk display capabilities. { $index = $image->colorAllocate($ESPEInk_palettes[$pi][$i][0],$ESPEInk_palettes[$pi][$i][1],$ESPEInk_palettes[$pi][$i][2]); Log3 $hash, 5, "Allocating color values (".$ESPEInk_palettes[$pi][$i][0].",".$ESPEInk_palettes[$pi][$i][1].",".$ESPEInk_palettes[$pi][$i][2].") to index $index"; } } #--------------------------------------------------------------------------------------------------- # Remove all readings related to a text object when removeobject setting is triggered sub ESPEInk_RemoveTextReadings($$){ my ($hash,$itext) = @_; my $name = $hash->{NAME}; my $deftexts = ReadingsVal($name,"deftexts",0); if ($itext <= $deftexts) { my $isicon = ReadingsVal($name,"$itext-isIcon",0); my $issymbol = ReadingsVal($name,"$itext-isSymbol",0); readingsDelete($hash,$itext."-def"); readingsDelete($hash,$itext."-text"); readingsDelete($hash,$itext."-symbol"); readingsDelete($hash,$itext."-icon"); readingsDelete($hash,$itext."-trigger"); readingsDelete($hash,$itext."-x"); readingsDelete($hash,$itext."-y"); readingsDelete($hash,$itext."-size"); readingsDelete($hash,$itext."-angle"); readingsDelete($hash,$itext."-color"); readingsDelete($hash,$itext."-font"); readingsDelete($hash,$itext."-linegap"); readingsDelete($hash,$itext."-blockwidth"); readingsDelete($hash,$itext."-isIcon"); readingsDelete($hash,$itext."-isSymbol"); readingsDelete($hash,$itext."-width"); readingsDelete($hash,$itext."-height"); readingsDelete($hash,$itext."-arc"); readingsBeginUpdate($hash); for (my $i=$itext; $i<$deftexts; $i++) { $isicon = ReadingsVal($name,"$i-isIcon",0) if ($i > $itext); $issymbol = ReadingsVal($name,"$i-isSymbol",0) if ($i > $itext); readingsBulkUpdate($hash,$i."-def",ReadingsVal($name,($i+1)."-def","")); readingsBulkUpdate($hash,$i."-text",ReadingsVal($name,($i+1)."-text","")) if (!ReadingsVal($name,($i+1)."-isIcon",0) and !ReadingsVal($name,($i+1)."-isSymbol",0)); readingsBulkUpdate($hash,$i."-sybmol",ReadingsVal($name,($i+1)."-symbol","")) if (!ReadingsVal($name,($i+1)."-isIcon",0) and ReadingsVal($name,($i+1)."-isSymbol",0)); readingsBulkUpdate($hash,$i."-icon",ReadingsVal($name,($i+1)."-icon","")) if (ReadingsVal($name,($i+1)."-isIcon",0)); readingsBulkUpdate($hash,$i."-trigger",ReadingsVal($name,($i+1)."-trigger","")) if (ReadingsVal($name,($i+1)."-trigger",undef)); readingsBulkUpdate($hash,$i."-x",ReadingsVal($name,($i+1)."-x","")); readingsBulkUpdate($hash,$i."-y",ReadingsVal($name,($i+1)."-y","")); readingsBulkUpdate($hash,$i."-size",ReadingsVal($name,($i+1)."-size","")); readingsBulkUpdate($hash,$i."-angle",ReadingsVal($name,($i+1)."-angle","")); readingsBulkUpdate($hash,$i."-color",ReadingsVal($name,($i+1)."-color","")); readingsBulkUpdate($hash,$i."-font",ReadingsVal($name,($i+1)."-font","")) if (ReadingsVal($name,($i+1)."-font",undef)); readingsBulkUpdate($hash,$i."-linegap",ReadingsVal($name,($i+1)."-linegap","")) if (ReadingsVal($name,($i+1)."-linegap",undef)); readingsBulkUpdate($hash,$i."-blockwidth",ReadingsVal($name,($i+1)."-blockwidth","")) if (ReadingsVal($name,($i+1)."-blockwidth",undef)); readingsBulkUpdate($hash,$i."-width",ReadingsVal($name,($i+1)."-width","")) if (ReadingsVal($name,($i+1)."-width",undef)); readingsBulkUpdate($hash,$i."-height",ReadingsVal($name,($i+1)."-height","")) if (ReadingsVal($name,($i+1)."-height",undef)); readingsBulkUpdate($hash,$i."-arc",ReadingsVal($name,($i+1)."-arc","")) if (ReadingsVal($name,($i+1)."-arc",undef)); addToDevAttrList($name, "$i-font") if ($isicon && !ReadingsVal($name,($i+1)."-isIcon",0) && !ReadingsVal($name,($i+1)."-isSymbol",0)); delFromDevAttrList($name, "$i-font") if (ReadingsVal($name,($i+1)."-isIcon",0) && !ReadingsVal($name,($i+1)."-isSymbol",0)); addToDevAttrList($name, "$i-width") if ($isicon && !ReadingsVal($name,($i+1)."-isIcon",0) && ReadingsVal($name,($i+1)."-isSymbol",0)); delFromDevAttrList($name, "$i-width") if (ReadingsVal($name,($i+1)."-isIcon",0) && ReadingsVal($name,($i+1)."-isSymbol",0)); addToDevAttrList($name, "$i-height") if ($isicon && !ReadingsVal($name,($i+1)."-isIcon",0) && ReadingsVal($name,($i+1)."-isSymbol",0)); delFromDevAttrList($name, "$i-height") if (ReadingsVal($name,($i+1)."-isIcon",0) && ReadingsVal($name,($i+1)."-isSymbol",0)); addToDevAttrList($name, "$i-arc") if ($isicon && !ReadingsVal($name,($i+1)."-isIcon",0) && ReadingsVal($name,($i+1)."-isSymbol",0)); delFromDevAttrList($name, "$i-arc") if (ReadingsVal($name,($i+1)."-isIcon",0) && ReadingsVal($name,($i+1)."-isSymbol",0)); readingsBulkUpdate($hash,$i."-isIcon",ReadingsVal($name,($i+1)."-isIcon","")); readingsBulkUpdate($hash,$i."-isSymbol",ReadingsVal($name,($i+1)."-isSymbol","")); if (defined(AttrVal($name,($i+1).'-text',undef))) {fhem("attr $name $i-text ".AttrVal($name,($i+1)."-text",""))} else {delFromDevAttrList($name, "$i-text")}; if (defined(AttrVal($name,($i+1).'-sybmol',undef))) {fhem("attr $name $i-sybmol ".AttrVal($name,($i+1)."-sybmol",""))} else {delFromDevAttrList($name, "$i-sybmol")}; if (defined(AttrVal($name,($i+1).'-icon',undef))) {fhem("attr $name $i-icon ".AttrVal($name,($i+1)."-icon",""))} else {delFromDevAttrList($name, "$i-icon")}; if (defined(AttrVal($name,($i+1).'-trigger',undef))) {fhem("attr $name $i-trigger ".AttrVal($name,($i+1)."-trigger",""))} else {delFromDevAttrList($name, "$i-trigger")}; if (defined(AttrVal($name,($i+1).'-x',undef))) {fhem("attr $name $i-x ".AttrVal($name,($i+1)."-x",0))} else {delFromDevAttrList($name, "$i-x")}; if (defined(AttrVal($name,($i+1).'-y',undef))) {fhem("attr $name $i-y ".AttrVal($name,($i+1)."-y",0))} else {delFromDevAttrList($name, "$i-y")}; if (defined(AttrVal($name,($i+1).'-size',undef))) {fhem("attr $name $i-size ".AttrVal($name,($i+1)."-size",10))} else {delFromDevAttrList($name, "$i-size")}; if (defined(AttrVal($name,($i+1).'-angle',undef))) {fhem("attr $name $i-angle ".AttrVal($name,($i+1)."-angle",0))} else {delFromDevAttrList($name, "$i-angle")}; if (defined(AttrVal($name,($i+1).'-color',undef))) {fhem("attr $name $i-color ".AttrVal($name,($i+1)."-color","000000"))} else {delFromDevAttrList($name, "$i-color")}; if (defined(AttrVal($name,($i+1).'-font',undef))) {fhem("attr $name $i-font ".AttrVal($name,($i+1)."-font","medium"))} else {delFromDevAttrList($name, "$i-font")}; if (defined(AttrVal($name,($i+1).'-linegap',undef))) {fhem("attr $name $i-linegap ".AttrVal($name,($i+1)."-linegap",0))} else {delFromDevAttrList($name, "$i-linegap")}; if (defined(AttrVal($name,($i+1).'-blockwidth',undef))) {fhem("attr $name $i-blockwidth ".AttrVal($name,($i+1)."-blockwidth",0))} else {delFromDevAttrList($name, "$i-blockwidth")}; if (defined(AttrVal($name,($i+1).'-width',undef))) {fhem("attr $name $i-width ".AttrVal($name,($i+1)."-width",0))} else {delFromDevAttrList($name, "$i-width")}; if (defined(AttrVal($name,($i+1).'-height',undef))) {fhem("attr $name $i-height ".AttrVal($name,($i+1)."-height",0))} else {delFromDevAttrList($name, "$i-height")}; if (defined(AttrVal($name,($i+1).'-arc',undef))) {fhem("attr $name $i-arc ".AttrVal($name,($i+1)."-arc",0))} else {delFromDevAttrList($name, "$i-arc")}; } readingsBulkUpdate($hash,"deftexts",($deftexts-1)); readingsEndUpdate($hash,1); readingsDelete($hash,"$deftexts-def"); readingsDelete($hash,"$deftexts-text"); readingsDelete($hash,"$deftexts-symbol"); readingsDelete($hash,"$deftexts-icon"); readingsDelete($hash,"$deftexts-trigger"); readingsDelete($hash,"$deftexts-x"); readingsDelete($hash,"$deftexts-y"); readingsDelete($hash,"$deftexts-size"); readingsDelete($hash,"$deftexts-angle"); readingsDelete($hash,"$deftexts-color"); readingsDelete($hash,"$deftexts-font"); readingsDelete($hash,"$deftexts-linegap"); readingsDelete($hash,"$deftexts-blockwidth"); readingsDelete($hash,"$deftexts-isIcon"); readingsDelete($hash,"$deftexts-isSymbol"); readingsDelete($hash,"$deftexts-width"); readingsDelete($hash,"$deftexts-height"); readingsDelete($hash,"$deftexts-arc"); delFromDevAttrList($name, "$deftexts-text"); delFromDevAttrList($name, "$deftexts-symbol"); delFromDevAttrList($name, "$deftexts-icon"); delFromDevAttrList($name, "$deftexts-trigger"); delFromDevAttrList($name, "$deftexts-x"); delFromDevAttrList($name, "$deftexts-y"); delFromDevAttrList($name, "$deftexts-size"); delFromDevAttrList($name, "$deftexts-angle"); delFromDevAttrList($name, "$deftexts-color"); delFromDevAttrList($name, "$deftexts-font"); delFromDevAttrList($name, "$deftexts-linegap"); delFromDevAttrList($name, "$deftexts-blockwidth"); delFromDevAttrList($name, "$deftexts-width"); delFromDevAttrList($name, "$deftexts-height"); delFromDevAttrList($name, "$deftexts-arc"); Log3 $hash, 5, "$name: Removed readings for ".(ReadingsVal($name,"$deftexts-isIcon",0)?"icon":(ReadingsVal($name,"$deftexts-isSymbol",0)?"symbol":"text")); } } #--------------------------------------------------------------------------------------------------- # perform Floyd Steinberg dithering (if attribute dithering is set) sub ESPEInk_Dither($$) { my ($image,$pi) = @_; my $length = $ESPEInk_palettes[$pi][0]; my ($w,$h) = $image->getBounds; my $pixel; my $distance; my $indexOpt; my $newdist; my $error; my $thisline=0; my $nextline=1; my @dary; my @errors; for (my $ix=0; $ix<$w; $ix++) {$errors[$nextline][$ix][0]=0.0;$errors[$nextline][$ix][1]=0.0;$errors[$nextline][$ix][2]=0.0;} for (my $iy=0; $iy<$h; $iy++) { $thisline = ($thisline+1)%2; $nextline = ($nextline+1)%2; for (my $ix=0; $ix<$w; $ix++) {$errors[$nextline][$ix][0]=0.0;$errors[$nextline][$ix][1]=0.0;$errors[$nextline][$ix][2]=0.0;} for (my $ix=0; $ix<$w; $ix++) { my ($r, $g, $b) = $image->rgb($image->getPixel($ix,$iy)); $r += $errors[$thisline][$ix][0]; $g += $errors[$thisline][$ix][1]; $b += $errors[$thisline][$ix][2]; $distance = (abs($r-$ESPEInk_palettes[$pi][1][0]) + abs($g-$ESPEInk_palettes[$pi][1][1]) + abs($b-$ESPEInk_palettes[$pi][1][2]))/3; $indexOpt = 1; for (my $i=2; $i<=$length; $i++) { $newdist = (abs($r-$ESPEInk_palettes[$pi][$i][0]) + abs($g-$ESPEInk_palettes[$pi][$i][1]) + abs($b-$ESPEInk_palettes[$pi][$i][2]))/3; if ($newdist < $distance) { $distance = $newdist; $indexOpt = $i; } } $image->setPixel($ix,$iy,$image->colorClosest($ESPEInk_palettes[$pi][$indexOpt][0],$ESPEInk_palettes[$pi][$indexOpt][1],$ESPEInk_palettes[$pi][$indexOpt][2])); $dary[0] = $r - $errors[$thisline][$ix][0] - $ESPEInk_palettes[$pi][$indexOpt][0]; $dary[1] = $g - $errors[$thisline][$ix][1] - $ESPEInk_palettes[$pi][$indexOpt][1]; $dary[2] = $b - $errors[$thisline][$ix][2] - $ESPEInk_palettes[$pi][$indexOpt][2]; if ($ix == 0) { for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix][$i] = $errors[$nextline][$ix][$i] + ($dary[$i]*7.0)/16.0;} for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix+1][$i] = $errors[$nextline][$ix+1][$i] + ($dary[$i]*2.0)/16.0;} for (my $i=0; $i<3; $i++) {$errors[$thisline][$ix+1][$i] = $errors[$thisline][$ix+1][$i] + ($dary[$i]*7.0)/16.0;} } elsif ($ix == $w-1) { for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix-1][$i] = $errors[$nextline][$ix-1][$i] + ($dary[$i]*7.0)/16.0;} for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix][$i] = $errors[$nextline][$ix][$i] + ($dary[$i]*9.0)/16.0;} } else { for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix-1][$i] = $errors[$nextline][$ix-1][$i] + ($dary[$i]*3.0)/16.0;} for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix][$i] = $errors[$nextline][$ix][$i] + ($dary[$i]*5.0)/16.0;} for (my $i=0; $i<3; $i++) {$errors[$nextline][$ix+1][$i] = $errors[$nextline][$ix+1][$i] + ($dary[$i]*1.0)/16.0;} for (my $i=0; $i<3; $i++) {$errors[$thisline][$ix+1][$i] = $errors[$thisline][$ix+1][$i] + ($dary[$i]*7.0)/16.0;} } } } } #--------------------------------------------------------------------------------------------------- # update picture from file to have always the latest version on disk represented sub ESPEInk_UpdatePicture($$) { my ($hash,$filename) = @_; my $name = $hash->{NAME}; if (!open(IMAGE, $filename)) { my $err = "Invalid filename $filename. Must be a readable file."; return $err; } else { close IMAGE; my $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root unlink $rootname."/".$hash->{SUBFOLDER}."/".$name."/*.*"; # remove all existing files in directory with device name copy($filename,$rootname."/".$hash->{SUBFOLDER}."/".$name); # local copy of the file for usage in FHEMWEB (display) ESPEInk_MakePictureHTML($name,"source_picture",basename($filename)); return undef; } } #--------------------------------------------------------------------------------------------------- # generate html code to display picture as reading of device sub ESPEInk_MakePictureHTML($$$) { my ($devname, $reading, $filename) = @_; my $hash = $defs{$devname}; my $filedesc = $filename; $filedesc =~ s/\?.*$//g; readingsSingleUpdate( $hash,$reading,"{SUBFOLDER}."/".$devname."/".$filename."?dummy=".rand(1000000).">
/fhem/".$hash->{SUBFOLDER}."/".$devname."/".$filedesc."
",1); } sub ESPEInk_StoreFile($$$$) { my ($name, $fname, $extension, $image) = @_; my $rootname; my $outfilename; $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root $outfilename = catfile($rootname,$name,$extension.basename($fname)); open(IMAGE,">",$outfilename); binmode IMAGE; print IMAGE $image->png; close IMAGE; } #--------------------------------------------------------------------------------------------------- # helper function to check attributes and internal values for consistent setting of parameters sub ESPEInk_GetSetting($$) { my ($name, $setting) = @_; return defined(AttrVal($name,$setting,undef))?$attr{$name}{$setting}:InternalVal($name,uc $setting,undef); } #--------------------------------------------------------------------------------------------------- # Add a text object to the picture sub ESPEInk_FormatBlockText($$$$$$$) { my ($name,$txt,$fnt,$angle,$size,$blockwidth,$th) = @_; my $text = $txt; if ($blockwidth) { # insert additional \n if width of text reaches maximum width my @words; my $xoffset = 0; my ($wspc, $ww, $wh); my $newline = 1; ($wspc, $wh) = ESPEInk_GetStringPixelWidth($name," ",$fnt,$angle,$size); $text =~ s/\n/\\n/gm; # Debug "text :$text:"; @words = split(/[ ]/,$text); $text = ""; foreach my $word (@words) { my $ww2; my $wd; while ( $word =~ /(.*)\\n(.*)/ ) { $wd = $1; $word = $2; my ($ww2, $wh) = ESPEInk_GetStringPixelWidth($name,$wd,$fnt,$angle,$size); # Debug " offset :$xoffset: width :$ww2: spacew :$wspc: text :$wd: "; if (($xoffset+$ww2) <= $blockwidth) { $wd = (($newline)?"":" ") . $wd; } else { $wd = "\\n" . $wd; } $xoffset = 0; $newline = 1; $text = $text . $wd . "\\n"; } ($ww, $wh) = ESPEInk_GetStringPixelWidth($name,$word,$fnt,$angle,$size); # Debug " offset :$xoffset: width :$ww: spacew :$wspc: text :$word: "; Log3 $defs{$name}, 5, "--->> Width (outer) of $word (lengt ".length($word).") is: $ww, Width of Space is: $wspc, Text offset is: $xoffset"; if (($xoffset+$ww) <= $blockwidth) { $word = (($newline)?"":" ") . $word; $xoffset = $xoffset + $ww + $wspc; $newline = 0; } else { $word = "\\n" . $word . " "; # $word = $word . "\\n" if ($newline); $xoffset = $ww + $wspc; $newline = 1; } $text = $text . $word; } Log3 $defs{$name}, 5, "--->> Text is: $text"; } return $text; } #--------------------------------------------------------------------------------------------------- # Add a text object to the picture sub ESPEInk_AddObjects($$) { my ($name, $image) = @_; my ($r,$g,$b); my $color; my $font; my $deftexts = ReadingsVal($name,"deftexts",0); my $angle; my $icon_img = undef; my $rsvg = undef; my $hash = $defs{$name}; my $rootname; my $outfile; my $definition = AttrVal($name,"definition",undef); my $definitionFile = AttrVal($name,"definitionFile",undef); if ($definitionFile) { my ($error, @content) = FileRead({FileName=>$definitionFile, ForceType=>"file"}); if (!$error) { $definition .= "\n" . (join("\n", @content)); } else { Log3 $hash, 1, "Error ($error) reading definition from file $definitionFile"; } } if ($definition) { # work on all definitions if definition attribute is defined foreach my $line (split(/\n/,$definition)) { # go through the definition line by line Log3 $hash, 4, "check1: $line" . " - " . length($line); next if (length($line) <1); # Hajo 5 next if ($line =~ /^\s*\#.*/); # check for comment lines my ($type, $text, $x, $y, $size, $ang, $col, $fnt,$linegap,$blockwidth,$docolor); $type = undef; $text = undef; ($type, $text, $x, $y, $size, $ang, $col, $fnt, $linegap, $blockwidth) = split("#",$line); if (!defined $fnt) {$fnt = ''}; # Hajo 6 if (!defined $linegap) {$linegap = ''}; # Hajo 7 if (!defined $blockwidth) {$blockwidth = ''}; # Hajo 8 ($x, $y, $size, $ang, $col, $fnt,$linegap,$blockwidth) = ESPEInk_FetchReadings($x, $y, $size, $ang, $col, $fnt,$linegap,$blockwidth); $linegap = int($linegap) if ($linegap); $blockwidth = int($blockwidth) if ($blockwidth); next if (!defined $type); next if (!defined $text); $x = 0 if (!$x || (($x !~ '-?\d+')&&($x !~ '(left|mid|right)'))); $y = 0 if (!$y || (($y !~ '-?\d+')&&($y !~ '(top|mid|bottom)'))); $size = 10 if (!$size || ($size !~ '-?\d+')); $ang = 0 if (!$ang || ($ang !~ '-?\d+')); $docolor = $col?1:0; $col = "000000" if (!$col || !ESPEInk_CheckColorString($col)); $fnt = "medium" if (!ESPEInk_CheckFontString($fnt) && $type ne "addsymbol"); $angle = $ang/180*(4*atan2(1,1)); $color = $col; if ($type eq "iconreading" || $type eq "textreading") { my $eval=0; ($text,$eval) = split("{",$text); my ($device,$reading) = split(':',$text,2); $reading = "state" if (!$reading); $text = ReadingsVal($device,$reading,''); next if (($type eq 'iconreading') && (length($text) == 0)); # nothing to do, just skip - Hajo 2 if ($eval) { $eval =~ s/\}//g; $text = sprintf($eval,ReadingsVal($device,$reading,"")); } } if ($type eq "addicon") { my ($ret,$path) = ESPEInk_CheckIconString($text); next if (!$ret); } $r= hex(substr($col,0,2)); $g= hex(substr($col,2,2)); $b= hex(substr($col,4,2)); $color = $image->colorResolve($r,$g,$b); if ($type eq "addicon" || $type eq "iconreading") { my ($ret,$path) = ESPEInk_CheckIconString($text); my ($ext) = $path =~ /(\.[^.]+)$/; Log3 $defs{$name}, 2, "$name: icon of type 'svg' specified but SVG support not available setting ignored" if ($ext eq '.svg' && !$ESPEInk_has_SVG); if ($path =~ /http(?:s)\:\/\//) { # icon path seems to be a web path, lets download and use data content my $picdata = undef; ($ret, $picdata) = HttpUtils_BlockingGet({url=>$path,timeout=>30}); # load the image data from file on web server with url if ($ext eq '.svg' && defined $picdata) { $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root my $infile = catfile($rootname,$hash->{SUBFOLDER},$name,'tmp_svg.svg'); if (!open(RESULT,">",$infile)) { Log3 $hash, 1, "File $infile cannot be written"; } else { binmode RESULT; print RESULT $picdata; close RESULT; } $outfile = catfile($rootname,$hash->{SUBFOLDER},$name,'tmp_png.png'); if ($ESPEInk_has_SVG) { $rsvg = new Image::LibRSVG(); $rsvg->convert($infile, $outfile); unlink $infile; } if ($rsvg) {$icon_img = GD::Image->newFromPng($outfile,1);} } elsif ($ext eq '.png') { $icon_img = GD::Image->newFromPngData($picdata,1); } elsif ($ext eq '.gif') { $icon_img = GD::Image->newFromGifData($picdata); } elsif ($ext eq '.jpg') { $icon_img = GD::Image->newFromJpegData($picdata,1); } } else { # plain file, just open from file system if ($ext eq '.svg') { $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root $outfile = catfile($rootname,$hash->{SUBFOLDER},$name,'tmp_png.png'); if ($ESPEInk_has_SVG) { $rsvg = new Image::LibRSVG(); $rsvg->convert($path, $outfile); } if ($rsvg) {$icon_img = GD::Image->newFromPng($outfile,1);} } elsif ($ext eq '.png') { $icon_img = GD::Image->newFromPng($path,1); } elsif ($ext eq '.gif') { $icon_img = GD::Image->newFromGif($path); } elsif ($ext eq '.jpg') { $icon_img = GD::Image->newFromJpeg($path,1); } } if ($icon_img) { my ($sw,$sh) = $icon_img->getBounds; my $usedcolors = $icon_img->colorsTotal(); if ($docolor) { for (my $iy=0; $iy<$sh; $iy++) { for (my $ix=0; $ix<$sw; $ix++) { ($r,$g,$b) = $icon_img->rgb($icon_img->getPixel($ix,$iy)); # get color values in source file (my $alpha) = $icon_img->alpha($icon_img->getPixel($ix,$iy)); # get alpha-channel $icon_img->setPixel($ix,$iy,$color) if ($alpha == 0 && $r<180 && $g<180 && $b<180); # set color to given color if original color is black *your favorite tresholds may be different $icon_img->setPixel($ix,$iy,$color) if ($r>0 && $g>0 && $b>0); # set color to given color if original color is black #Log3 $hash, 1, "$r, $g, $b"; #$icon_img->setPixel($ix,$iy,$color) if ($r>0 && $g>0 && $b>0); # set color to given color if original color is black } } } my $srw = abs($sw*cos($angle)+$sh*sin($angle)); my $srh = abs($sh*cos($angle)+$sw*sin($angle)); my $icon_img_rot = GD::Image->new($srw,$srh,1); $icon_img_rot->alphaBlending(0); $icon_img_rot->fill($srw/2,$srh/2,$icon_img_rot->colorAllocateAlpha(0,0,0,127)); $icon_img_rot->copyRotated($icon_img,$srw/2,$srh/2,0,0,$sw,$sh,$ang); my $dh = $size; my $dw = $srw*$dh/$srh; my ($iw,$ih) = $image->getBounds; ($x,$y) = ESPEInk_CorrectXY($name,$type,"$dw#$dh",$fnt,$ang,$size,$iw,$ih,$x,$y); $image->copyResized($icon_img_rot,$x,$y,0,0,$dw,$dh,$srw,$srh); } } elsif ($type ne "addsymbol") { $font = gdGiantFont if ($fnt eq "giant"); $font = gdLargeFont if ($fnt eq "large"); $font = gdMediumBoldFont if ($fnt eq "medium"); $font = gdSmallFont if ($fnt eq "small"); my ($dw,$dh) = $image->getBounds; my $ly = $y; # Do not use full text here, since only height is relevant here and this can be calced from one char # my ($tw, $th) = ESPEInk_GetStringPixelWidth($name,"A",$fnt,$ang,$size); # Debug "line height : $th :"; $th += $linegap if ($linegap); # Debug "line height after : $th :"; $text = ESPEInk_FormatBlockText($name,$text,$fnt,$ang,$size,$blockwidth,$th); foreach my $tline (split(/\\n/,$text)) { ($x,$y) = ESPEInk_CorrectXY($name,$type,$tline,$fnt,$ang,$size,$dw,$dh,$x,$ly); if (!$font) { #use TTF from file given my $font = $fnt; my @bounds = $image->stringFT($color,$font,$size,$angle,$x,$size+$y,$tline); } else { if (($ang < -45) || ($ang > 45)) { $image->stringUp($font,$x,$y,$tline,$color); } else { $image->string($font,$x,$y,$tline,$color); } } $ly += $th; } } else { my ($sym,$s1,$s2) = ("","",""); ($sym,$s1,$s2) = split("-",$text); my $width = $fnt; my $height = $linegap; $image->setStyle($color); if ($sym eq "line") { my $angle = atan2($height,$width); for (my $i=0;$i<$size;$i++) { $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); $image->line($x,$y,$x+$width,$y+$height,gdStyled); $x+=sin($angle); $y-=cos($angle); } } elsif ($sym eq "rectangle") { $image->setThickness($size); $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); if ($s1 eq "filled" or $s2 eq "filled") { $image->filledRectangle($x,$y,$x+$width,$y+$height,gdStyled); } else { $image->rectangle($x,$y,$x+$width,$y+$height,gdStyled); } } elsif ($sym eq "ellipse") { $image->setThickness($size); $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); if ($s1 eq "filled" or $s2 eq "filled") { $image->filledEllipse($x+$width/2,$y+$height/2,$width,$height,gdStyled); } else { $image->ellipse($x+$width/2,$y+$height/2,$width,$height,gdStyled); } } elsif ($sym eq "arc") { $image->setThickness($size); $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); if ($s1 eq "filled" or $s2 eq "filled") { $image->filledArc($x+$width/2,$y+$height/2,$width,$height,$ang,$ang+$blockwidth,gdStyled,gdEdged); } else { $image->arc($x+$width/2,$y+$height/2,$width,$height,$ang,$ang+$blockwidth,gdStyled); } } } } } for (my $itext=1; $itext<=$deftexts; $itext++) { my $ccolor = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-color",'000000')); my $docolor = ($ccolor ne 0) && ($ccolor ne '000000'); $r= hex(substr($ccolor,0,2)); $g= hex(substr($ccolor,2,2)); $b= hex(substr($ccolor,4,2)); $color = $image->colorResolve($r,$g,$b); $angle = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0))/180*(4*atan2(1,1)); if (ReadingsVal($name,"$itext-isIcon",0)) { my ($ret,$path) = ESPEInk_CheckIconString(ReadingsVal($name,"$itext-icon","")); my ($ext) = $path =~ /(\.[^.]+)$/; Log3 $defs{$name}, 2, "$name: icon of type 'svg' specified but SVG support not available setting ignored" if ($ext eq '.svg' && !$ESPEInk_has_SVG); if ($path =~ /http(?:s)\:\/\//) { # icon path seems to be a web path, lets download and use data content my $picdata = undef; ($ret, $picdata) = HttpUtils_BlockingGet({url=>$path,timeout=>30}); # load the image data from file on web server with url if ($ext eq '.svg' && defined $picdata) { $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root my $infile = catfile($rootname,$hash->{SUBFOLDER},$name,'tmp_svg.svg'); if (!open(RESULT,">",$infile)) { Log3 $hash, 1, "File $infile cannot be written"; } else { binmode RESULT; print RESULT $picdata; close RESULT; } $outfile = catfile($rootname,$hash->{SUBFOLDER},$name,'tmp_png.png'); if ($ESPEInk_has_SVG) { $rsvg = new Image::LibRSVG(); $rsvg->convert($infile, $outfile); unlink $infile; } if ($rsvg) {$icon_img = GD::Image->newFromPng($outfile,1);} } elsif ($ext eq '.png') { $icon_img = GD::Image->newFromPngData($picdata,1); } elsif ($ext eq '.gif') { $icon_img = GD::Image->newFromGifData($picdata); } elsif ($ext eq '.jpg') { $icon_img = GD::Image->newFromJpegData($picdata,1); } } else { # plain file, just open from file system if ($ext eq '.svg') { $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root $outfile = catfile($rootname,$hash->{SUBFOLDER},$name,'tmp_png.png'); if ($ESPEInk_has_SVG) { $rsvg = new Image::LibRSVG(); $rsvg->convert($path, $outfile); } if ($rsvg) {$icon_img = GD::Image->newFromPng($outfile,1);} } elsif ($ext eq '.png') { $icon_img = GD::Image->newFromPng($path,1); } elsif ($ext eq '.gif') { $icon_img = GD::Image->newFromGif($path); } elsif ($ext eq '.jpg') { $icon_img = GD::Image->newFromJpeg($path,1); } } if ($icon_img) { my ($sw,$sh) = $icon_img->getBounds; if ($docolor) { for (my $iy=0; $iy<$sh; $iy++) { for (my $ix=0; $ix<$sw; $ix++) { ($r,$g,$b) = $icon_img->rgb($icon_img->getPixel($ix,$iy)); # get color values in source file (my $alpha) = $icon_img->alpha($icon_img->getPixel($ix,$iy)); # get alpha-channel $icon_img->setPixel($ix,$iy,$color) if ($alpha == 0 && $r<180 && $g<180 && $b<180); # set color to given color if original color is black *your favorite tresholds may be different $icon_img->setPixel($ix,$iy,$color) if ($r>0 && $g>0 && $b>0); # set color to given color if original color is black #$icon_img->setPixel($ix,$iy,$color) if ($r>0 && $g>0 && $b>0); # set color to given color if original color is black } } } my $srw = abs($sw*cos($angle)+$sh*sin($angle)); my $srh = abs($sh*cos($angle)+$sw*sin($angle)); my $icon_img_rot = GD::Image->new($srw,$srh,1); $icon_img_rot->alphaBlending(0); $icon_img_rot->fill($srw/2,$srh/2,$icon_img_rot->colorAllocateAlpha(0,0,0,127)); $icon_img_rot->copyRotated($icon_img,$srw/2,$srh/2,0,0,$sw,$sh,ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0))); my $dh = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size",10)); my $dw = $srw*$dh/$srh; my ($iw,$ih) = $image->getBounds; my ($x,$y) = ESPEInk_CorrectXY($name,"icon","$dw#$dh",ESPEInk_FetchReadings(ReadingsVal($name,"$itext-font","")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle","")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size","")),$iw,$ih,ESPEInk_FetchReadings(ReadingsVal($name,"$itext-x",0)),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-y",0))); $image->copyResized($icon_img_rot,$x,$y,0,0,$dw,$dh,$srw,$srh); } } elsif (!ReadingsVal($name,"$itext-isSymbol",0)) { my $cfont = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-font","")); $font = undef; $font = gdGiantFont if ($cfont eq "giant"); $font = gdLargeFont if ($cfont eq "large"); $font = gdMediumBoldFont if ($cfont eq "medium"); $font = gdSmallFont if ($cfont eq "small"); my ($dw,$dh) = $image->getBounds; my $ly = ReadingsVal($name,"$itext-y",0); # Do not use full text here, since only height is relevant here and this can be calced from one char # my ($tw, $th) = ESPEInk_GetStringPixelWidth($name,"A",ESPEInk_FetchReadings(ReadingsVal($name,"$itext-font","small")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0)),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size",10))); $th += int(ReadingsVal($name,"$itext-linegap",0)); my $text = ESPEInk_FormatBlockText($name,ReadingsVal($name,"$itext-text",""),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-font","small")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0)),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size",10)),int(ESPEInk_FetchReadings(ReadingsVal($name,"$itext-blockwidth",0))),$th); foreach my $tline (split(/\\n/,$text)) { my ($x,$y) = ESPEInk_CorrectXY($name,"text",$tline,ESPEInk_FetchReadings(ReadingsVal($name,"$itext-font","")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle","")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size","")),$dw,$dh,ESPEInk_FetchReadings(ReadingsVal($name,"$itext-x",0)),$ly); if (!$font) { #use TTF from file given my $fontfile = ReadingsVal($name,"$itext-font",""); my @bounds = $image->stringFT($color,ESPEInk_FetchReadings(ReadingsVal($name,"$itext-font","")),ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size",10)),$angle,$x,ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size",10))+$y,$tline); } else { if ((ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0)) < -45) || (ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0)) > 45)) { $image->stringUp($font,$x,$y,$tline,$color); } else { $image->string($font,$x,$y,$tline,$color); } } $ly += $th; } } else { my ($sym,$s1,$s2) = ("","",""); ($sym,$s1,$s2) = split("-",ReadingsVal($name,"$itext-symbol","")); my $size = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-size","")); my $width = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-width",0)); my $height = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-height",0)); my $ang = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-angle",0)); my $arc = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-arc",0)); my $x = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-x",0)); my $y = ESPEInk_FetchReadings(ReadingsVal($name,"$itext-y",0)); $image->setStyle($color); if ($sym eq "line") { my $angle = atan2($height,$width); for (my $i=0;$i<$size;$i++) { $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); $image->line($x,$y,$x+$width,$y+$height,gdStyled); $x+=sin($angle); $y-=cos($angle); } } elsif ($sym eq "rectangle") { $image->setThickness($size); $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); if ($s1 eq "filled" or $s2 eq "filled") { $image->filledRectangle($x,$y,$x+$width,$y+$height,gdStyled); } else { $image->rectangle($x,$y,$x+$width,$y+$height,gdStyled); } } elsif ($sym eq "ellipse") { $image->setThickness($size); $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); if ($s1 eq "filled" or $s2 eq "filled") { $image->filledEllipse($x+$width/2,$y+$height/2,$width,$height,gdStyled); } else { $image->ellipse($x+$width/2,$y+$height/2,$width,$height,gdStyled); } } elsif ($sym eq "arc") { $image->setThickness($size); $image->setStyle($color,$color,$color,$color,gdTransparent,gdTransparent,gdTransparent,gdTransparent) if ($s1 eq "dashed" or $s2 eq "dashed"); $image->setStyle($color,$color,gdTransparent,gdTransparent) if ($s1 eq "dotted" or $s2 eq "dotted"); if ($s1 eq "filled" or $s2 eq "filled") { $image->filledArc($x+$width/2,$y+$height/2,$width,$height,$ang,$ang+$arc,gdStyled,gdEdged); } else { $image->arc($x+$width/2,$y+$height/2,$width,$height,$ang,$ang+$arc,gdStyled); } } } } } #--------------------------------------------------------------------------------------------------- # convert the input picture into a picture taking into account capabilities of given eInk module # if wanted, Floyd-Steinberg-Dithering is performed to better represent pictures with many colors sub ESPEInk_ConvertDone(@) { my ($string) = @_; return unless ( defined($string) ); my ( $name, $upload, %values ) = split( "\\|", $string ); my $hash = $defs{$name}; Log3 $hash, 4, "Finished conversion in background".(($upload eq "1")?" doing automatic upload as requested":""); $hash->{STATE} = "Finished conversion in background"; delete( $hash->{helper}{DO_UPLOAD} ); delete( $hash->{helper}{RUNNING_PID} ); ESPEInk_MakePictureHTML($name,"result_picture","result.png"); ESPEInk_Upload($hash) if ($upload eq "1"); } #--------------------------------------------------------------------------------------------------- # convert the input picture into a picture taking into account capabilities of given eInk module # if wanted, Floyd-Steinberg-Dithering is performed to better represent pictures with many colors sub ESPEInk_ConvertAborted(@) { my ($hash) = @_; delete( $hash->{helper}{DO_UPLOAD} ); delete( $hash->{helper}{RUNNING_PID} ); Log3 $hash, 4, "Forked process timed out"; } #--------------------------------------------------------------------------------------------------- # convert the input picture into a picture taking into account capabilities of given eInk module # if wanted, Floyd-Steinberg-Dithering is performed to better represent pictures with many colors sub ESPEInk_Convert(@) { my ($hash,$upload) = @_; my $name = $hash->{NAME}; if (defined ($hash->{helper}{RUNNING_PID})) { BlockingKill($hash->{helper}{RUNNING_PID}); delete( $hash->{helper}{DO_UPLOAD} ); delete( $hash->{helper}{RUNNING_PID} ); Log3 $hash, 4, "Killing old forked process"; } unless (defined ($hash->{helper}{RUNNING_PID})) { $hash->{helper}{DO_UPLOAD} = $upload; $hash->{helper}{RUNNING_PID} = BlockingCall( "ESPEInk_DoConvert", # callback worker task $hash, # hash of the device and upload trigger "ESPEInk_ConvertDone", # callback result method AttrVal($name,"uploadTimeout",290), # timeout seconds "ESPEInk_ConvertAborted", # callback for abortion $hash ); # parameter for abortion Log3 $hash, 4, "Start forked process to convert output picture"; return "Starting conversion in background"; } else { Log3 $hash, 1, "Could not start forked process, old process still running"; } } #--------------------------------------------------------------------------------------------------- # convert the input picture into a picture taking into account capabilities of given eInk module # if wanted, Floyd-Steinberg-Dithering is performed to better represent pictures with many colors sub ESPEInk_DoConvert(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $srcimage; my $srcimage_orig; my $dstimage; my $fname; my $colormode; my $convertmode; my $devtype; my ($r,$g,$b); my $w; my $h; my $dw; my $dh; my $dx; my $dy; my $y; my $x; my $index; my $pind; my $placement; my $scale2fit; my $rootname; my $outfilename; my $ext; my @coloroffset; my $upload = $hash->{helper}{DO_UPLOAD}; $fname = ESPEInk_GetSetting($name,"picturefile"); ESPEInk_UpdatePicture($hash,$fname); $colormode = ESPEInk_GetSetting($name,"colormode"); $convertmode = ESPEInk_GetSetting($name,"convertmode"); $devtype = ESPEInk_GetSetting($name,"devicetype"); $dw = AttrVal($name,"width",$ESPEInk_devices{$devtype}{"width"}); $dh = AttrVal($name,"height",$ESPEInk_devices{$devtype}{"height"}); $placement = AttrVal($name,"placement","top-left"); $scale2fit = AttrVal($name,"scale2fit",0); @coloroffset = split(";", AttrVal($name,"coloroffset","0;0;0")); if ($colormode eq 'monochrome') { $pind = 0; # use only two colors for output no matter of capabilities } else { $pind = $ESPEInk_devices{$devtype}{"pindex"}; } if (!open(IMAGE, $fname)) { Log3 $hash, 1, "$name: File $fname cannot be opened"; return "$name|$upload|ESPEInk - Convert: Error opening sourcefile"; } else { close IMAGE; GD::Image->trueColor(1); ($ext) = $fname =~ /(\.[^.]+)$/; if ($ext eq '.png') { $srcimage_orig = GD::Image->newFromPng($fname,1); } elsif ($ext eq '.jpg') { $srcimage_orig = GD::Image->newFromJpeg($fname,1); } elsif ($ext eq '.gif') { $srcimage_orig = GD::Image->newFromGif($fname,1); } if (!$srcimage_orig) {return "$name|$upload|ESPEInk - Convert: Error opening sourcefile";} ESPEInk_AddObjects($name,$srcimage_orig); ($w,$h) = $srcimage_orig->getBounds; if (($w/$h<1) and ($dw/$dh>1) or ($w/$h>1) and ($dw/$dh<1)) { # rotate image 90 deg. $srcimage_orig = $srcimage_orig->copyRotate90(); } if ($scale2fit) { $srcimage = GD::Image->new($dw,$dh,1); $srcimage->copyResized($srcimage_orig,0,0,0,0,$dw,$dh,$w,$h); } else { $srcimage = $srcimage_orig; #$srcimage->copyResized($srcimage_orig,0,0,0,0,$w,$h,$w,$h); } ($w,$h) = $srcimage->getBounds; Log3 $hash, 4, "File $fname opened, sizes is $w x $h"; if ($convertmode ne 'level') { ESPEInk_Dither($srcimage,$pind); } if ($placement eq 'top-left') { $dx = AttrVal($name,"x0",0); $dy = AttrVal($name,"y0",0); } elsif ($placement eq 'top-right') { $dx = AttrVal($name,"x0",$w-$dw); $dy = AttrVal($name,"y0",0); } elsif ($placement eq 'bottom-left') { $dx = AttrVal($name,"x0",0); $dy = AttrVal($name,"y0",$h-$dh); } elsif ($placement eq 'bottom-right') { $dx = AttrVal($name,"x0",$w-$dw); $dy = AttrVal($name,"y0",$h-$dh); } GD::Image->trueColor(0); $dstimage = GD::Image->new($dw,$dh,0); if (!$dstimage) {return "$name|$upload|ESPEInk - Convert: Error opening destinationfile";} ESPEInk_AllocateColors($hash,$dstimage,$pind); # allocate colors according to capabilities of selected EInk display for (my $iy=0; $iy<$dh; $iy++) { $y = $iy + $dy; if (($y<0)||($y>=($h))){ # outside of source area, fill destination with alternating black and white in chess board kind for (my $i=0;$i<$dw;$i++) {$dstimage->setPixel($i,$iy,(($i+$iy)%2==0)?1:0);} next; } for (my $ix=0; $ix<$dw; $ix++) { $x = $ix + $dx; if (($x<0)||($x>=($w))){ # outside of source area, fill destination with alternating black and white in chess board kind $dstimage->setPixel($ix,$iy,(($ix+$iy)%2==0)?1:0); next; } ($r,$g,$b) = $srcimage->rgb($srcimage->getPixel($x,$y)); # get color values in source file $dstimage->setPixel($ix,$iy,$dstimage->colorClosest($r+$coloroffset[0],$g+$coloroffset[1],$b+$coloroffset[2])); # set destination color to closest fitting color of eInk display } } $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root $outfilename = catfile($rootname,$hash->{SUBFOLDER},$name,"result.png"); if (!open(RESULT,">",$outfilename)) { Log3 $hash, 1, "File $outfilename cannot be written"; } else { binmode RESULT; print RESULT $dstimage->png; close RESULT; } } return "$name|$upload"; } #--------------------------------------------------------------------------------------------------- # get pixel from image sub ESPEInk_getPixel($$$$) { my ($image, $w, $h, $indx ) = @_; my $iy = int( $indx / $w ); my $ix = $indx % $w; my ($r,$g,$b) = $image->rgb($image->getPixel($ix,$iy)); return ($r==0&&$g==0)?0:(($r==255&&$g==255)?1:(($r==127&&$g==127)?2:3)); #convert color values to values between 0 and 3 } #--------------------------------------------------------------------------------------------------- # get pixel from image sub ESPEInk_getPixel7C($$$$) { my ($image, $w, $h, $indx ) = @_; my $iy = int( $indx / $w ); my $ix = $indx % $w; my ($r,$g,$b) = $image->rgb($image->getPixel($ix,$iy)); my $retval = 7; #convert color values to values between 0 and 6 $retval = 0 if ($r==0&&$g==0&&$b==0); $retval = 1 if ($r==255&&$g==255&&$b==255); $retval = 2 if ($r==0&&$g==255&&$b==0); $retval = 3 if ($r==0&&$g==0&&$b==255); $retval = 4 if ($r==255&&$g==0&&$b==0); $retval = 5 if ($r==255&&$g==255&&$b==0); $retval = 6 if ($r==255&&$g==128&&$b==0); return $retval; } #--------------------------------------------------------------------------------------------------- # helper function for coding picture pixels to EInk bit settings sub ESPEInk_CodePixel2BitsImage($$$$$$$$) { my ($indx,$bits,$comp,$imax,$image,$w,$h,$isize) = @_; my $v; my $x; my $str; if ($bits == 8) { if ($comp == -1) { $str = ""; while (($indx < $imax)) { $x = 0; while ($x < 122) { $v = 0; for (my $i=0; $i<$bits&&$x<122; $i++,$x++) { if ((ESPEInk_getPixel($image,$w,$h,$indx)!=0)) { $v|=(128>>$i); } $indx++; } $str .= ESPEInk_Byte2String($v); } } return ($indx, $str); } else { $v = 0; for (my $i=0; $i<$bits; $i++) { if (($indx<$isize)&&(ESPEInk_getPixel($image,$w,$h,$indx)!=$comp)) { $v|=(128>>$i); } $indx++; } return ($indx, ESPEInk_Byte2String($v)); } } elsif ($bits == 16) { $v = 0; if ($comp == -2) { for (my $i=0; $i<$bits; $i+=4) { if ($indx<$isize) { $v|=(ESPEInk_getPixel7C($image,$w,$h,$indx)<<$i); } $indx++; } } else { for (my $i=0; $i<$bits; $i+=2) { if ($indx<$isize) { $v|=(ESPEInk_getPixel($image,$w,$h,$indx)<<$i); } $indx++; } } return ($indx, ESPEInk_Word2String($v)); } } # #--------------------------------------------------------------------------------------------------- # # helper function for coding picture pixels to EInk bit settings # sub ESPEInk_CodePixel2Bits($$$$$) { # my ($indx,$bits,$comp,$imax,$array) = @_; # my $v; # my $x; # my $str; # if ($bits == 8) { # if ($comp == -1) { # $str = ""; # while (($indx < $imax)) { # $x = 0; # while ($x < 122) { # $v = 0; # for (my $i=0; $i<$bits&&$x<122; $i++,$x++) { # if ((${$array}[$indx]!=0)) { # $v|=(128>>$i); # } # $indx++; # } # $str .= ESPEInk_Byte2String($v); # } # } # return ($indx, $str); # } else { # $v = 0; # for (my $i=0; $i<$bits; $i++) { # if (($indx<@{$array})&&(${$array}[$indx]!=$comp)) { # $v|=(128>>$i); # } # $indx++; # } # return ($indx, ESPEInk_Byte2String($v)); # } # } elsif ($bits == 16) { # $v = 0; # for (my $i=0; $i<$bits; $i+=2) { # if ($indx<@{$array}) { # $v|=(${$array}[$indx]<<$i); # } # $indx++; # } # return ($indx, ESPEInk_Word2String($v)); # } # } #--------------------------------------------------------------------------------------------------- # helper function for converting a byte value to a string readable by EInk display sub ESPEInk_Byte2String($) { my ($v) = @_; return chr(($v & 0xF) + 97).chr((($v >> 4) & 0xF) + 97); } #--------------------------------------------------------------------------------------------------- # helper function for converting a 2 byte value to a string readable by EInk display sub ESPEInk_Word2String($) { my ($v) = @_; return ESPEInk_Byte2String($v&0xFF).ESPEInk_Byte2String(($v>>8)&0xFF); } #--------------------------------------------------------------------------------------------------- # helper function to encode the picture pixels to the right values for the EIknk display sub ESPEInk_Encode4UploadImage($$$) { my($param,$bits,$comp) = @_; my $i = int($param->{control}{srcindex}); my $ret = ""; my $imax; my $image = $param->{image}; my $w = $param->{imagew}; my $h = $param->{imageh}; my $isize = $param->{imagesize}; if (($i+int($param->{maxulsize})*($bits==8?4:($comp==-2?1:2))) > ($isize-1)) { $imax = $isize-1; } else { $imax = $i+int($param->{maxulsize})*($bits==8?4:($comp==-2?1:2)); } if ($param->{device} == 3) { $imax = $i+int($param->{maxulsize})*($bits==8?4:($comp==-2?1:2))-(($param->{board} eq "ESP8266")?366:122); } my $postdata = ""; while ($i < $imax) { ($i, $ret) = ESPEInk_CodePixel2BitsImage($i,$bits,$comp,$imax,$image, $w, $h, $isize); $postdata .= $ret; } return ($i,$postdata.ESPEInk_Word2String(length($postdata))."LOAD"); } # #--------------------------------------------------------------------------------------------------- # # helper function to encode the picture pixels to the right values for the EIknk display # sub ESPEInk_Encode4Upload($$$$) { # my($param,$bits,$comp,$data) = @_; # my $i = int($param->{control}{srcindex}); # my $ret = ""; # my $imax; # if (($i+int($param->{maxulsize})*($bits==8?4:2)) > (@{$data}-1)) { # $imax = @{$data}-1; # } else { # $imax = $i+int($param->{maxulsize})*($bits==8?4:2); # } # if ($param->{device} == 3) { # $imax = $i+int($param->{maxulsize})*($bits==8?4:2)-(($param->{board} eq "ESP8266")?366:122); # } # my $postdata = ""; # while ($i < $imax) { # ($i, $ret) = ESPEInk_CodePixel2Bits($i,$bits,$comp,$imax,\@{$data}); # $postdata .= $ret; # } # return ($i,$postdata.ESPEInk_Word2String(length($postdata))."LOAD"); # } #--------------------------------------------------------------------------------------------------- # callback function for the http calls. Here all upload logic is happening sub ESPEInk_HTTPCallbackA(@) { my ($inparam, $err, @dat) = @_; my $devname = $inparam->{devname}; my $param = $inparam->{$devname}; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $imax; my $postdata; my $i; my $comp; my $bits; Log3 ($hash, 5, "$name: callback from command $param->{command}, URL $inparam->{url} status code $inparam->{code}"); my $cparams = { url => 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}, timeout => $param->{control}{timeout}, method => $inparam->{method}, header => $inparam->{header}, callback => $inparam->{callback}, devname => $name }; if ($inparam->{code} != 200 && ($param->{board} eq "ESP8266")) { # there seemed to be problem with the communication lets try again with the old data. $param->{control}{retries} += 1; if ($param->{control}{retries} > $param->{control}{maxretries}) { # respect max retries Log3 ($hash, 1, "$name: problems with communication to device, max retries ($param->{control}{maxretries}) reached"); $hash->{STATE} = "Error uploading image to device, max retries ($param->{control}{maxretries}) reached"; } else { # we still have retries, go on with same data Log3 ($hash, 3, "$name: problems with communication to device, trying once more ($param->{control}{retries} of $param->{control}{maxretries} done)"); $cparams->{$name} = $param; HttpUtils_NonblockingGet($cparams); } } else { if ($param->{device} == 3) { # special treatment for this device if ($param->{control}{stepindex} == 0) { # we have different steps for uploading the different colors and closing the upload $comp = -1; $bits = 8; $param->{command} = "LOAD"; # start uploading black channel $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; ($param->{control}{srcindex},$param->{data}) = ESPEInk_Encode4UploadImage($param,$bits,$comp); $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{data}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++ if ($param->{control}{srcindex} >= $param->{imagesize}); # complete array has been coded and sent, go to next step $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); Log3 $hash, 5, "$name: $param->{control}{srcindex}, $param->{control}{stepindex}, ".$param->{data}; } elsif ($param->{control}{stepindex} == 1) { $param->{command} = "SHOW"; # all data uploaded tell device to show uploaded data $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++; $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); } elsif ($param->{control}{stepindex} == 2) { $hash->{STATE} = "Successfully uploaded image to device"; } } elsif (($param->{device} == 0) || ($param->{device} == 3) || ($param->{device} == 6) || ($param->{device} == 7) || ($param->{device} == 9) || ($param->{device} == 12) || ($param->{device} == 16) || ($param->{device} == 19) || ($param->{device} == 22)) { # black and white display if ($param->{control}{stepindex} == 0) { # we have different steps for uploading the different colors and closing the upload $comp = 0; $bits = 8; $param->{command} = "LOAD"; # start uploading black channel $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; ($param->{control}{srcindex},$param->{data}) = ESPEInk_Encode4UploadImage($param,$bits,$comp); $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{data}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++ if ($param->{control}{srcindex} >= $param->{imagesize}); # complete array has been coded and sent, go to next step $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); Log3 $hash, 5, "$name: $param->{control}{srcindex}, $param->{control}{stepindex}, ".$param->{data}; } elsif ($param->{control}{stepindex} == 1) { $param->{command} = "SHOW"; # all data uploaded tell device to show uploaded data $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++; $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); } elsif ($param->{control}{stepindex} == 2) { $hash->{STATE} = "Successfully uploaded image to device"; } } elsif ((($param->{device} > 15) && ($param->{device} < 22)) || $param->{device} == 25) { # special treatment for bigger displays, upload is done in one step for all colors if ($param->{control}{stepindex} == 0) { # we have different steps for uploading the different colors and closing the upload $comp = $param->{device}==25?-2:-1; $bits = 16; $param->{command} = "LOAD"; # start uploading black channel $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; ($param->{control}{srcindex},$param->{data}) = ESPEInk_Encode4UploadImage($param,$bits,$comp); $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{data}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++ if ($param->{control}{srcindex} >= $param->{imagesize}); # complete array has been coded and sent, go to next step $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); Log3 $hash, 5, "$name: $param->{control}{srcindex}, $param->{control}{stepindex}, ".$param->{data}; } elsif ($param->{control}{stepindex} == 1) { $param->{command} = "SHOW"; # all data uploaded tell device to show uploaded data $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++; $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); } elsif ($param->{control}{stepindex} == 2) { $hash->{STATE} = "Successfully uploaded image to device"; } } else { # treatment for all smaller color capable displays if ($param->{control}{stepindex} == 0) { # we have different steps for uploading the different colors and closing the upload if ($param->{device} == 1 || $param->{device} == 12) { $comp = -1; $bits = 16; } elsif ($param->{device} == 23) { $comp = 0; $bits = 8; } else { $comp = 0; $bits = 8; } $param->{command} = "LOAD"; # start uploading black channel $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; ($param->{control}{srcindex},$param->{data}) = ESPEInk_Encode4UploadImage($param,$bits,$comp); $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{data}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++ if ($param->{control}{srcindex} >= $param->{imagesize}); # complete array has been coded and sent, go to next step $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); Log3 $hash, 5, "$name: $param->{control}{srcindex}, $param->{control}{stepindex}, ".$param->{data}; } elsif ($param->{control}{stepindex} == 1) { $param->{control}{srcindex} = 0; # black channel upload done, tell device that next step starts $param->{command} = "NEXT"; $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++; $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); Log3 $hash, 5, "$name: $param->{control}{srcindex}, $param->{control}{stepindex}, ".$param->{data}; } elsif ($param->{control}{stepindex} == 2) { $comp = 3; $bits = 8; $param->{command} = "LOAD"; # start uploading black channel $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; ($param->{control}{srcindex},$param->{data}) = ESPEInk_Encode4UploadImage($param,$bits,$comp); $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{data}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++ if ($param->{control}{srcindex} >= $param->{imagesize}); # complete array has been coded and sent, go to next step $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); Log3 $hash, 5, "$name: $param->{control}{srcindex}, $param->{control}{stepindex}, ".$param->{data}; } elsif ($param->{control}{stepindex} == 3) { $param->{command} = "SHOW"; # all data uploaded tell device to show uploaded data $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}; $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$param->{command}.'_' if ($param->{board} eq "ESP32"); $param->{data} = '' if ($param->{board} eq "ESP32"); $param->{control}{stepindex}++; $cparams->{$name} = $param; $cparams->{data} = $param->{data}; HttpUtils_NonblockingGet($cparams); } elsif ($param->{control}{stepindex} == 4) { $hash->{STATE} = "Successfully uploaded image to device"; } } } } #--------------------------------------------------------------------------------------------------- # initialization of upload and first http call (rest is cared for in the callback function) sub ESPEInk_PostToDeviceImage($$$$$) { my ($hash,$command,$devind,$control,$image) = @_; my $name = $hash->{NAME}; my ($w,$h) = $image->getBounds if ( defined( $image ) ); # JV my $params = { command => $command, device => $devind, # outarray => @outarray, image => $image, # JV imagew => $w, # JV imageh => $h, # JV imagesize => $w*$h, # JV board => ($command eq "EPDx_")?"ESP32":"ESP8266", hash => $hash, control => $control, maxulsize => ($command eq "EPDx_")?1000:1500 }; my $cparams = { method => "POST", header => "Content-Type: text/plain\r\nUser-Agent: fhem\r\nAccept: */*", data => ESPEInk_Byte2String($devind), callback => \&ESPEInk_HTTPCallbackA, devname => $name }; if ($params->{board} eq "ESP32") { if ($params->{device} == 3) { # special treatment for this device $params->{command} =~ s/x_/d_/g; } else { # treatment for all smaller color capable displays my $subst = chr($params->{device}+97)."_"; $params->{command} =~ s/x_/$subst/g; } } $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$params->{command}; $cparams->{data} = '' if ($params->{board} eq "ESP32"); $cparams->{$name} = $params; Log3 ($hash, 3, "$name: sending HTTP request to $cparams->{url} with data: $cparams->{data}"); HttpUtils_NonblockingGet($cparams); } # #--------------------------------------------------------------------------------------------------- # # initialization of upload and first http call (rest is cared for in the callback function) # sub ESPEInk_PostToDevice($$$$$) { # my ($hash,$command,$devind,$control,@outarray) = @_; # my $name = $hash->{NAME}; # my $params = { # command => $command, # device => $devind, # outarray => @outarray, # board => ($command eq "EPDx_")?"ESP32":"ESP8266", # hash => $hash, # control => $control, # maxulsize => ($command eq "EPDx_")?1000:1500 # }; # my $cparams = { # method => "POST", # header => "Content-Type: text/plain\r\nUser-Agent: fhem\r\nAccept: */*", # data => ESPEInk_Byte2String($devind), # callback => \&ESPEInk_HTTPCallbackA, # devname => $name # }; # if ($params->{board} eq "ESP32") { # if ($params->{device} == 3) { # special treatment for this device # $params->{command} =~ s/x_/d_/g; # } else { # treatment for all smaller color capable displays # my $subst = chr($params->{device}+97)."_"; # $params->{command} =~ s/x_/$subst/g; # } # } # $cparams->{url} = 'http://'.ESPEInk_GetSetting($name,"url").'/'.$params->{command}; # $cparams->{data} = '' if ($params->{board} eq "ESP32"); # $cparams->{$name} = $params; # Log3 ($hash, 3, "$name: sending HTTP request to $cparams->{url} with data: $cparams->{data}"); # HttpUtils_NonblockingGet($cparams); # } #--------------------------------------------------------------------------------------------------- # code converted picture and send it to eInk driver board (and thus to the connected eInk display) sub ESPEInk_Upload(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $devtype = ESPEInk_GetSetting($name,"devicetype"); my $minint = AttrVal($name,"mininterval",0); if ($minint eq 'auto') { $minint = $ESPEInk_devices{$devtype}{"updateint"}; } my $dlastupdate = gettimeofday()-ReadingsVal($name,"updatestart",0); if ($dlastupdate < $minint) { Log3 ($hash, 3, "$name: Time $dlastupdate since last Upload too small, try again later or change mininterval ($minint)"); return "Time since last Upload too small, try again later or change mininterval"; } my $url = ESPEInk_GetSetting($name,"url"); if (!defined $url) { return "Error, missing url. Define url to device first"; } if ($hash->{STATE} =~ /Uploading/) { Log3 ($hash, 2, "$name: Upload of image currently running, try again later"); return $hash->{STATE}; } $hash->{STATE} = "Uploading image to device"; readingsSingleUpdate( $hash, "updatestart", gettimeofday(), 1 ); my @outarray; my $boardtype = ESPEInk_GetSetting($name,"boardtype"); my $devind = $ESPEInk_devices{$devtype}{"id"}; my $rootname = $FW_dir; #File::Spec->rel2abs($FW_dir); # get Filename of FHEMWEB root my $filename = catfile($rootname,$hash->{SUBFOLDER},$name,"result.png"); my $image; # JV if (!open(RESULT,$filename)) { Log3 $hash, 1, "File $filename cannot be opened"; return "Error opening image file $filename for upload"; } # else { close RESULT; # my $image = GD::Image->newFromPng($filename); # JV $image = GD::Image->newFromPng($filename); # JV # my ($w,$h) = $image->getBounds; # my ($r,$g,$b); # my $i = 0; # for (my $iy=0; $iy<$h; $iy++) { # for (my $ix=0; $ix<$w; $ix++) { # ($r,$g,$b) = $image->rgb($image->getPixel($ix,$iy)); # $outarray[$i] = ($r==0&&$g==0)?0:(($r==255&&$g==255)?1:(($r==127&&$g==127)?2:3)); #convert color values to values between 0 and 3 # $i++; # } # } # } $ESPEInk_uploadcontrol{"retries"} = 0; # actual number of retries $ESPEInk_uploadcontrol{"maxretries"} = AttrVal($name,"maxretries",3); # maximum number of retries $ESPEInk_uploadcontrol{"timeout"} = AttrVal($name,"timeout",10); # timout for HTTP calls $ESPEInk_uploadcontrol{"stepindex"} = 0; # step currently performed (for multi color displays each color is transferred separately) $ESPEInk_uploadcontrol{"srcindex"} = 0; # index of source array currently transferred (we might need to split due to the limit on arduino code in data transfer length via HTTP) ESPEInk_PostToDeviceImage($hash,($boardtype eq "ESP32")?'EPDx_':'EPD',$devind,\%ESPEInk_uploadcontrol,$image); # convert data from conversion result in appropriate format and send it to the device via HTTP return $hash->{STATE}; } 1; =pod =item device =item summary Generate output pictures for EInk devices with ESP32 or ESP8266 driver. =begin html

ESPEInk

    ESPEInk This module implements the possibility to send pictures generated in FHEM to an raw eInk paper connected to an ESP8266 or ESP32 eInk WLAN driver board from waveshare (see details about the board at Wiki ESP8266 Waveshare Driver Board and Wiki ESP32 Waveshare Driver Board).
    The module consists of 2 parts. One is the preparation of the picture to be send to the board. Here a template picture can be defined and additional texts can be added. Furthermore it can be specified how the conversion should be done (e.g. if size of template picture should be automatically fit to eInk size, dithering, color mode or monochrome) The second part converts the picture to the right format and sends it to the board and thus changes the display on the eInk paper.

    Prerequisites

      This module requires an eInk paper raw module from Waveshare (exisiting in different sizes, number of colors and resolutions) and a WLAN driver board (Wiki Waveshare Driver Board). The driver board needs the installation of an adruino sketch as described in the Wiki (see link before). After this the module together with the eInk display only needs power supply. The data exchange between FHEM and the display is done via WLAN. The module further needs installation of the GD library for perl (see installation instructions at GD for Perl and at Image::LibRSVG for Perl).

    Define
      define <name> ESPEInk <picturefile> [url] [interval] [colormode] [convertmode]

      Example: define myEInk ESPEInk /opt/fhem/www/images/test.png

      The picturefile parameter must be path to a readable file in the filesystem of FHEM.
      [url] is the url of the WLAN driver board (typically an IP address but could also be a Web-URL). This parameter is optional but needed as soon as you want to not only generate the picture but also upload it to the eInk display. For upload it is also necessary to specify the correct device type (default is 1.54inch_e-Paper_Module see also attribute devicetype, for a list of all available device types use get devices).
      [interval] is a time interval for regular updates of the information to the eInk display. It automatically converts the inputs and uploads the result to the eInk display and defaults to 300 seconds.
      [colormode] can be either color or monochrome and specifies if the inputs should be converted to a fitting color picture or if the conversion should be done in monochrome format.
      [convertmode] can be either level or dithering. If level is specified, the conversion transforms the given colors to the ones to the closest color the display is capable of (depending on the display type this can be black, red or yellow). When dithering is specified, the conversion performs a Floyd Steinberg dithering to fit the input picture better to the capabilities of the eInk display.

    Set
      set <name> <option> <value>

      Options:
      • addtext
        This option allows to specify a text that should be added to the template picture at any position.
        The value must be given in the form: text#x#y#size#angle#color#font#linegap#blockwidth
        where:
        text is the text that should be added. This is the only mandatory part of the value.
        x is the x-position of the lower left corner of the text relative to the lower left corner of the display area
        y is the y-position of the lower left corner of the text relative to the lower left corner of the display area
        size is the size of the text in pixels. This parameter is only used if a TTF font type is specified (see below)
        angle is the rotation angle (counterclockwise) that should be used when drawing the text. In case of not using TTF fonts (see below), the resulting rotation can be only 0 or 90 degrees.
        color is an RGB hex string that specifies the RGB values of the color respectively (e.g. 00FF00 for green)
        font is either one of small medium large giant or a path to a valid TTF font file in the FHEM file system.
        linegap The distance between consecutive lines if blocksetting is used (see blockwidth below) positive values increase the distance negative values reduce the distance.
        blockwidth The width in pixels of the text block generated by the text string. If the string width on the display is higher than the given width in pixels, an automatic linebreak will be added.
        For each of the specified texts a list of readings and attributes is added to the device. The readings are holding the parameters finally taken for the generation of the output picture. The attributes allow an easy change of the readings (see details in the description below). Once at least one text is specified, the set option removeobject is added.
      • addicon
        This option allows to specify an icon that should be added to the template picture at any position.
        The value must be given in the form: icon#x#y#size#angle#color
        where:
        icon is the name of the icon that should be added. This is the only mandatory part of the value. Icons can either be specified as fhem icon names (any installed fhem icons are supported), or as url links to web icons
        x is the x-position of the lower left corner of the icon relative to the lower left corner of the display area
        y is the y-position of the lower left corner of the icon relative to the lower left corner of the display area
        size is the size of the icon in pixels. This parameter is only used if a TTF font type is specified (see below)
        angle is the rotation angle (counterclockwise) that should be used when drawing the icon. In case of not using TTF fonts (see below), the resulting rotation can be only 0 or 90 degrees.
        color is an RGB hex string that specifies the RGB values of the color respectively (e.g. 00FF00 for green)
        For each of the specified icons a list of readings and attributes is added to the device. The readings are holding the parameters finally taken for the generation of the output picture. The attributes allow an easy change of the readings (see details in the description below). Once at least one icon is specified, the set option removeobject is added.
      • addsymbol
        This option allows to specify an symbols (lines, ellipses, rectangles, arcs) that should be added to the template picture at any position.
        The value must be given in the form: symbol#x#y#thickness#angle#color#width#height#segment
        where:
        symbol is specifying which symbol should be drawn. Possible values are line or rectangle or ellipse or arc. If -filled is added, the symbol is filled, if -dotted or -dashed is added, the line is drawn dotted or dashed. For example a symbol value given as 'line-dotted' will draw a dotted line, 'rectangle-filled' will draw a filled rectangle.
        x is the x-position of the lower left corner of the symbol relative to the lower left corner of the display area
        y is the y-position of the lower left corner of the symbol relative to the lower left corner of the display area
        thickness is the thickness of lines or outlines of the symbols drawn.
        angle is the rotation angle (counterclockwise) that should be used when drawing the symbol.
        color is an RGB hex string that specifies the RGB values of the color respectively (e.g. 00FF00 for green)
        width is the width of the symbol (in case of lines, the line is drawn width pixels in x direction and height pixels in y direction).
        height is the width of the symbol (in case of lines, the line is drawn width pixels in x direction and height pixels in y direction).
        segment is only used for arc type symbols. In this case angle (see above) specifies the start of the arc and segment specifices the width of the arc.
        For each of the specified symbols a list of readings and attributes is added to the device. The readings are holding the parameters finally taken for the generation of the output picture. The attributes allow an easy change of the readings (see details in the description below). Once at least one symbol is specified, the set option removeobject is added.
      • textreading
        This option allows to specify a device:reading as trigger for adding texts to the template picture at any position.
        The value must be given in the form: device:reading#x#y#size#angle#color#font#linegap#blockwidth
        where:
        device:reading specifies a device (and potentially a reading otherwise state will be taken as reading) that should contain the text. This is the only mandatory part of the value. When the internal parameter INTERVAL ist set to 0 (at definition time or through the attribute interval) the change of the reading will directly trigger an update of the result picture and an upload to the device.
        x is the x-position of the lower left corner of the text relative to the lower left corner of the display area
        y is the y-position of the lower left corner of the text relative to the lower left corner of the display area
        size is the size of the text in pixels. This parameter is only used if a TTF font type is specified (see below)
        angle is the rotation angle (counterclockwise) that should be used when drawing the text. In case of not using TTF fonts (see below), the resulting rotation can be only 0 or 90 degrees.
        color is an RGB hex string that specifies the RGB values of the color respectively (e.g. 00FF00 for green)
        font is either one of small medium large giant or a path to a valid TTF font file in the FHEM file system.
        linegap The distance between consecutive lines if blocksetting is used (see blockwidth below) positive values increase the distance negative values reduce the distance.
        blockwidth The width in pixels of the text block generated by the text string. If the string width on the display is higher than the given width in pixels, an automatic linebreak will be added.
        For each of the specified texts a list of readings and attributes is added to the device. The readings are holding the parameters finally taken for the generation of the output picture. The attributes allow an easy change of the readings (see details in the description below). Once at least one text is specified, the set option removeobject is added.
      • iconreading
        This option allows to specify a device:reading as trigger for adding icons to the template picture at any position.
        The value must be given in the form: device:reading#x#y#size#angle#color
        where:
        device:reading specifies a device (and potentially a reading otherwise state will be taken as reading) that should contain the icons names (see possible icon names at "addicon" option). This is the only mandatory part of the value. When the internal parameter INTERVAL ist set to 0 (at definition time or through the attribute interval) the change of the reading will directly trigger an update of the result picture and an upload to the device.
        x is the x-position of the lower left corner of the icon relative to the lower left corner of the display area
        y is the y-position of the lower left corner of the icon relative to the lower left corner of the display area
        size is the size of the icon in pixels. This parameter is only used if a TTF font type is specified (see below)
        angle is the rotation angle (counterclockwise) that should be used when drawing the icon. In case of not using TTF fonts (see below), the resulting rotation can be only 0 or 90 degrees.
        color is an RGB hex string that specifies the RGB values of the color respectively (e.g. 00FF00 for green)
        font is either one of small medium large giant or a path to a valid TTF font file in the FHEM file system.
        For each of the specified icons a list of readings and attributes is added to the device. The readings are holding the parameters finally taken for the generation of the output picture. The attributes allow an easy change of the readings (see details in the description below). Once at least one icon is specified, the set option removeobject is added.
      • convert
        With this option the conversion can be triggered. Whenever a conversion is triggered, the resulting picture is added as reading result_picture to the device (or an existing reading is changed). Conversion is done "non-blocking" in the backround so that FHEM remains accessible.
      • removeobject
        With this option existing text definitions can be removed. The value for this option is a comma separated list of integer numbers refering to the index of the respective texts to be deleted. This option is only available if at least one text has been added to the device with set addtext or set addicon.
      • opload
        Uploads the current version of the result picture (result from most recent conversion step) to the eInk display device for display.

    Get
      get <name> <option>

      Currently the only supported get option is get devices. This will display a list of all available device types in a popup window. the get command.

    Readings
      The following readings are generated by the module:

      Readings:
      • source_picture
        Displays the given picture template specified at the definition of a device or given by attribute picturefile.
      • result_picture
        Displays the result of the most recent conversion once set convert has been issued the first time.
      • deftexts
        The number of text attributes defined to be added to the template picture when doing the conversion.
      • [0-n]-text
        The text specified for a given text object. [0-n] is an integer number and refers to the index of the text.
      • [0-n]-icon
        The icon specified for a given icon object. [0-n] is an integer number and refers to the index of the icon.
      • [0-n]-x
        The x-position of the lower left corner of the text relative to the lower left corner of the display area.
      • [0-n]-y
        The y-position of the lower left corner of the text relative to the lower left corner of the display area.
      • [0-n]-size
        The size of the text in pixels. This parameter is only used if a TTF font type is specified (see below).
      • [0-n]-angle
        The rotation angle (degrees counterclockwise) that will be used when drawing the text. In case of not using TTF fonts (see below), the resulting rotation can be only 0 or 90 degrees.
      • [0-n]-color
        An RGB hex string that tells the respective RGB values of the color used for displaying the text (e.g. 00FF00 for green).
      • [0-n]-font
        Either one of small medium large giant or a path to a valid TTF font file in the FHEM file system specifying the font to be used when displaying the text.
      • [0-n]-linegap
        The distance between consecutive lines if blocksetting is used (see blockwidth below) positive values increase the distance negative values reduce the distance
      • [0-n]-blockwidth
        The width in pixels of the text block generated by the text string. If the string width on the display is higher than the given width in pixels, an automatic linebreak will be added
      • [0-n]-trigger
        The triggering condition of the object if the object is related to areading (see set textreading and set iconreading).
      • [0-n]-def
        The initial definition of the object. This is used to reset to inital settings for color, font etc. when attributes are removed.
      • [0-n]-isIcon
        Internal setting for marking entry as icon or text.

    Attributes
      attr <name> <attribute> <value>

      See commandref#attr for more info about the attr command. In addition to the standard attributes the following device specific attributes are existing.

      Attributes:
      • picturefile
        Set the picture template. This parameter must be a path to a readable file in the filesystem of FHEM
      • url
        The url of the WLAN driver board the eInk display is connected to (typically an IP address but could also be a Web-URL)
      • definition
        This is an alternative to using the set addtext/addicon/addsymbol/textreadin/iconreading command. The attribute contains a list of defintions separated by newline. The syntax of the defintion is the same as in the reading [0-n]-def. It contains the command (addtext or addicon or addsymbol or textreading or iconreading) followed by '#' and then the same defintion values as statet in set (addtext ...) above. Lines starting with a # are ignored and treated as comment lines. Example: addtext#Hello#10#10#12#0#000000#giant
      • definitionFile
        This is another alternative to using the set addtext/addicon/addsymbol/textreadin/iconreading command. The attribute holdes the name of a text file containing a list of defintions separated by newline. The syntax of the defintion is the same as in the reading [0-n]-def. It contains the command (addtext or addicon or addsymbol or textreading or iconreading) followed by '#' and then the same defintion values as statet in set (addtext ...) above. Lines starting with a # are ignored and treated as comment lines. Example: addtext#Hello#10#10#12#0#000000#giant
      • interval
        The time interval for regular updates of the information to the eInk display. The device automatically converts the inputs and uploads the result to the eInk display in this interval. If this value is set to 0 there will be automatical updates in case a triggering device is specified (see set textreading). Otherwise 0 means no automatic updates.
      • mininterval
        The time interval that shall at least be elapsed since the last upload. If the interval since last upload is smaller than this value, not uploads will be performed.
      • boardtype
        The type of driver board to be used. Currently ESP8266 and ESP32 driver boards are supported.
      • devicetype
        The device type to be used. Refer to get devices for a list of available devices.
      • colomode
        Can be either color or monochrome and specifies if the inputs should be converted to a fitting color picture or if the conversion should be done in monochrome format.
      • convertmode
        Can be either level or dithering. If level is specified, the conversion transforms the given colors to the ones to the closest color the display is capable of (depending on the display type this can be black, red or yellow). When dithering is specified, the conversion performs a Floyd Steinberg dithering to fit the input picture better to the capabilities of the eInk display.
      • width
        The width of the result picture. Is normally taken from the devicetype default settings. Should be only used for non predefined devices.
      • height
        The height of the result picture. Is normally taken from the devicetype default settings. Should be only used for non predefined devices.
      • x0
        An x-offset for the placement of the source picture into the result picture during conversion. Reference is the lower left corner of the result.
      • y0
        An y-offset for the placement of the source picture into the result picture during conversion. Reference is the lower left corner of the result.
      • placement
        Tells the conversion algorithm where to place the source picture in the result picture if there are different widths and heights. Can be one of top-left top-right bottom-left bottom-right.
      • scale2fit
        If set to 1 the source picture is scaled to fit into the result picture in x and y directions.
      • coloroffset
        Provides a possibility for changing the mapping of source colors to result colors. Given in the form r;g;b.
      • maxretries
        Set the maximum number of retries for sending data to the WLAN display driver of the eInk display. Whenever the sending of a certain data package is not successful, sending is repeated until succes or number of maxretries is reached. Default is 3.

      • The following attributes are only available for the defined text objects to be added to the result picture. There is one set of attributes for each defined texts starting with the respective text index before the -

      • [0-n]-text
        The text specified for a given text object. [0-n] is an integer number and refers to the index of the text.
      • [0-n]-icon
        The icon name or url specified for a given icon object. [0-n] is an integer number and refers to the index of the text.
      • [0-n]-x
        The x-position of the lower left corner of the text relative to the lower left corner of the display area.
      • [0-n]-y
        The y-position of the lower left corner of the text relative to the lower left corner of the display area.
      • [0-n]-size
        The size of the text in pixels. This parameter is only used if a TTF font type is specified (see below).
      • [0-n]-angle
        The rotation angle (degrees counterclockwise) that will be used when drawing the text. In case of not using TTF fonts (see below), the resulting rotation can be only 0 or 90 degrees.
      • [0-n]-color
        An RGB hex string that tells the respective RGB values of the color used for displaying the text (e.g. 00FF00 for green).
      • [0-n]-font
        Either one of small medium large giant or a path to a valid TTF font file in the FHEM file system specifying the font to be used when displaying the text.
      • [0-n]-linegap
        The distance between consecutive lines if blocksetting is used (see blockwidth below) positive values increase the distance negative values reduce the distance
      • [0-n]-blockwidth
        The width in pixels of the text block generated by the text string. If the string width on the display is higher than the given width in pixels, an automatic linebreak will be added
=end html =cut