From 36c7cd4e4c01dc134ae81f30079f3899f237aef9 Mon Sep 17 00:00:00 2001 From: marvin78 Date: Tue, 15 Jan 2019 06:26:31 +0000 Subject: [PATCH] todoist: initial checkin git-svn-id: https://svn.fhem.de/fhem/trunk@18263 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/98_todoist.pm | 2463 ++++++++++++++++++++++++++++++++++++++ fhem/MAINTAINER.txt | 1 + fhem/www/pgm2/todoist.js | 348 ++++++ 4 files changed, 2813 insertions(+) create mode 100644 fhem/FHEM/98_todoist.pm create mode 100644 fhem/www/pgm2/todoist.js diff --git a/fhem/CHANGED b/fhem/CHANGED index f51385e7b..47a455b24 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 98_todoist: initial release - change: 49_SSCam: generate event of every LastSnapFilename,LastSnapId, LastSnapTime if snap was called with arguments, Forum:#45671 #msg887484 diff --git a/fhem/FHEM/98_todoist.pm b/fhem/FHEM/98_todoist.pm new file mode 100644 index 000000000..e8cbcdcee --- /dev/null +++ b/fhem/FHEM/98_todoist.pm @@ -0,0 +1,2463 @@ +############################################## +# $Id$ + + +package main; + +use strict; +use warnings; +use Data::Dumper; +use JSON; +use Encode; +use Date::Parse; +use Data::UUID; + +####################### +# Global variables +my $version = "1.2.0.1"; + +my %gets = ( + "version:noArg" => "", +); + +## define variables for multi language +my %todoist_transtable_EN = ( + "check" => "Check", + "delete" => "Delete", + "refreshList" => "Refresh list", + "clearList" => "Delete all elements", + "alreadythere" => "is already on the list", + "error" => "Error", + "clearconfirm" => "Are you sure? This deletes ALL the task in this list permanently.", + "delconfirm" => "Are you sure? This deletes the task permanently.", + "gotodetail" => "Click to show detail page of todoist device", + "newentry" => "Add new task to list", + "nolistdata" => "No data for this list", + "idnotfound" => "Could not find the task", +); + +my %todoist_transtable_DE = ( + "check" => "Erledigen", + "delete" => "Löschen", + "refreshList" => "Liste aktualisieren", + "clearList" => "Alle Elemente löschen", + "alreadythere" => "befindet sich bereits auf der Liste", + "error" => "Fehler", + "clearconfirm" => "Wirklich alle Elemente löschen?", + "delconfirm" => "Task wirklich löschen?", + "gotodetail" => "Detailseite des todoist-Devices aufrufen", + "newentry" => "Neuen Eintrag zur Liste hinzufügen", + "nolistdata" => "List ist leer", + "idnotfound" => "Task konnte nicht gefundern werden", +); + +my $todoist_tt; + +sub todoist_Initialize($) { + my ($hash) = @_; + + $hash->{SetFn} = "todoist_Set"; + $hash->{GetFn} = "todoist_Get"; + $hash->{DefFn} = "todoist_Define"; + $hash->{UndefFn} = "todoist_Undefine"; + $hash->{AttrFn} = "todoist_Attr"; + $hash->{RenameFn} = "todoist_Rename"; + $hash->{CopyFn} = "todoist_Copy"; + $hash->{DeleteFn} = "todoist_Delete"; + $hash->{NotifyFn} = "todoist_Notify"; + + $hash->{FW_detailFn} = "todoist_detailFn"; + + $hash->{AttrList} = "disable:1,0 ". + "pollInterval ". + "do_not_notify ". + "sortTasks:1,0 ". + "getCompleted:1,0 ". + "showPriority:1,0 ". + "showAssignedBy:1,0 ". + "showResponsible:1,0 ". + "showIndent:1,0 ". + "showChecked:1,0 ". + "showDeleted:1,0 ". + "showOrder:1,0 ". + "hideId:1,0 ". + "autoGetUsers:1,0 ". + "avoidDuplicates:1,0 ". + "listDivider ". + "showDetailWidget:1,0 ". + "hideListIfEmpty:1,0 ". + "delDeletedLists:1,0 ". + "language:EN,DE ". + $readingFnAttributes; + + $hash->{NotifyOrderPrefix} = "64-"; + + ## renew version in reload + foreach my $d ( sort keys %{ $modules{todoist}{defptr} } ) { + my $hash = $modules{todoist}{defptr}{$d}; + $hash->{VERSION} = $version; + } + + if( !defined($todoist_tt) ){ + my @devs = devspec2array("TYPE=todoist"); + if (@devs) { + if ($devs[0]) { + # in any attribute redefinition readjust language + my $lang = AttrVal($devs[0],"language", AttrVal("global","language","EN")); + if( $lang eq "DE") { + $todoist_tt = \%todoist_transtable_DE; + } + else{ + $todoist_tt = \%todoist_transtable_EN; + } + } + } + } + + return undef; +} + +sub todoist_Define($$) { + my ($hash, $def) = @_; + my $now = time(); + my $name = $hash->{NAME}; + + if( !defined($todoist_tt) ){ + # in any attribute redefinition readjust language + my $lang = AttrVal($name,"language", AttrVal("global","language","EN")); + if( $lang eq "DE") { + $todoist_tt = \%todoist_transtable_DE; + } + else{ + $todoist_tt = \%todoist_transtable_EN; + } + } + + + my @a = split( "[ \t][ \t]*", $def ); + + if ( int(@a) < 2 ) { + my $msg = "Wrong syntax: define todoist "; + Log3 $name, 4, $msg; + return $msg; + } + + ## set internal variables + $hash->{PID}=$a[2]; + $hash->{INTERVAL}=AttrVal($name,"pollInterval",undef)?AttrVal($name,"pollInterval",undef):1800; + $hash->{VERSION}=$version; + $hash->{MID} = 'da39a3ee5e634fdss43434bf3457bdbfef95601890afd80709'; # + + $modules{todoist}{defptr}{ $hash->{MID} } = $hash; #MID for internal purposes + + ## check if Access Token is needed + my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; + my ($err, $password) = getKeyValue($index); + + $hash->{helper}{PWD_NEEDED}=1 if ($err || !$password); + + $hash->{NOTIFYDEV}= "global"; + + ## start polling + if ($init_done) { + ## at first, we delete old readings. List could have changed + CommandDeleteReading(undef, "$hash->{NAME} (T|t)ask_.*"); + CommandDeleteReading(undef, "$hash->{NAME} listText"); + ## set state + readingsSingleUpdate($hash,"state","active",1) if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name) ); + readingsSingleUpdate($hash,"state","inactive",1) if ($hash->{helper}{PWD_NEEDED} || ReadingsVal($name,"state","-") eq "-"); + ## remove timers + RemoveInternalTimer($hash,"todoist_GetTasks"); + todoist_GetTasks($hash) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); + } + + return undef; +} + +# get token from file +sub todoist_GetPwd($) { + my ($hash) = @_; + + my $name=$hash->{NAME}; + + my $pwd=""; + + my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; + my $key = getUniqueId().$index; + + my ($err, $password) = getKeyValue($index); + + if ($err) { + $hash->{helper}{PWD_NEEDED} = 1; + Log3 $name, 4, "todoist ($name): unable to read password from file: $err"; + return undef; + } + + #some decryption + if ( defined($password) ) { + if ( eval "use Digest::MD5;1" ) { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { + my $decode=chop($key); + $pwd.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + } + + return undef if ($pwd eq ""); + + return $pwd; +} + +## set error Readings and log +sub todoist_ErrorReadings($;$$) { + my ($hash,$errorLog,$errorMessage) = @_; + + my $level=2; + + if (!defined($errorLog)) { + $level=3; + $errorLog="no data"; + } + $errorMessage="no data" if (!defined($errorMessage)); + + if (defined($hash->{helper}{errorData}) && $hash->{helper}{errorData} ne "") { + $errorLog=$hash->{helper}{errorData}; + } + + if (defined($hash->{helper}{errorMessage}) && $hash->{helper}{errorMessage} ne "") { + $errorMessage=$hash->{helper}{errorMessage}; + } + + my $name = $hash->{NAME}; + + readingsBeginUpdate( $hash ); + readingsBulkUpdate( $hash,"error",$errorMessage ); + readingsBulkUpdate( $hash,"lastError",$errorMessage ); + readingsEndUpdate( $hash, 1 ); + + Log3 $name,$level, "todoist ($name): Error Message: ".$errorMessage; + Log3 $name,4, "todoist ($name): Api-Error Callback-data: ".$errorLog; + + $hash->{helper}{errorData}=""; + $hash->{helper}{errorMessage}=""; + return undef; +} + + +# update Task +sub todoist_UpdateTask($$$) { + my ($hash,$cmd, $type) = @_; + + my($a, $h) = parseParams($cmd); + + my $name=$hash->{NAME}; + + Log3 $name,5, "$name: Type: ".Dumper($type); + + my $param; + + my $pwd=""; + + my %commands=(); + + my $method; + my $taskId=0; + my $title; + + ## get Task-ID + my $tid = @$a[0]; + + ## check if ID is todoist ID (ID:.*) or title (TITLE:.*) + my @temp=split(":",$tid); + + + ## use the todoist ID + if (@temp && $temp[0] =~ /id/i) { + $taskId = int($temp[1]); + $title = $hash->{helper}{"TITLE"}{$temp[1]}; + } + ## use task content + elsif (@temp && $temp[0] =~ /title/i) { + $title = encode_utf8($temp[1]); + $taskId = $hash->{helper}{"TITLES"}{$title} if ($hash->{helper}{"TITLES"}); + } + ## use Task-Number + else { + $tid=int($tid); + $taskId=$hash->{helper}{"IDS"}{"Task_".$tid}; + $title=ReadingsVal($name,"Task_".sprintf('%03d',$tid),"-"); + } + + # error if we did not get a task id + if ($taskId == 0) { + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_ErrorDialog === \"function\") todoist_ErrorDialog('$name','$title ".$todoist_tt->{"idnotfound"}."','".$todoist_tt->{"error"}."')", "")} devspec2array("TYPE=FHEMWEB"); + todoist_ErrorReadings($hash,"Task $title could not be found for $type","task $title not found"); + return undef; + } + + # some random string for UUID + my $uuidO=Data::UUID->new; + my $uuid=$uuidO->create_str(); + + # JSON String start- and endpoint + my $commandsStart="[{"; + + my $commandsEnd="}]"; + + my $tType; + my %args=(); + + + ## if no token is needed and device is not disabled, check token and get list vom todoist + if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { + + ## get password + $pwd=todoist_GetPwd($hash); + + if ($pwd) { + Log3 $name,5, "$name: hash: ".Dumper($hash); + + ## complete a task + if ($type eq "complete") { + + # variables for the commands parameter + $tType = "item_complete"; + %args = ( + ids => '['.$taskId.']', + ); + Log3 $name,5, "$name: Args: ".Dumper(%args); + $method="POST"; + } + ## close a task + elsif ($type eq "close") { + + # variables for the commands parameter + $tType = "item_close"; + %args = ( + id => $taskId, + ); + Log3 $name,5, "$name: Args: ".Dumper(%args); + $method="POST"; + } + ## uncomplete a task + elsif ($type eq "uncomplete") { + + # variables for the commands parameter + $tType = "item_uncomplete"; + %args = ( + ids => '['.$taskId.']', + ); + Log3 $name,5, "$name: Args: ".Dumper(%args); + $method="POST"; + } + ## update a task + elsif ($type eq "update") { + $tType = "item_update"; + %args = ( + id => $taskId, + ); + + ## change title + $args{'content'} = $h->{"title"} if($h->{'title'}); + ## change dueDate + $args{'date_string'} = $h->{"dueDate"} if($h->{'dueDate'}); + $args{'date_string'} = "" if ($h->{'dueDate'} && $h->{'dueDate'} =~ /(null|none|nix|leer|del)/); + ## change dueDate (if someone uses due_date in stead of dueDate) + $args{'date_string'} = $h->{"due_date"} if ($h->{'due_date'}); + $args{'date_string'} = "" if ($h->{'dueDate'} && $h->{'due_date'} =~ /(null|none|nix|leer|del)/); + ## change priority + $args{'priority'} = int($h->{"priority"}) if ($h->{"priority"}); + ## Who is responsible for the task + $args{'responsible_uid'} = $h->{"responsibleUid"} if ($h->{"responsibleUid"}); + $args{'responsible_uid'} = $h->{"responsible"} if ($h->{"responsible"}); + ## who assigned the task? + $args{'assigned_by_uid'} = $h->{"assignedByUid"} if ($h->{"assignedByUid"}); + $args{'assigned_by_uid'} = $h->{"assignedBy"} if ($h->{"assignedByUid"}); + ## order of the task + $args{'item_order'} = $h->{"order"} if ($h->{"order"}); + ## indent of the task + $args{'indent'} = $h->{"indent"} if ($h->{"indent"}); + ## parent_id + $args{'parent_id'} = $h->{"parent_id"} if ($h->{"parent_id"}); + $args{'parent_id'} = $h->{"parentID"} if ($h->{"parentID"}); + $args{'parent_id'} = $h->{"parentId"} if ($h->{"parentId"}); + + if ($args{'parent_id'}) { + my $pid=$args{'parent_id'}; + if ($hash->{helper}{"INDENT"}{$pid}) { + $args{'indent'} = int($hash->{helper}{"INDENT"}{$pid})+1; + } + } + + $args{'parent_id'} = "" if ($h->{"indent"}==1); + + ## remove attribute + if ($h->{"remove"}) { + my @temp; + my @rem = split(",",$h->{"remove"}); + foreach my $r (@rem) { + $args{'date_string'} = "" if ($r eq "dueDate" || $r eq "due_date"); + $args{'responsible_uid'} = "" if ($r eq "responsibleUid" || $r eq "responsible"); + $args{'assigned_by_uid'} = 0 if ($r eq "assignedByUid" || $r eq "assignedBy"); + if ($r eq "parent_id" || $r eq "parentID" || $r eq "parentId" || $r eq "indent") { + $args{'indent'} = 1; + $args{'parent_id'} = ""; + } + } + ## Debug + #Log3 $name, 1, "wunderlist ($name): Debug: ".Dumper($datas{'remove'}); + } + + ## Debug + #Log3 $name, 1, "todoist ($name): Debug: ".Dumper(%datas); + + $method="POST"; + } + ## delete a task + elsif ($type eq "delete") { + $tType = "item_delete"; + %args = ( + ids => '['.$taskId.']', + ); + $method="POST"; + } + else { + return undef; + } + + Log3 $name,5, "todoist ($name): Data Array sent to todoist API: ".Dumper(%args); + + my $dataArr=$commandsStart.'"type":"'.$tType.'","temp_id":"'.$taskId.'","uuid":"'.$uuid.'","args":'.encode_json(\%args).$commandsEnd; + + Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".$dataArr; + + my $data= { + token => $pwd, + commands => $dataArr + }; + + Log3 $name,4, "todoist ($name): JSON sent to todoist API: ".Dumper($data); + + $param = { + url => "https://todoist.com/api/v7/sync", + data => $data, + tTitle => $title, + method => $method, + wType => $type, + taskId => $taskId, + timeout => 7, + header => "Content-Type: application/x-www-form-urlencoded", + hash => $hash, + callback => \&todoist_HandleTaskCallback, ## call callback sub to work with the data we get + }; + + Log3 $name,5, "todoist ($name): Param: ".Dumper($param); + + ## non-blocking access to todoist API + InternalTimer(gettimeofday()+0.1, "HttpUtils_NonblockingGet", $param, 0); + } + else { + todoist_ErrorReadings($hash,"access token empty"); + } + } + else { + if (!IsDisabled($name)) { + todoist_ErrorReadings($hash,"no access token set"); + } + else { + todoist_ErrorReadings($hash,"device is disabled"); + } + + } + + return undef; +} + +# create Task +sub todoist_CreateTask($$) { + my ($hash,$cmd) = @_; + + my($a, $h) = parseParams($cmd); + + my $name=$hash->{NAME}; + + my $param; + + my $pwd=""; + + my $assigne_id=""; + + ## we try to send a due_date (in developement) + my @tmp = split( ":", join(" ",@$a) ); + + my $title=encode_utf8($tmp[0]); + + my $check=1; + + # we can avoid duplicates in FHEM. There can still be duplicates coming from another app + if (AttrVal($name,"avoidDuplicates",0) == 1 && todoist_inArray(\@{$hash->{helper}{"TITS"}},$title)) { + $check=-1; + } + + if ($check==1) { + + ## if no token is needed and device is not disabled, check token and get list vom todoist + if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { + + ## get password + $pwd=todoist_GetPwd($hash); + + if ($pwd) { + + Log3 $name,5, "$name: hash: ".Dumper($hash); + + # data array for API - we could transfer more data + my $data = { + project_id => int($hash->{PID}), + content => $title, + token => $pwd, + }; + + ## check for dueDate as Parameter or part of title - push to hash + if (!$tmp[1] && $h->{"dueDate"}) { ## parameter + $data->{'date_string'} = $h->{"dueDate"}; + } + elsif ($tmp[1]) { ## title + $data->{'date_string'} = $tmp[1]; + } + else { + + } + + ## if someone uses due_date - no problem + $data->{'date_string'} = $h->{"due_date"} if ($h->{"due_date"}); + + ## Task parent_id + $data->{'parent_id'} = int($h->{"parent_id"}) if ($h->{"parent_id"}); + $data->{'parent_id'} = int($h->{"parentID"}) if ($h->{"parentID"}); + $data->{'parent_id'} = int($h->{"parentId"}) if ($h->{"parentId"}); + + my $parentId = 0; + $parentId = $data->{'parent_id'} if ($data->{'parent_id'}); + + ## Task priority + $data->{'priority'} = $h->{"priority"} if ($h->{"priority"}); + + ## who is responsible for the task? + $data->{'responsible_uid'} = $h->{"responsibleUid"} if ($h->{"responsibleUid"}); + $data->{'responsible_uid'} = $h->{"responsible"} if ($h->{"responsible"}); + + ## who assigned the task? + $data->{'assigned_by_uid'} = $h->{"assignedByUid"} if ($h->{"assignedByUid"}); + $data->{'assigned_by_uid'} = $h->{"assignedBy"} if ($h->{"assignedByUid"}); + + ## order of the task + $data->{'item_order'} = $h->{"order"} if ($h->{"order"}); + + ## indent of the task + $data->{'indent'} = $h->{"indent"} if ($h->{"indent"}); + + + + Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".Dumper($data); + + + $param = { + url => "https://todoist.com/api/v7/items/add", + data => $data, + tTitle => $title, + method => "POST", + wType => "create", + parentId => $parentId, + timeout => 7, + header => "Content-Type: application/x-www-form-urlencoded", + hash => $hash, + callback => \&todoist_HandleTaskCallback, ## call callback sub to work with the data we get + }; + + Log3 $name,5, "todoist ($name): Param: ".Dumper($param); + + ## non-blocking access to todoist API + InternalTimer(gettimeofday()+0.1, "HttpUtils_NonblockingGet", $param, 0); + } + else { + todoist_ErrorReadings($hash,"access token empty"); + } + } + else { + if (!IsDisabled($name)) { + todoist_ErrorReadings($hash,"no access token set"); + } + else { + todoist_ErrorReadings($hash,"device is disabled"); + } + } + } + else { + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_ErrorDialog === \"function\") todoist_ErrorDialog('$name','$title ".$todoist_tt->{"alreadythere"}."','".$todoist_tt->{"error"}."')", "")} devspec2array("TYPE=FHEMWEB"); + todoist_ErrorReadings($hash,"duplicate detected","duplicate detected"); + } + + + return undef; +} + +# handle the callback data if task was created or updated +sub todoist_HandleTaskCallback($$$){ + my ($param, $err, $data) = @_; + + my $hash = $param->{hash}; + my $title = $param->{tTitle}; + + my $taskId = 0; + $taskId = $param->{taskId} if ($param->{taskId}); + + my $reading = $title; + + my $name = $hash->{NAME}; + + Log3 $name,4, "todoist ($name): ".$param->{wType}."Task Callback data: ".Dumper($data); + + my $error; + + ## errors? Log and readings + if ($err ne "") { + todoist_ErrorReadings($hash,$err); + } + else { + + ## if "sync_status" in $data, we were successfull + if((($data =~ /sync_status/ && $data=~/ok/) || $data =~ /sync_id/) && eval {decode_json($data)}) { + + readingsBeginUpdate($hash); + + if ($data ne "") { + my $decoded_json = decode_json($data); + + $taskId = $decoded_json->{id} if ($decoded_json->{id}); + + $reading .= " - ".$taskId; + + ## do some logging + Log3 $name,5, "todoist ($name): Task Callback data (decoded JSON): ".Dumper($decoded_json ); + + Log3 $name,4, "todoist ($name): Callback-ID: $taskId" if ($taskId); + } + Log3 $name,4, "todoist ($name): Task Callback error(s): ".Dumper($err) if ($err); + Log3 $name,5, "todoist ($name): Task Callback param: ".Dumper($param); + + # set information readings + readingsBulkUpdate($hash, "error","none"); + readingsBulkUpdate($hash, "lastCreatedTask",$reading) if ($param->{wType} eq "create"); + readingsBulkUpdate($hash, "lastCompletedTask",$reading) if ($param->{wType} eq "complete" || $param->{wType} eq "close"); + readingsBulkUpdate($hash, "lastUncompletedTask",$reading) if ($param->{wType} eq "uncomplete"); + readingsBulkUpdate($hash, "lastUpdatedTask",$reading) if ($param->{wType} eq "update"); + readingsBulkUpdate($hash, "lastDeletedTask",$reading) if ($param->{wType} eq "delete"); + + ## some Logging + Log3 $name, 4, "todoist ($name): successfully created new task $title" if ($param->{wType} eq "create"); + Log3 $name, 4, "todoist ($name): success: ".$param->{wType}." task $title" if ($title); + + readingsEndUpdate( $hash, 1 ); + + if ($param->{wType} =~ /(complete|delete|close)/) { + # remove line in possible webling widget + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_removeLine === \"function\") todoist_removeLine('$name','$taskId')", "")} devspec2array("TYPE=FHEMWEB"); + } + if ($param->{wType} eq "create") { + if ($param->{parentId}) { + # set parent id with additional updateTask command / API cannot add it in create + CommandSet(undef, "$name updateTask ID:$taskId parent_id=".$param->{parentId}); + Log3 $name, 3, "todoist ($name): startet set parent_id over update after create: Task-ID: ".$taskId." - parent_id: ".$param->{parentId}; + } + # add a line in possible weblink widget + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_addLine === \"function\") todoist_addLine('$name','$taskId','$title')", "")} devspec2array("TYPE=FHEMWEB"); + } + } + ## we got an error from the API + else { + my $error="malformed JSON"; + + # if the error is in the file, log this error + if (eval {decode_json($data)}) { + my $decoded_json = decode_json($data); + $error = $decoded_json->{error} if ($decoded_json->{error}); + } + $error .= " | ".$param->{wType}."Task: ".$taskId; + $error .= "Unknown"; + Log3 $name, 2, "todoist ($name): got error: ".$error; + todoist_ErrorReadings($hash,$error); + } + + } + # restart the timers + todoist_RestartGetTimer($hash); + + return undef; +} + + + +## get all Tasks +sub todoist_GetTasks($;$) { + my ($hash,$completed) = @_; + + my $name=$hash->{NAME}; + + # add loading circle to possivle weblink widget + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_addLoading === \"function\") todoist_addLoading('$name')", "")} devspec2array("TYPE=FHEMWEB"); + + $completed = 0 unless defined($completed); + + my $param; + my $param2; + + my $pwd=""; + + ## if no token is needed and device is not disabled, check token and get list vom todoist + if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { + + ## get password + $pwd=todoist_GetPwd($hash); + + if ($pwd) { + + Log3 $name,5, "$name: hash: ".Dumper($hash); + + my $data= { + token => $pwd, + project_id => $hash->{PID} + }; + + # set url for API access + my $url = "https://todoist.com/api/v7/projects/get_data"; + ## check if we get also the completed Tasks + if ($completed == 1) { + $url = "https://todoist.com/api/v7/completed/get_all"; + $data->{'limit'}=50; + } + + Log3 $name,4, "todoist ($name): Curl Data: ".Dumper($data); + + ## get the tasks + $param = { + url => $url, + method => "POST", + data => $data, + header => "Content-Type: application/x-www-form-urlencoded", + timeout => 7, + completed => $completed, + hash => $hash, + callback => \&todoist_GetTasksCallback, ## call callback sub to work with the data we get + }; + + + + Log3 $name,5, "todoist ($name): Param: ".Dumper($param); + + ## non-blocking access to todoist API + InternalTimer(gettimeofday()+0.2, "HttpUtils_NonblockingGet", $param, 0); + + + } + else { + todoist_ErrorReadings($hash,"access token empty"); + } + } + else { + if (!IsDisabled($name)) { + todoist_ErrorReadings($hash,"no access token set"); + } + else { + todoist_ErrorReadings($hash,"device is disabled"); + } + } + + ## one more time, if we want to get completed tasks + if (AttrVal($name,"getCompleted",0)==1 && $completed != 1) { + InternalTimer(gettimeofday()+0.5, "todoist_doGetCompTasks", $hash, 0); + } + InternalTimer(gettimeofday()+2, "todoist_GetUsers", $hash, 0) if ($completed != 1 && AttrVal($name,"autoGetUsers",1) == 1); + + return undef; +} + +# helper sub for getting completed tasks +sub todoist_doGetCompTasks($) { + my ($hash) = @_; + todoist_GetTasks($hash,1); +} + +## Callback for the lists tasks +sub todoist_GetTasksCallback($$$){ + my ($param, $err, $data) = @_; + + my $hash=$param->{hash}; + + my $name = $hash->{NAME}; + + Log3 $name,5, "todoist ($name): Task Callback data-raw: ".Dumper($data); + + my $lText=""; + + Log3 $name,5, "todoist ($name): Task Callback param: ".Dumper($param); + + readingsBeginUpdate($hash); + + ## Log possbile errors in callback + if ($err ne "") { + todoist_ErrorReadings($hash,$err); + } + else { + my $decoded_json=""; + + # checl for correct JSON + if (eval{decode_json($data)}) { + + $decoded_json = decode_json($data); + + Log3 $name,5, "todoist ($name): Task Callback data (decoded JSON): ".Dumper($decoded_json ); + } + + # mostly HTML response / todoist is down or locked + if ((ref($decoded_json) eq "HASH" && !$decoded_json->{items}) || $decoded_json eq "") { + $hash->{helper}{errorData} = Dumper($data); + $hash->{helper}{errorMessage} = "GetTasks: Response was damaged or empty. See log for details."; + InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); + } + # got project + else { + Log3 $name,4, "todoist ($name): getTasks was successful"; + Log3 $name,5, "todoist ($name): Task item data: ".Dumper(@{$decoded_json->{items}}); + ## items data + my @taskseries = @{$decoded_json->{items}}; + + ## project data + my $project = $decoded_json->{project}; + + # set some internals (project data) + if ($project) { + $hash->{PROJECT_NAME}=$project->{name}; + $hash->{PROJECT_INDENT}=$project->{indent}; + $hash->{PROJECT_COLOR}=$project->{color}; + $hash->{PROJECT_ORDER}=$project->{item_order}; + if ($project->{user_id}) { + $hash->{PROJECT_USER}=$project->{user_id}; + } + else { + delete($hash->{PROJECT_USER}); + } + if ($project->{parent_id}) { + $hash->{PROJECT_PARENT}=$project->{parent_id}; + # hidden reading + readingsBulkUpdate($hash, ".projectParentId",$project->{parent_id}); + } + else { + # delete parent_id if there is none + delete($hash->{PROJECT_PARENT}) if (defined($hash->{PROJECT_PARENT})); + CommandDeleteReading(undef, ".projectParentId"); + } + } + + ## do some logging + Log3 $name,5, "todoist ($name): Task Callback data (taskseries): ".Dumper(@taskseries ); + + my $i=0; + + ## count the results + my $count=@taskseries; + + ## delete Task_* readings for changed list + if ($param->{completed} != 1 || (ReadingsVal($name,"count",0)==0 && $count == 0)) { + CommandDeleteReading(undef, "$hash->{NAME} (T|t)ask_.*"); + delete($hash->{helper}); + } + + + + ## no data + if ($count==0 && $param->{completed} != 1) { + InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); + readingsBulkUpdate($hash, "count",0); + } + # got some tasks + else { + $i = ReadingsVal($name,"count",0) if ($param->{completed} == 1); + foreach my $task (@taskseries) { + my $title = encode_utf8($task->{content}); + $title =~ s/^\s+|\s+$//g; + + my $t = sprintf ('%03d',$i); + + ## get todoist-Task-ID + my $taskID = $task->{id}; + + readingsBulkUpdate($hash, "Task_".$t,$title); + readingsBulkUpdate($hash, "Task_".$t."_ID",$taskID) if (AttrVal($name,"hideId",0)!=1); + + ## a few helper for ID and revision + $hash->{helper}{"IDS"}{"Task_".$i}=$taskID; # todoist Task-ID + $hash->{helper}{"TITLE"}{$taskID}=$title; # Task title (content) + $hash->{helper}{"TITLES"}{$title}=$taskID; # Task title (content) + $hash->{helper}{"WID"}{$taskID}=$i; # FHEM Task-ID + $hash->{helper}{"INDENT"}{$taskID}=$task->{indent}; # todoist Task indent + $hash->{helper}{"PRIORITY"}{$taskID}=$task->{priority}; # todoist Task indent + push @{$hash->{helper}{"INDENTS"}{$task->{indent}}},$taskID; # ident for better widget + $hash->{helper}{"ORDER"}{$taskID}=$task->{item_order}; # todoist Task order + push @{$hash->{helper}{"TIDS"}},$taskID; # simple ID list + push @{$hash->{helper}{"TITS"}},$title; # simple ID list + + readingsBulkUpdate($hash, "Task_".$t."_indent",$task->{indent}) if (AttrVal($name,"showIndent",0)==1); + readingsBulkUpdate($hash, "Task_".$t."_order",$task->{item_order}) if (AttrVal($name,"showOrder",0)==1); + + ## set parent_id if not null + if (defined($task->{parent_id}) && $task->{parent_id} ne 'null') { + ## if this task has a parent_id we set the reading + readingsBulkUpdate($hash, "Task_".$t."_parentID",$task->{parent_id}); + $hash->{helper}{"PARENT_ID"}{$taskID}=$task->{parent_id}; + } + + ## set completed_date if present + if (defined($task->{checked}) && $task->{checked}!=0) { + readingsBulkUpdate($hash, "Task_".$t."_checked",$task->{checked}) if (AttrVal($name,"showChecked",1)==1); + $hash->{helper}{"CHECKED"}{$taskID}=$task->{checked}; + } + + ## set completed_date if present + if (defined($task->{is_deleted}) && $task->{is_deleted}!=0) { + readingsBulkUpdate($hash, "Task_".$t."_isDeleted",$task->{is_deleted}) if (AttrVal($name,"showDeleted",1)==1); + $hash->{helper}{"ISDELETED"}{$taskID}=$task->{is_deleted}; + } + + ## set completed_date if present + if (defined($task->{completed_date})) { + ## if there is a completed task, we create a new reading + readingsBulkUpdate($hash, "Task_".$t."_completedAt",FmtDateTime(str2time($task->{completed_date}))); + $hash->{helper}{"COMPLETED_AT"}{$taskID}=FmtDateTime(str2time($task->{completed_date})); + readingsBulkUpdate($hash, "Task_".$t."_completedById",$task->{user_id}); + $hash->{helper}{"COMPLETED_BY_ID"}{$taskID}=$task->{user_id}; + } + + ## set due_date if present + if (defined($task->{due_date_utc}) && $task->{due_date_utc} ne 'null') { + ## if there is a task with due date, we create a new reading + readingsBulkUpdate($hash, "Task_".$t."_dueDate",FmtDateTime(str2time($task->{due_date_utc}))); + $hash->{helper}{"DUE_DATE"}{$taskID}=FmtDateTime(str2time($task->{due_date_utc})); + } + + ## set responsible_uid if present + if (defined($task->{responsible_uid})) { + ## if there is a task with responsible_uid, we create a new reading + readingsBulkUpdate($hash, "Task_".$t."_responsibleUid",$task->{responsible_uid}) if (AttrVal($name,"showResponsible",0)==1); + $hash->{helper}{"RESPONSIBLE_UID"}{$taskID}=$task->{responsible_uid}; + } + + ## set assigned_by_uid if present + if (defined($task->{assigned_by_uid})) { + ## if there is a task with assigned_by_uid, we create a new reading + readingsBulkUpdate($hash, "Task_".$t."_assignedByUid",$task->{assigned_by_uid}) if (AttrVal($name,"showAssignedBy",0)==1); + $hash->{helper}{"ASSIGNEDBY_UID"}{$taskID}=$task->{assigned_by_uid}; + } + + ## set priority if present + if (defined($task->{priority})) { + readingsBulkUpdate($hash, "Task_".$t."_priority",$task->{priority}) if (AttrVal($name,"showPriority",0)==1); + $hash->{helper}{"PRIORITY"}{$taskID}=$task->{priority}; + } + + ## set recurrence_type and count if present + if (defined($task->{date_string})) { + ## if there is a task with recurrence_type, we create new readings + readingsBulkUpdate($hash, "Task_".$t."_recurrenceType",encode_utf8($task->{date_string})); + $hash->{helper}{"RECURRENCE_TYPE"}{$taskID}=encode_utf8($task->{date_string}); + } + + if ($param->{completed} != 1) { + $lText.=AttrVal($name,"listDivider",", ") if ($i != 0); + $lText.=$title; + } + $i++; + } + readingsBulkUpdate($hash, "error","none"); + readingsBulkUpdate($hash, "count",$i); + + + } + } + } + + ## list Text for TTS, Text-Message... + if ($param->{completed} != 1) { + $lText="-" if ($lText eq ""); + readingsBulkUpdate($hash,"listText",$lText) if ($lText ne ""); + } + + + readingsEndUpdate( $hash, 1 ); + + ## sort Tasks alphabetically if set + todoist_sort($hash) if (AttrVal($name,"sortTasks",0) == 1); + + + RemoveInternalTimer($hash,"todoist_GetTasks"); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "todoist_GetTasks", $hash, 0); ## loop with Interval + + todoist_ReloadTable($name); + + return undef; +} + +## get all Users +sub todoist_GetUsers($) { + my ($hash) = @_; + + my $name=$hash->{NAME}; + + my $param; + + my $pwd=""; + + ## if no token is needed and device is not disabled, check token and get list vom todoist + if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { + + ## get password + $pwd=todoist_GetPwd($hash); + + my $data= { + token => $pwd, + sync_token => '*', + resource_types => '["collaborators"]' + }; + + if ($pwd) { + + Log3 $name,5, "$name: hash: ".Dumper($hash); + + $param = { + url => "https://todoist.com/api/v7/sync", + data => $data, + timeout => 7, + method => "POST", + header => "Content-Type: application/x-www-form-urlencoded", + hash => $hash, + callback => \&todoist_GetUsersCallback, ## call callback sub to work with the data we get + }; + + + Log3 $name,5, "todoist ($name): Param: ".Dumper($param); + + ## non-blocking access to todoist API + InternalTimer(gettimeofday()+1, "HttpUtils_NonblockingGet", $param, 0); + } + else { + todoist_ErrorReadings($hash,"access token empty"); + } + } + else { + if (!IsDisabled($name)) { + todoist_ErrorReadings($hash,"no access token set"); + } + else { + todoist_ErrorReadings($hash,"device is disabled"); + } + } + + return undef; +} + +sub todoist_GetUsersCallback($$$){ + my ($param, $err, $data) = @_; + + my $hash=$param->{hash}; + + my $name = $hash->{NAME}; + + Log3 $name,5, "todoist ($name): User Callback data: ".Dumper($data); + + if ($err ne "") { + todoist_ErrorReadings($hash,$err); + } + else { + my $decoded_json=""; + + if (eval{decode_json($data)}) { + + $decoded_json = decode_json($data); + + Log3 $name,5, "todoist ($name): User Callback data (decoded JSON): ".Dumper($decoded_json ); + } + + readingsBeginUpdate($hash); + if ((ref($decoded_json) eq "HASH" && !$decoded_json->{collaborators}) || $decoded_json eq "") { + $hash->{helper}{errorData} = Dumper($data); + $hash->{helper}{errorMessage} = "Response was damaged or empty. See log for details."; + InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); + } + else { + my @users = @{$decoded_json->{collaborators}}; + my @states = @{$decoded_json->{collaborator_states}}; + ## count the results + my $count=@users; + + ## delete Task_* readings for changed list + CommandDeleteReading(undef, "$hash->{NAME} (U|u)ser_.*"); + delete($hash->{helper}{USER}); + + Log3 $name,5, "todoist ($name): Task States: ".Dumper(@states); + + ## no data + if ($count==0) { + readingsBulkUpdate($hash, "error","none"); + readingsBulkUpdate($hash, "countUsers",0); + } + else { + + my $i=0; + foreach my $user (@users) { + my $do=0; + foreach my $state (@states) { + $do=1 if ($user->{id} == $state->{user_id} && $state->{project_id} == $hash->{PID}); + } + + if ($do==1) { + my $userName = encode_utf8($user->{full_name}); + my $t = sprintf ('%03d',$i); + + ## get todoist-User-ID + my $userID = $user->{id}; + + readingsBulkUpdate($hash, "User_".$t,$userName); + readingsBulkUpdate($hash, "User_".$t."_ID",$userID); + + ## a few helper for ID and revision + $hash->{helper}{USER}{"IDS"}{"User_".$i}=$userID; + $hash->{helper}{USER}{"NAME"}{$userID}=$userName; + $hash->{helper}{USER}{"WID"}{$userID}=$i; + $i++; + } + } + readingsBulkUpdate($hash, "error","none"); + readingsBulkUpdate($hash, "countUsers",$i); + } + } + readingsEndUpdate( $hash, 1 ); + } + + +} + +# get all projects +sub todoist_GetProjects($) { + my ($hash) = @_; + + my $name=$hash->{NAME}; + + my $param; + + my $pwd=""; + + ## if no token is needed and device is not disabled, check token and get list vom todoist + if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { + + ## get password + $pwd=todoist_GetPwd($hash); + + my $data= { + token => $pwd, + sync_token => '*', + resource_types => '["projects"]' + }; + + if ($pwd) { + + Log3 $name,5, "$name: hash: ".Dumper($hash); + + $param = { + url => "https://todoist.com/api/v7/sync", + data => $data, + timeout => 7, + method => "POST", + header => "Content-Type: application/x-www-form-urlencoded", + hash => $hash, + callback => \&todoist_GetProjectsCallback, ## call callback sub to work with the data we get + }; + + + Log3 $name,5, "todoist ($name): Param: ".Dumper($param); + + ## non-blocking access to todoist API + InternalTimer(gettimeofday()+1, "HttpUtils_NonblockingGet", $param, 0); + } + else { + todoist_ErrorReadings($hash,"access token empty"); + } + } + else { + if (!IsDisabled($name)) { + todoist_ErrorReadings($hash,"no access token set"); + } + else { + todoist_ErrorReadings($hash,"device is disabled"); + } + } + + return undef; + +} + +# get projects callback + +sub todoist_GetProjectsCallback($$$){ + my ($param, $err, $data) = @_; + + my $hash=$param->{hash}; + + my $name = $hash->{NAME}; + + ## some Project-Info + my $pid = $hash->{PID}; + my $room = AttrVal($name,"room",undef); + my $group = AttrVal($name,"group",undef); + + my $return=""; + + my $i=0; + + Log3 $name,5, "todoist ($name): Projects Callback data: ".Dumper($data); + + readingsBeginUpdate($hash); + + if ($err ne "") { + todoist_ErrorReadings($hash,$err); + } + else { + my $decoded_json=""; + + if (eval{decode_json($data)}) { + + $decoded_json = decode_json($data); + + Log3 $name,5, "todoist ($name): User Callback data (decoded JSON): ".Dumper($decoded_json ); + } + if ((ref($decoded_json) eq "HASH" && !$decoded_json->{projects}) || $decoded_json eq "") { + $hash->{helper}{errorData} = Dumper($data); + $hash->{helper}{errorMessage} = "GetTasks: Response was damaged or empty. See log for details."; + InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); + } + else { + Log3 $name,4, "todoist ($name): getProjects was successful"; + Log3 $name,5, "todoist ($name): Task projects data: ".Dumper(@{$decoded_json->{projects}}); + ## items data + my @projects = @{$decoded_json->{projects}}; + + ## count the results + my $count=@projects; + + ## no data + if ($count==0) { + InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); + readingsBulkUpdate($hash, "count",0); + } + else { + # array for possible deletion + my @comDevs; + # walk through projects + foreach my $project (@projects) { + my $project_id = $project->{id}; + my $parent_id = $project->{parent_id}; + my $new_name = encode_utf8($project->{name}); + # new title + my $title = $name."_".$new_name; + # is this parent_id equal to id of current project? + if ($pid == $parent_id) { + # push to deletion array + push @comDevs,$project_id; + Log3 $name,4, "todoist ($name): get Projects: Project-ID: $project_id - Title: $new_name - Parent-ID: $parent_id"; + # does this project exist in FHEM? + my $existP = devspec2array("PID=$project_id"); + if (!$existP && (!$project->{is_deleted} || $project->{is_deleted}!=1)) { + # if not, define new todoist device by copying the parent + fhem("copy $name $title $project_id") if (!defined($defs{$title})); + my $new_hash = $defs{$title}; + # set some internals (project data) and set parent_id + if ($new_hash) { + $return.=" $title"; + $i++; + Log3 $name,4, "todoist ($name): new project $title, defined by cChildProjects"; + $new_hash->{PROJECT_NAME}=$project->{name}; + $new_hash->{PROJECT_INDENT}=$project->{indent}; + $new_hash->{PROJECT_COLOR}=$project->{color}; + $new_hash->{PROJECT_ORDER}=$project->{item_order}; + if ($project->{user_id}) { + $new_hash->{PROJECT_USER}=$project->{user_id}; + } + else { + delete($new_hash->{PROJECT_USER}); + } + if ($project->{parent_id}) { + $new_hash->{PROJECT_PARENT}=$project->{parent_id}; + # invisible reading + readingsSingleUpdate($new_hash, ".projectParentId",$project->{parent_id},1); + } + else { + delete($new_hash->{PROJECT_PARENT}) if (defined($new_hash->{PROJECT_PARENT})); + CommandDeleteReading(undef, ".projectParentId"); + } + } + } + } + } + if (AttrVal($name,"delDeletedLists",0)==1) { + # get todoist projects for this parent_id + my @tDevs = devspec2array(".projectParentId=$pid"); + foreach my $dev (@tDevs) { + my $tDev = $defs{$dev}; + # delete device if PID is not in array + if (!todoist_inArray(\@comDevs,$tDev->{PID})) { + CommandDelete(undef,$dev); + Log3 $name,3, "todoist ($name): deleted device ".$dev." in cChildProjects"; + } + } + } + } + } + } + Log3 $name,3, "todoist ($name): Got $i new projects: ".$return; + return "Got $i new projects: ".$return; +} + +# sort alphabetically (probably deprecated) +sub todoist_sort($) { + my ($hash) = @_; + + my $name=$hash->{NAME}; + + my $lText=""; + + my %list; + + + ## get all Readings + my $readings = $hash->{READINGS}; + ## prepare Hash for sorting + foreach my $key (keys %{$readings}) { + if ($key =~ m/^Task_\d\d\d$/) { + my @temp = split("_",$key); + my $tid = int($temp[1]); + my $val = $readings->{$key}{VAL}; + my $id = $hash->{helper}{IDS}{'Task_'.$tid}; + $list{$tid} = {content => $val, ID => $id}; + } + } + + CommandDeleteReading(undef, "$hash->{NAME} (T|t)ask_.*"); + + readingsBeginUpdate($hash); + + delete($hash->{helper}{"IDS"}); + + ## sort Tasks and write them back + my $i = 0; + foreach my $key (sort {lc($list{$a}->{content}) cmp lc($list{$b}->{content})} keys(%list)) { + my $data = $list{$key}; + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i),$data->{content}); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_dueDate",$hash->{helper}{"DUE_DATE"}{$data->{ID}}) if ($hash->{helper}{"DUE_DATE"}{$data->{ID}}); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_responsibleUid",$hash->{helper}{"RESPONSIBLE_UID"}{$data->{ID}}) if ($hash->{helper}{"RESPONSIBLE_UID"}{$data->{ID}} && AttrVal($name,"showResponsible",0)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_assignedByUid",$hash->{helper}{"ASSIGNEDBY_UID"}{$data->{ID}}) if ($hash->{helper}{"ASSIGNEDBY_UID"}{$data->{ID}} && AttrVal($name,"showAssignedBy",0)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_priority",$hash->{helper}{"PRIORITY"}{$data->{ID}}) if ($hash->{helper}{"PRIORITY"}{$data->{ID}} && AttrVal($name,"showPriority",0)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_recurrenceType",$hash->{helper}{"RECURRENCE_TYPE"}{$data->{ID}}) if ($hash->{helper}{"RECURRENCE_TYPE"}{$data->{ID}}); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_completedAt",$hash->{helper}{"COMPLETED_AT"}{$data->{ID}}) if ($hash->{helper}{"COMPLETED_AT"}{$data->{ID}}); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_completedById",$hash->{helper}{"COMPLETED_BY_ID"}{$data->{ID}}) if ($hash->{helper}{"COMPLETED_BY_ID"}{$data->{ID}}); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_indent",$hash->{helper}{"INDENT"}{$data->{ID}}) if ($hash->{helper}{"INDENT"}{$data->{ID}} && AttrVal($name,"showIndent",0)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_order",$hash->{helper}{"ORDER"}{$data->{ID}}) if ($hash->{helper}{"ORDER"}{$data->{ID}} && AttrVal($name,"showOrder",0)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_parentID",$hash->{helper}{"PARENT_ID"}{$data->{ID}}) if ($hash->{helper}{"PARENT_ID"}{$data->{ID}}); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_checked",$hash->{helper}{"CHECKED"}{$data->{ID}}) if ($hash->{helper}{"CHECKED"}{$data->{ID}} && AttrVal($name,"showChecked",1)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_isDeleted",$hash->{helper}{"ISDELETED"}{$data->{ID}}) if ($hash->{helper}{"ISDELETED"}{$data->{ID}} && AttrVal($name,"showDeleted",1)==1); + readingsBulkUpdate($hash,"Task_".sprintf("%03s",$i)."_ID",$data->{ID}) if (AttrVal($name,"hideId",0)!=1); + + $hash->{helper}{"IDS"}{"Task_".$i} = $data->{ID}; + $hash->{helper}{"WID"}{$data->{ID}} = $i; + + if (!$hash->{helper}{"COMPLETED_AT"}{$data->{ID}}) { + $lText.=", " if ($i != 0); + $lText.=$data->{content}; + } + $i++; + } + + ## list Text for TTS, Text-Message... + $lText="-" if ($lText eq ""); + readingsBulkUpdate($hash,"listText",$lText) if ($lText ne ""); + + readingsEndUpdate( $hash, 1 ); + return undef; +} + + +################################################# +# delete all Tasks from list +sub todoist_clearList($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + my $i=0; + ## iterate through all tasks + foreach my $id (%{$hash->{helper}{IDS}}) { + my $dHash->{hash}=$hash; + if ($id !~ /Task_/) { + $dHash->{id}=$id; + InternalTimer(gettimeofday()+0.4, "todoist_doUpdateTask", $dHash, 0); + $i++; + } + } + if ($i==0) { + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_removeLoading === \"function\") todoist_removeLoading('$name')", "")} devspec2array("TYPE=FHEMWEB"); + } +} + +sub todoist_doUpdateTask($) { + my ($dHash) = @_; + my $hash = $dHash->{hash}; + my $id = $dHash->{id}; + my $name = $hash->{NAME}; + todoist_UpdateTask($hash,"ID:".$id,"delete"); +} + + +sub todoist_Undefine($$) { + my ($hash, $arg) = @_; + + RemoveInternalTimer($hash); + + return undef; +} + +################################################ +# If Device is deleted, delete the password data +sub todoist_Delete($$) { + my ($hash, $name) = @_; + + my $old_index = "todoist_".$name."_passwd"; + + my $old_key =getUniqueId().$old_index; + + my ($err, $old_pwd) = getKeyValue($old_index); + + return undef unless(defined($old_pwd)); + + setKeyValue($old_index, undef); + + + Log3 $name, 3, "todoist: device $name as been deleted. Access-Token has been deleted too."; +} + +################################################ +# If Device is renamed, copy the password data +sub todoist_Rename($$) { + my ($new, $old) = @_; + + my $old_index = "todoist_".$old."_passwd"; + my $new_index = "todoist_".$new."_passwd"; + my $key = getUniqueId().$old_index; + + my $new_hash = $defs{$new}; + my $name = $new_hash->{NAME}; + + my $old_key=""; + + my ($err, $password) = getKeyValue($old_index); + + if ($err) { + $new_hash->{helper}{PWD_NEEDED} = 1; + Log3 $name, 4, "todoist ($name): unable to read password from file: $err"; + return undef; + } + + if ( defined($password) ) { + if ( eval "use Digest::MD5;1" ) { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { + my $decode=chop($key); + $old_key.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + } + my $new_key = $old_key; + + + return undef unless(defined($old_key)); + + todoist_setPwd($new_hash, $name, $new_key); + + Log3 $new, 3, "todoist: device has been renamed from $old to $new. Access-Token has been assigned to new name."; +} + +################################################ +# If Device is copied, copy the password data +sub todoist_Copy($$;$) { + my ($old, $new) = @_; + + my $old_index = "todoist_".$old."_passwd"; + my $new_index = "todoist_".$new."_passwd"; + my $key = getUniqueId().$old_index; + + my $new_hash = $defs{$new}; + my $name = $new_hash->{NAME}; + + my $old_key=""; + + my ($err, $password) = getKeyValue($old_index); + + if ($err) { + $new_hash->{helper}{PWD_NEEDED} = 1; + Log3 $name, 4, "todoist ($name): unable to read password from file: $err"; + return undef; + } + + if ( defined($password) ) { + if ( eval "use Digest::MD5;1" ) { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { + my $decode=chop($key); + $old_key.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + } + my $new_key = $old_key; + + return undef unless(defined($old_key)); + + todoist_setPwd($new_hash, $name, $new_key); + + delete($new_hash->{helper}{PWD_NEEDED}); + + readingsSingleUpdate($new_hash,"state","inactive",1); + + Log3 $new, 3, "todoist: device has been copied from $old to $new. Access-Token has been assigned to new device."; +} + +## some checks if attribute is set or deleted +sub todoist_Attr($@) { + my ($cmd, $name, $attrName, $attrVal) = @_; + + my $orig = $attrVal; + + my $hash = $defs{$name}; + + if ( $attrName eq "disable" ) { + + if ( $cmd eq "set" && $attrVal == 1 ) { + if ($hash->{READINGS}{state}{VAL} ne "disabled") { + readingsSingleUpdate($hash,"state","disabled",1); + RemoveInternalTimer($hash); + RemoveInternalTimer($hash,"todoist_GetTasks"); + Log3 $name, 4, "todoist ($name): $name is now disabled"; + } + } + elsif ( $cmd eq "del" || $attrVal == 0 ) { + if ($hash->{READINGS}{state}{VAL} ne "active") { + readingsSingleUpdate($hash,"state","active",1); + RemoveInternalTimer($hash); + Log3 $name, 4, "todoist ($name): $name is now ensabled"; + todoist_RestartGetTimer($hash); + } + } + } + + if ( $attrName eq "pollInterval") { + if ( $cmd eq "set" ) { + return "$name: pollInterval has to be a number (seconds)" if ($attrVal!~ /\d+/); + return "$name: pollInterval has to be greater than or equal 20" if ($attrVal < 20); + $hash->{INTERVAL}=$attrVal; + Log3 $name, 4, "todoist ($name): set new pollInterval to $attrVal"; + } + elsif ( $cmd eq "del" ) { + $hash->{INTERVAL}=1800; + Log3 $name, 4, "todoist ($name): set new pollInterval to 1800 (standard)"; + } + todoist_RestartGetTimer($hash); + } + + if ($attrName eq "listDivider") { + todoist_RestartGetTimer($hash); + } + + if ($attrName eq "language") { + # in any attribute redefinition readjust language + if ($cmd eq "set") { + return "$name: language can only be DE or EN" if ($attrVal !~ /(^DE|EN)$/); + if( $attrVal eq "DE") { + $todoist_tt = \%todoist_transtable_DE; + } + else{ + $todoist_tt = \%todoist_transtable_EN; + } + } + else { + $todoist_tt = \%todoist_transtable_EN; + } + } + + if ( $attrName eq "sortTasks" || $attrName =~ /(show(Priority|AssignedBy|Responsible|Indent|Order|DetailWidget)|getCompleted|hide(Id|ListIfEmpty)|autoGetUsers|avoidDuplicates|delDeletedLists)/) { + if ( $cmd eq "set" ) { + return "$name: $attrName has to be 0 or 1" if ($attrVal !~ /^(0|1)$/); + Log3 $name, 4, "todoist ($name): set attribut $attrName to $attrVal"; + } + elsif ( $cmd eq "del" ) { + Log3 $name, 4, "todoist ($name): deleted attribut $attrName"; + } + todoist_RestartGetTimer($hash); + } + + return; +} + +sub todoist_Set ($@) { + my ($hash, $name, $cmd, @args) = @_; + + my @sets = (); + + push @sets, "active:noArg" if (IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); + push @sets, "inactive:noArg" if (!IsDisabled($name)); + if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}) { + push @sets, "addTask"; + push @sets, "completeTask"; + push @sets, "closeTask"; + push @sets, "uncompleteTask"; + push @sets, "deleteTask"; + push @sets, "updateTask"; + push @sets, "clearList:noArg"; + push @sets, "getTasks:noArg"; + push @sets, "cChildProjects:noArg"; + push @sets, "getUsers:noArg"; + } + push @sets, "accessToken" if ($hash->{helper}{PWD_NEEDED}); + push @sets, "newAccessToken" if (!$hash->{helper}{PWD_NEEDED}); + + return join(" ", @sets) if ($cmd eq "?"); + + my $usage = "Unknown argument ".$cmd.", choose one of ".join(" ", @sets) if(scalar @sets > 0); + + if (IsDisabled($name) && $cmd !~ /^(active|inactive|.*ccessToken)?$/) { + Log3 $name, 3, "todoist ($name): Device is disabled at set Device $cmd"; + return "Device is disabled. Enable it on order to use command ".$cmd; + } + + if ( $cmd =~ /^(active|inactive)?$/ ) { + readingsSingleUpdate($hash,"state",$cmd,1); + RemoveInternalTimer($hash,"todoist_GetTasks"); + CommandDeleteAttr(undef,"$name disable") if ($cmd eq "active" && AttrVal($name,"disable",0)==1); + InternalTimer(gettimeofday()+1, "todoist_GetTasks", $hash, 0) if (!IsDisabled($name) && $cmd eq "active"); + Log3 $name, 3, "todoist ($name): set Device $cmd"; + } + elsif ($cmd eq "getTasks") { + RemoveInternalTimer($hash,"todoist_GetTasks"); + Log3 $name, 4, "todoist ($name): set getTasks manually. Timer restartet."; + InternalTimer(gettimeofday()+1, "todoist_GetTasks", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); + } + elsif ($cmd eq "cChildProjects") { + RemoveInternalTimer($hash,"todoist_GetProjects"); + Log3 $name, 4, "todoist ($name): set getProjects manually. Timer restartet."; + InternalTimer(gettimeofday()+1, "todoist_GetProjects", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); + } + elsif ($cmd eq "getUsers") { + RemoveInternalTimer($hash,"todoist_GetUsers"); + Log3 $name, 4, "todoist ($name): set getUsers manually."; + InternalTimer(gettimeofday()+1, "todoist_GetUsers", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); + } + elsif ($cmd eq "accessToken" || $cmd eq "newAccessToken") { + return todoist_setPwd ($hash,$name,@args); + } + elsif ($cmd eq "addTask" || $cmd eq "newTask") { + my $count=@args; + if ($count!=0) { + my $exp=decode_utf8(join(" ",@args)); + todoist_CreateTask ($hash,$exp); + } + return "new Task needs a title" if ($count==0); + } + elsif ($cmd eq "completeTask" || $cmd eq "closeTask") { + my $term=$cmd eq "completeTask"?"complete":"close"; + my $count=@args; + if ($count!=0) { + my $exp=decode_utf8(join(" ",@args)); + Log3 $name,5, "todoist ($name): ".$term."d startet with exp: $exp"; + todoist_UpdateTask ($hash,$exp,$term); + } + return "in order to complete or close a task, we need it's ID" if ($count==0); + } + elsif ($cmd eq "uncompleteTask") { + my $count=@args; + if ($count!=0) { + my $exp=decode_utf8(join(" ",@args)); + Log3 $name,5, "todoist ($name): Uncompleted startet with exp: $exp"; + todoist_UpdateTask ($hash,$exp,"uncomplete"); + } + return "in order to complete a task, we need it's ID" if ($count==0); + } + elsif ($cmd eq "updateTask") { + my $count=@args; + if ($count!=0) { + my $exp=decode_utf8(join(" ",@args)); + todoist_UpdateTask ($hash,$exp,"update"); + } + return "in order to complete a task, we need it's ID" if ($count==0); + } + elsif ($cmd eq "deleteTask") { + my $count=@args; + if ($count!=0) { + my $exp=decode_utf8(join(" ",@args)); + todoist_UpdateTask ($hash,$exp,"delete"); + } + return "in order to delete a task, we need it's ID" if ($count==0); + } + elsif ($cmd eq "sortTasks") { + todoist_sort($hash); + } + elsif ($cmd eq "clearList") { + todoist_clearList($hash); + } + else { + return $usage; + } + + return undef; + +} + +sub todoist_Get($@) { + my ($hash, $name, $cmd, @args) = @_; + my $ret = undef; + + if ( $cmd eq "version") { + $hash->{VERSION} = $version; + return "Version: ".$version; + } + else { + $ret ="$name get with unknown argument $cmd, choose one of " . join(" ", sort keys %gets); + } + + return $ret; +} + +##################################### +# sets todoist Access Token +sub todoist_setPwd($$@) { + my ($hash, $name, @pwd) = @_; + + return "Password can't be empty" if (!@pwd); + + my $pwdString=$pwd[0]; + my $enc_pwd = ""; + + my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; + my $key = getUniqueId().$index; + + if(eval "use Digest::MD5;1") { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (split //, $pwdString) { + my $encode=chop($key); + $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); + $key=$encode.$key; + } + + Log3 $name,5,"todoist ($name): encoded pwd: $enc_pwd"; + + my $err = setKeyValue($index, $enc_pwd); + + return "error while saving the password - $err" if(defined($err)); + + delete($hash->{helper}{PWD_NEEDED}) if(exists($hash->{helper}{PWD_NEEDED})); + + + if (AttrVal($name,"disable",0) != 1) { + readingsSingleUpdate($hash,"state","active",1); + } + + todoist_RestartGetTimer($hash); + + Log3 $name, 3, "todoist ($name). New Password set."; + + return "password successfully saved"; + +} + +##################################### +# reads the Access Token and checks it +sub todoist_checkPwd ($$) { + my ($hash, $pwd) = @_; + my $name = $hash->{NAME}; + + my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; + my $key = getUniqueId().$index; + + my ($err, $password) = getKeyValue($index); + + if ($err) { + $hash->{helper}{PWD_NEEDED} = 1; + Log3 $name, 3, "todoist ($name): unable to read password from file: $err"; + return undef; + } + + if ( defined($password) ) { + if ( eval "use Digest::MD5;1" ) { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + my $dec_pwd = ''; + + for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { + my $decode=chop($key); + $dec_pwd.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + + return 1 if ($dec_pwd eq $pwd); + } + else { + return "no password saved" if (!$password); + } + + return 0; +} + +sub todoist_Notify ($$) { + my ($hash,$dev) = @_; + + my $name = $hash->{NAME}; + + return if($dev->{NAME} ne "global"); + + return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); + + todoist_RestartGetTimer($hash); + + return undef; + +} + +# restart timers for getTasks if active +sub todoist_RestartGetTimer($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash, "todoist_GetTasks"); + InternalTimer(gettimeofday()+0.3, "todoist_GetTasks", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); + + return undef; +} + +# placeholder for older widgets +sub todoist_AllHtml(;$) { + my ($regEx) = @_; + return todoist_Html($regEx); +} + +# show widget in detail view of todoist device +sub todoist_detailFn(){ + my ($FW_wname, $devname, $room, $pageHash) = @_; # pageHash is set for summaryFn. + + my $hash = $defs{$devname}; + + $hash->{mayBeVisible} = 1; + + my $name=$hash->{NAME}; + + return undef if (IsDisabled($name) || AttrVal($name,"showDetailWidget",1)!=1); + + my $html=""; + + my $icon = AttrVal($devname, "icon", ""); + $icon = FW_makeImage($icon,$icon,"icon") . " " if($icon); + + #$html = '
'. + # ''. + # ''. + # '
'.$icon.' '.AttrVal($name,"alias",$name).'
'.InternalVal($devname,"STATE",ReadingsVal($devname,"state","-")).'
'; + + $html .= todoist_Html($name,undef,1); + + return $html; +} + +# frontend weblink widget (FHEMWEB) +sub todoist_Html(;$$$) { + my ($regEx,$refreshGet,$detail) = @_; + + $regEx="" if (!defined($regEx)); + $refreshGet=0 if (!defined($refreshGet)); + $detail=0 if (!defined($detail)); + + my $filter=""; + + $filter.=":FILTER=".$regEx; + + my @devs = devspec2array("TYPE=todoist".$filter); + my $ret=""; + my $rot=""; + + my $r=0; + + my $count = @devs; + my $width = 95/$count; + + # refresh request? don't return everything + if (!$refreshGet) { + # define global JS variables + $rot .= " "; + # Javascript and CSS + $rot .= " + + "; + + $ret .= "
\n"; + } + + foreach my $name (@devs) { + + $r++; + + my $hash = $defs{$name}; + my $id = $defs{$name}{NR}; + + my $countList = ReadingsVal($name,"count",0); + my $hLIE = AttrVal($name,"hideListIfEmpty",0); + my $showList = ($hLIE==1 && $countList==0)?0:1; + + # show active lists only + if (!IsDisabled($name) && ($showList || $detail)) { + + # refresh request? don't return everything + if (!$refreshGet) { + + $ret .= "\n"; + + $ret .= "\n". + " \n". + "\n"; + $ret .= "\n"; + + $ret .= "
\n". + " \n". + "
\n"; + + } + + my $i=1; + my $eo; + my $cs=3; + + # show data + foreach (@{$hash->{helper}{TIDS}}) { + + if ($i%2==0) { + $eo="even"; + } + else { + $eo="odd"; + } + + my $ind=0; + + my $indent=$hash->{helper}{INDENT}{$_}; + + $ret .= "\n". + " \n". + " \n"; + + $ret .= "\n"; + + $ret .= "\n"; + + $i++; + } + + # refresh request? don't return everything + if (!$refreshGet) { + + my $showPH = 0; + $showPH = 1 if ($i==1); + + $ret .= ""; + $ret .= " "; + $ret .= ""; + + + $ret .= ""; + + + $ret .= ""; + + $ret .= ""; + + $ret .= "
\n". + "
\n". + " {'check'}."\" class=\"todoist_checkbox_".$name."\" type=\"checkbox\" id=\"check_".$_."\" data-id=\"".$_."\" />\n". + "
\n". + " ".$hash->{helper}{TITLE}{$_}."\n". + " {helper}{TITLE}{$_}."\" />\n". + " \n". + " {'delete'}."\" href=\"#\" class=\"todoist_delete_".$name."\" data-id=\"".$_."\">\n". + " x\n". + " \n". + "
". + " ".$todoist_tt->{'nolistdata'}.".\n". + "
". + " \n". + " {'newentry'}."\" type=\"text\" id=\"newEntry_".$name."\" />\n". + "
\n"; + + } + } + } + + # refresh request? don't return everything + if (!$refreshGet) { + + $ret .= "
\n"; + $ret .= "
"; + + } + + return $rot.$ret; +} + +# called if weblink widget table has to be updated +sub todoist_ReloadTable($) { + my ($name) = @_; + + my $ret = todoist_Html($name,1); + $ret =~ s/\"/\'/g; + $ret =~ s/\n//g; + + map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_reloadTable === \"function\") todoist_reloadTable('$name',\"$ret\")", "")} devspec2array("TYPE=FHEMWEB"); +} + +# check if element is in array +sub todoist_inArray { + my ($arr,$search_for) = @_; + foreach (@$arr) { + return 1 if ($_ eq $search_for); + } + return 0; +} + +1; + +=pod +=item device +=item summary uses todoist API to add, read, complete and delete tasks in a todoist tasklist +=item summary_DE Taskverwaltung einer todoist Taskliste über die todoist API +=begin html + + +

todoist

+
    + A module to get a task list as readings from todoist. Tasks can be completed, updated and deleted. + The modul comes with a weblink-widget to manage tasks comfortable. +

    + As preparation to use this module, you need to get your API Symbol (API ID) from the + preferences of your account. +

    + Notes:
    +
      +
    • JSON, Data::Dumper, Digest::MD5, Date::Parse and Data::UUID have to be installed on the FHEM host.
    • +
    +

    + +

    Define

    +
      + define <name> todoist <PROJECT-ID>
      +
      + PROJECT-ID: The ID of your project (=list).
      +

      + + Example: +
        + define Einkaufsliste todoist 257528237
        +
      +

    + +

    Set

    +
      +
    • accessToken - set the API Symbol for your todoist app

    • +
    • active - set the device active (starts the timer for reading task-list periodically)

    • +
    • inactive - set the device inactive (deletes the timer, polling is off)

    • +
    • newAccessToken - replace the saved token with a new one.

    • +
    • getTasks - get the task list immediately, reset timer.

    • +
    • getUsers - get the projects users immediately.

    • +
    • addTask - create a new task. Needs title as parameter.


    • + set <DEVICE> addTask <TASK_TITLE>[:<DUE_DATE>]

      + Additional Parameters are:
      +
        +
      • dueDate (due_date)=<DUE_DATE> (can be free form text or format: YYYY-MM-DDTHH:MM)
      • +
      • priority=the priority of the task (a number between 1 and 4, 4 for very urgent and 1 for natural).
      • +
      • responsibleUid=the todoist-ID of the user who is responsible for accomplishing the current task
      • +
      • assignedByUid=the todoist-ID of the user who assigned the current task
      • +
      • order=the order of the task inside a project (the smallest value would place the task at the top)
      • +
      • parentID=parent_id of the parent task. indent will be updated automatically.
      • +
      • indent=the indent of the task (a number between 1 and 4, where 1 is top-level)
      • +

      + Examples:

      + set <DEVICE> addTask <TASK_TITLE> dueDate=2017-01-15 priority=2

      + set <DEVICE> addTask <TASK_TITLE> dueDate=morgen

      +
    • updateTask - update a task. Needs Task-ID or todoist-Task-ID as parameter

      + Possible additional parameters are:
      +
        +
      • dueDate (due_date)=<DUE_DATE> (can be free form text or format: YYYY-MM-DDTHH:MM)
      • +
      • priority=(1..4) (string)
      • +
      • title=<TITLE> (string)
      • +
      • responsibleUid=the todoist-ID of the user who is responsible for accomplishing the current task
      • +
      • assignedByUid=the todoist-ID of the user who assigned the current task
      • +
      • order=the order of the task inside a project (the smallest value would place the task at the top)
      • +
      • indent=the indent of the task (a number between 1 and 4, where 1 is top-level)
      • +
      • parentID=parent_id of the parent task. indent will be updated automatically.
      • +
      • remove=<TYPE> (comma seperated list of attributes which should be removed from the task)
      • +

      + Examples:

      + set <DEVICE> updateTask ID:12345678 dueDate=2017-01-15 priority=1
      + set <DEVICE> updateTask 1 dueDate=übermorgen
      + set <DEVICE> updateTask TITLE:Brot dueDate=übermorgen

    • +
    • completeTask - completes a task. Needs number of task (reading 'Task_NUMBER'), the title (TITLE:<TITLE>) or the + todoist-Task-ID (ID:<ID>) as parameter

      + set <DEVICE> completeTask <TASK-ID> - completes a task by number
      + set <DEVICE> completeTask ID:<todoist-TASK-ID> - completes a task by todoist-Task-ID + set <DEVICE> completeTask TITLE:<Task title> - completes a task by title

    • +
    • closeTask - closes a task. Needs number of task (reading 'Task_NUMBER')m the title (TITLE:<TITLE>) or the + todoist-Task-ID (ID:) as parameter
      + Difference to complete is: regular task is completed and moved to history, subtask is checked (marked as done, but not moved to history),
      + recurring task is moved forward (due date is updated).

      + set <DEVICE> closeTask <TASK-ID> - completes a task by number
      + set <DEVICE> closeTask ID:<todoist-TASK-ID> - completes a task by todoist-Task-ID + set <DEVICE> closeTask TITLE:<Task title> - completes a task by title

    • +
    • uncompleteTask - uncompletes a Task. Use it like complete.

    • +
    • deleteTask - deletes a task. Needs number of task (reading 'Task_NUMBER'), title (TITLE:<TITLE>) or the todoist-Task-ID (ID:<ID>) as parameter

      + set <DEVICE> deleteTask <TASK-ID> - deletes a task by number
      + set <DEVICE> deleteTask ID:<todoist-TASK-ID> - deletes a task by todoist-Task-ID + set <DEVICE> deleteTask TITLE:<Task title> - deletes a task by title

    • +
    • sortTasks - sort Tasks alphabetically

    • +
    • clearList - deletes all Tasks from the list (only FHEM listed Tasks can be deleted)

    • +
    • cChildProjects - searches for children and defines them if possible, deletes lists that are deleted + in todoist (Attribut delDeletedLists)
    • +
    +
    + +

    Attributes

    +
      +
    • readingFnAttributes
    • +
    • do_not_notify
    • +
    • disable
    • +
    • pollInterval + get the list every pollInterval seconds. Default is 1800. Smallest possible value is 20.

    • +
    • sortTasks +
        +
      • 0: don't sort the tasks (default)
      • +
      • 1: sorts Tasks alphabetically after every update
      • + +
    • +
      +
    • hideId +
        +
      • 0: show todoist-Task-ID (default)
      • +
      • 1: hide the todoist-Task-ID
      • +
    • +
      +
    • hideListIfEmpty +
        +
      • 0: don't hide list in widget, if empty (default)
      • +
      • 1: hide list in widget, if empty
      • +
    • +
      +
    • showPriority +
        +
      • 0: don't show priority (default)
      • +
      • 1: show priority
      • +
    • +
      +
    • showAssignedBy +
        +
      • 0: don't show assignedByUid (default)
      • +
      • 1: show assignedByUid
      • +
    • +
      +
    • showIndent +
        +
      • 0: don't show indent of the task (default)
      • +
      • 1: show indent
      • +
    • +
      +
    • showOrder +
        +
      • 0: don't show order no. of the task (default)
      • +
      • 1: show order number
      • +
    • +
      +
    • showChecked +
        +
      • 0: don't show if a task is checked (for tasks with parent_id)
      • +
      • 1: show if a task is checked (default)
      • +
    • +
      +
    • showDeleted +
        +
      • 0: don't show if a task is deleted (for tasks with parent_id)
      • +
      • 1: show if a task is deleted (default)
      • +
    • +
      +
    • showResponsible +
        +
      • 0: don't show responsibleUid (default)
      • +
      • 1: show responsibleUid
      • +
    • +
      +
    • showDetailWidget +
        +
      • 0: don't show widget in detail view of device
      • +
      • 1: show widget in detail view of device (default)
      • +
    • +
      +
    • getCompleted +
        +
      • 0: don't get completet tasks (default)
      • +
      • 1: get completed tasks
      • +

      + ATTENTION: Only premium users have access to completed tasks!
    • +

      +
    • autoGetUsers +
        +
      • 0: don't get users automatically
      • +
      • 1: get users after every "getTasks" (default)
      • +
    • +
      +
    • avoidDuplicates +
        +
      • 0: don't check for duplicates (default)
      • +
      • 1: check for duplicates, deny new entries if task exists (exactly)
      • +
    • +
      +
    • delDeletedLists +
        +
      • 0: don't delete deleted childrens (default)
      • +
      • 1: delete deleted childrens
      • +
    • +
      +
    • listDivider
      + set the divider for the Reading listText. Default is ", ".
    • +
    + + +

    Readings

    +
      +
    • Task_XXX
      + the tasks are listet as Task_000, Task_001 [...].
    • +
    • Task_XXX_parentID
      + parent ID of task XXX if not null
    • +
    • Task_XXX_checked
      + 1 when a task with parent_id is checked
    • +
    • Task_XXX_isDeleted
      + 1 when a task with parent_id is deleted
    • +
    • Task_XXX_dueDate
      + if a task has a due date, this reading should be filled with the date.
    • +
    • Task_XXX_priority
      + the priority of your task.
    • +
    • Task_XXX_ID
      + the todoist ID of Task_X.
    • +
    • Task_XXX_completedAt
      + only for completed Tasks (attribute getCompleted).
    • +
    • Task_XXX_completedById
      + only for completed Tasks (attribute getCompleted).
    • +
    • Task_XXX_assignedByUid
      + the user this task was assigned by.
    • +
    • Task_XXX_responsibleUid
      + the user this task was assigned to.
    • +
    • Task_XXX_indent
      + shows the indent of the task (attribute showIndent).
    • +
    • Task_XXX_order
      + shows the order no. of the task (attribute showOrder).
    • +
    • User_XXX
      + the lists users are listet as User_000, User_001 [...].
    • +
    • User_XXX_ID
      + the users todoist ID.
    • +
    • listText
      + a comma seperated list of tasks in the specified list. This may be used for TTS, Messages etc.
    • +
    • count
      + number of Tasks in list.
    • +
    • error
      + current error. Default is none.
    • +
    • lastCompletedTask
      + title of the last completed task.
    • +
    • lastCreatedTask
      + title of the last created task.
    • +
    • lastDeletedTask
      + title of the last deleted task.
    • +
    • lastError
      + last Error.
    • +
    • state
      + state of the todoist-Device
    • +

    + +

    Weblinks

    + FHEMWEB weblink as frontend for one or more. There are 3 different ways to setup a todoist-weblink:

    +
      +
    • Define a simple weblink for a Task list: +

      + define <NAME> weblink htmlCode {todoist_Html("<TODOIST-DEVCICENAME>")}

    • +
    • Define a simple weblink for all active Task lists: +

      + define <NAME> weblink htmlCode {todoist_Html()}

    • +
    • Define a simple weblink for a group of active Task lists: +

      + define <NAME> weblink htmlCode {todoist_Html('<REGEX-FILTER>')}

    • +
    + Examples:

    + define EinkaufslisteWeb weblink htmlCode {todoist_Html("Einkaufsliste")} - single list
    + define EinkaufslistenWeb weblink htmlCode {todoist_Html()} - all active lists
    + define PapaLists weblink htmlCode {todoist_Html("Papa_list.*")} - all active lists beginning with "Papa_list" +

    +
      +
    • Tasks can be sorted, updated and added inside the widget. You can reload a list and delete all entries of a list.
    • +
    • To check (close) a task, click the checkbox in front of the completed task.
    • +
    • A list can be sorted by by drag & drop. You can also move a task from one list to another. This deletes the task from the "old" list and
      + adds it to the new one.
    • +
    • The input field under the actual list can be used for adding tasks to this list. Leaving the field or pressing enter triggers addTask.
    • +
    • Update a task by clicking it's text. You can edit the task in an input field. Leaving the field or pressing enter triggers updateTask.
    • +
    • You can use parameters like "dueDate" in the input fields like this: +
        + Milch -dueDate=morgen +
      +
    • +
    • Clicking the cross behind every task deletes this one for good.
    • +
    • Clicking the trashcan symbol in the title bar of every list deletes all tasks from the list.
    • +
    • A list can be reloaded by clicking the round arrow symbol in the title bar of every list.
    • +
    • A list is refreshed if a getTasks for this is fired (by interval or manually).
    • +
    +
+ + +=end html \ No newline at end of file diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index db6a8a582..941a3e858 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -503,6 +503,7 @@ FHEM/98_structure.pm rudolfkoenig Automatisierung FHEM/98_Siro.pm Byte09 / Dr. Smag Sonstige Systeme FHEM/98_MSwitch.pm Byte09 Automatisierung FHEM/98_telnet.pm rudolfkoenig Automatisierung +FHEM/98_todoist.pm marvin78 Unterstuetzende Dienste FHEM/98_update.pm rudolfkoenig Sonstiges FHEM/98_version.pm markusbloch Sonstiges FHEM/98_vitoconnect.pm andreas13 Heizungssteuerung/Raumklima diff --git a/fhem/www/pgm2/todoist.js b/fhem/www/pgm2/todoist.js new file mode 100644 index 000000000..b25869cee --- /dev/null +++ b/fhem/www/pgm2/todoist.js @@ -0,0 +1,348 @@ +FW_version["todoist.js"] = "$Id$"; + +if (typeof todoist_checkVar === 'undefined') { + + var todoist_checkVar=1; + + var req = new XMLHttpRequest(); + req.open('GET', document.location, false); + req.send(null); + var csrfToken = req.getResponseHeader('X-FHEM-csrfToken'); + + var todoist_icon={}; + + var todoist_svgPrefix=''; + todoist_icon.del=todoist_svgPrefix+'d="M0 84V56c0-13.3 10.7-24 24-24h112l9.4-18.7c4-8.2 12.3-13.3 21.4-13.3h114.3c9.1 0 17.4 5.1 21.5 13.3L312 32h112c13.3 0 24 10.7 24 24v28c0 6.6-5.4 12-12 12H12C5.4 96 0 90.6 0 84zm416 56v324c0 26.5-21.5 48-48 48H80c-26.5 0-48-21.5-48-48V140c0-6.6 5.4-12 12-12h360c6.6 0 12 5.4 12 12zm-272 68c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208zm96 0c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208zm96 0c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208z"/>'; + todoist_icon.loading=''; + + function todoist_encodeParm(oldVal) { + var newVal; + newVal = oldVal.replace(/\$/g, '\\%24'); + newVal = newVal.replace(/"/g, '%27'); + newVal = newVal.replace(/#/g, '%23'); + newVal = newVal.replace(/\+/g, '%2B'); + newVal = newVal.replace(/&/g, '%26'); + newVal = newVal.replace(/'/g, '%27'); + newVal = newVal.replace(/=/g, '%3D'); + newVal = newVal.replace(/\?/g, '%3F'); + newVal = newVal.replace(/\|/g, '%7C'); + newVal = newVal.replace(/\s/g, '%20'); + return newVal; + }; + + function todoist_dialog(message,title) { + if (typeof title === 'undefined') title="Message"; + $('
').appendTo('body').html('
' + message + '
').dialog({ + modal: true, title: 'Todoist '+title, zIndex: 10000, autoOpen: true, + width: 'auto', resizable: false, + buttons: { + OK: function () { + $(this).dialog("close"); + } + }, + close: function (event, ui) { + $(this).remove(); + } + }); + setTimeout(function(){ + $('.ui-dialog').remove(); + },10000); + }; + + function todoist_refreshTable(name,sortit) { + var i=1; + $('#todoistTable_' + name).find('tr.todoist_data').each(function() { + // order + var tid = $(this).attr("data-line-id"); + $(this).removeClass("odd even"); + if (i%2==0) $(this).addClass("even"); + else $(this).addClass("odd"); + if (typeof sortit != 'undefined') todoist_sendCommand('set ' + name + ' updateTask ID:'+ tid + ' order="' + i + '"'); + i++; + }); + if (i!=1) $('#todoistTable_' + name).find("tr.todoist_ph").hide(); + if (i==1) $('#todoistTable_' + name).find("tr.todoist_ph").show(); + refreshInput(name); + refreshInputs(name); + todoist_removeLoading(name); + } + + function todoist_refreshTableWidth() { + $('.sortable').each(function() { + $(this).css('width',''); + }); + } + + function todoist_reloadTable(name,val) { + var todoist_small = (screen.width < 480 || screen.height < 480); + $('#todoistTable_' + name).find('tr.todoist_data').remove(); + $('#todoistTable_' + name).find('#newEntry_'+name).parent().parent().before(val); + todoist_refreshTable(name); + if (!todoist_small) $('#newEntry_' + name).focus(); + } + + function refreshInputs(name) { + $('#todoistTable_' + name).find('tr.todoist_data').find('td.todoist_input').find('input[type=text]').each(function() { + var w = $(this).prev('span').width()+5; + $(this).width(w); + }); + } + + function refreshInput(name) { + $('#newEntry_'+name).width(0); + var w = $('#newEntry_'+name).parent('td').width()-4; + $('#newEntry_'+name).width(w); + } + + function todoist_sendCommand(cmd) { + var name = cmd.split(" ")[1]; + todoist_addLoading(name); + var location = document.location.pathname; + if (location.substr(location.length -1, 1) == '/') { + location = location.substr(0, location.length -1); + } + var url = document.location.protocol + "//" + document.location.host + location; + FW_cmd(url + '?XHR=1&fwcsrf=' + csrfToken + '&cmd.' + name + '=' + cmd); + } + + function todoist_addLoading(name) { + if ( $('.todoist_devType_' + name).find('.todoist_loadingDiv').length ) { + $('.todoist_devType_' + name).find('.todoist_loadingDiv').remove(); + } + else { + $('.todoist_devType_' + name).append('
' + todoist_icon.loading + '
'); + setTimeout(function(){ + todoist_removeLoading(name); + }, 10000); + } + } + + function todoist_removeLoading(name) { + $('.todoist_devType_' + name).find('.todoist_loadingDiv').remove(); + } + + function todoist_ErrorDialog(name,text,title) { + todoist_dialog(text,title); + todoist_removeLoading(name); + } + + function todoist_removeLine(name,id) { + var i=1; + $('#todoistTable_' + name).find('tr.todoist_data').each(function() { + var tid = $(this).attr("data-line-id"); + if (tid==id) $(this).remove(); + else { + $(this).removeClass("odd even"); + if (i%2==0) $(this).addClass("even"); + else $(this).addClass("odd"); + i++; + } + }); + if (i==1) $('#todoistTable_' + name).find("tr.todoist_ph").show(); + todoist_refreshTable(name); + todoist_getSizes(); + } + + function todoist_addLine(name,id,title) { + var lastEl=$('#todoistTable_' + name).find('tr').last(); + var prevEl=$(lastEl).prev('tr'); + var cl="odd"; + if (prevEl != 'undefined') { + cl = $(prevEl).attr('class'); + if (cl=="odd") cl="even"; + else cl="odd" + } + $(lastEl).before('\n' + + ' \n'+ + '
\n'+ + ' \n'+ + ' \n' + + ' \n'+ + ' ' + title + '\n'+ + ' '+ + ' \n' + + ' \n' + + ' \n'+ + ' x\n'+ + ' \n'+ + ' \n'+ + '\n' + ); + $('#todoistTable_' + name).find("tr.todoist_ph").hide(); + todoist_getSizes(); + todoist_refreshTable(name); + } + + function resizable (el, factor) { + var int = Number(factor) || 7.7; + function resize() {el.style.width = ((el.value.length+1) * int) + 'px'} + var e = 'keyup,keypress,focus,blur,change'.split(','); + for (var i in e) el.addEventListener(e[i],resize,false); + resize(); + } + + + function todoist_getSizes() { + var height = 0; + var width = 0; + $('.sortable .sortit').each(function() { + var tHeight = $(this).outerHeight(); + if (tHeight > height) height = tHeight; + }); + $('.sortable').css('max-height',height).css('height',height); + } + + function todoist_addHeaders() { + $("
").appendTo($('.todoist_devType')).html(todoist_icon.ref); + $("
").appendTo($('.todoist_devType')).html(todoist_icon.del); + } + + $(document).ready(function(){ + todoist_getSizes(); + todoist_addHeaders(); + $('.todoist_name').each(function() { + var name = $(this).val(); + todoist_refreshTable(name); + + $('.todoist_devType_' + name).on('click','div.todoist_deleteAll',function(e) { + if (confirm(todoist_tt.clearconfirm)) { + todoist_sendCommand('set ' + name + ' clearList'); + } + }); + $('.todoist_devType_' + name).on('click','div.todoist_refresh',function(e) { + todoist_sendCommand('set ' + name + ' getTasks'); + }); + + $('#todoistTable_' + name).on('mouseover','.sortit',function(e) { + $(this).find('div.todoist_move').addClass('todoist_sortit_handler'); + }); + $('#todoistTable_' + name).on('mouseout','.sortit',function(e) { + $(this).find('div.todoist_move').removeClass('todoist_sortit_handler'); + }); + $('#todoistTable_' + name).on('blur keypress','#newEntry_' + name,function(e) { + if (e.type!='keypress' || e.which==13) { + e.preventDefault(); + var v=todoist_encodeParm($(this).val()); + if (v!="") { + todoist_sendCommand('set '+ name +' addTask ' + v); + $(this).val(""); + } + } + }); + $('#todoistTable_' + name).on('click','input[type="checkbox"]',function(e) { + var val=$(this).attr('checked'); + if (!val) { + var id=$(this).attr('data-id'); + todoist_sendCommand('set ' + name + ' closeTask ID:'+ id); + } + }); + $('#todoistTable_' + name).on('click','a.todoist_delete_'+name,function(e) { + if (confirm(todoist_tt.delconfirm)) { + var id=$(this).attr('data-id'); + todoist_sendCommand('set ' + name + ' deleteTask ID:'+ id); + } + return false; + }); + $('#todoistTable_' + name).on('click','span.todoist_task_text',function(e) { + var id = $(this).attr("data-id"); + var val=$(this).html(); + var width=$(this).width()+20; + $(this).hide(); + $("input[data-id='" + id +"']").show().focus().val("").val(val); + }); + $('#todoistTable_' + name).on('blur keypress','input.todoist_input_'+name,function(e) { + if (e.type!='keypress' || e.which==13) { + e.preventDefault(); + var val = $(this).val(); + + var comp = $(this).prev().html(); + var id = $(this).attr("data-id"); + var val = $(this).val(); + $(this).hide(); + $("span.todoist_task_text[data-id='" + id +"']").show(); + if (val != "" && comp != val) { + $("span.todoist_task_text[data-id='" + id +"']").html(val); + todoist_sendCommand('set ' + name + ' updateTask ID:'+ id + ' title="' + val + '"'); + } + + if (val == "" && e.which==13) { + if (confirm('Are you sure?')) { + $('#newEntry_' + name).focus(); + todoist_sendCommand('set ' + name + ' deleteTask ID:'+ id); + } + } + todoist_refreshTable(name); + } + if (e.type=='keypress') { + resizable(this,7); + refreshInput(name); + } + }); + }); + var fixHelper = function(e, ui) { + ui.children().each(function() { + console.log(e); + $(this).width($(this).width()); + }); + return ui; + }; + $( ".todoist_table table.sortable" ).sortable({ + //axis: 'y', + revert: true, + items: "> tbody > tr.sortit", + handle: ".todoist_sortit_handler", + forceHelperSize: true, + placeholder: "sortable-placeholder", + connectWith: '.todoist_table table.sortable', + helper: fixHelper, + start: function( event, ui ) { + var width = ui.item.innerWidth(); + var height = ui.item.innerHeight(); + ui.placeholder.css("width",width).css("height",height); + }, + stop: function (event,ui) { + var parent = ui.item.parent().parent(); + var id = $(parent).attr('id'); + var name = id.split(/_(.+)/)[1]; + if (ui.item.attr('data-remove')==1) ui.item.remove(); + todoist_refreshTable(name,1); + todoist_refreshTableWidth(); + }, + remove: function (event,ui) { + var id=ui.item.attr('data-line-id'); + var tid = ui.item.attr('id'); + var nameHT = tid.split("_"); + var lastVal = nameHT.pop(); // Get last element + var nameH = nameHT.join("_"); + todoist_sendCommand('set ' + nameH + ' deleteTask ID:'+ id); + }, + over: function (event,ui) { + var width = ui.item.innerWidth(); + var height = ui.item.innerHeight(); + var hwidth = ui.placeholder.innerWidth(); + if (width>hwidth) ui.placeholder.parent().parent().css("width",width).css("height",height); + if (width