fhem-mirror/FHEM/98_todoist.pm
marvin78 b3929a5317 98_todoist: UTF8 fix and docu
git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@24386 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2021-05-05 08:18:00 +00:00

2613 lines
88 KiB
Perl

##############################################
# $Id$
package main;
use strict;
use warnings;
my $missingModule = "";
eval "use Data::Dumper;1" or $missingModule .= "Data::Dumper ";
eval "use JSON;1" or $missingModule .= "JSON ";
eval "use Encode;1" or $missingModule .= "Encode ";
eval "use Date::Parse;1" or $missingModule .= "Date::Parse ";
#######################
# Global variables
my $version = "1.3.11";
my $srandUsed;
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",
"today" => "today",
"tomorrow" => "tomorrow",
"dayaftertomorrow" => "the day after tomorrow",
"yesterday" => "yesterday",
);
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",
"today" => "heute",
"tomorrow" => "morgen",
"dayaftertomorrow" => "übermorgen",
"yesterday" => "gestern",
);
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 ".
"getCompleted:1,0 ".
"showPriority:1,0 ".
"showAssignedBy:1,0 ".
"showResponsible:1,0 ".
"showParent:1,0 ".
"showSection: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 ".
"sslVersion ".
$readingFnAttributes;
$hash->{NotifyOrderPrefix} = "64-";
## renew version and language in reload
foreach my $d ( sort keys %{ $modules{todoist}{defptr} } ) {
my $hash = $modules{todoist}{defptr}{$d};
$hash->{VERSION} = $version;
my $lang = AttrVal($hash->{NAME},"language", AttrVal("global","language","EN"));
if( $lang eq "DE") {
$todoist_tt = \%todoist_transtable_DE;
}
else{
$todoist_tt = \%todoist_transtable_EN;
}
}
return undef;
#return FHEM::Meta::InitMod( __FILE__, $hash );
}
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 <name> todoist <Project id>";
Log3 $name, 4, $msg;
return $msg;
}
return "Cannot define a todoist device. Perl module(s) $missingModule is/are missing." if ( $missingModule );
## set internal variables
$hash->{PID}=$a[2];
$hash->{INTERVAL}=AttrVal($name,"pollInterval",undef)?AttrVal($name,"pollInterval",undef):1800;
$hash->{VERSION}=$version;
$hash->{MID} = 'todoist_'.$a[2]; #
$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";
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");
## start polling
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 if ($errorMessage ne "no data");
Log3 $name,4, "todoist ($name): Api-Error Callback-data: ".$errorLog;
$hash->{helper}{errorData}="";
$hash->{helper}{errorMessage}="";
return undef;
}
# reorderTasks
sub todoist_ReorderTasks ($$) {
my ($hash,$cmd) = @_;
my $name=$hash->{NAME};
Log3 $name,4, "$name: cmd: ".$cmd;
my $pwd="";
my $param;
my %commands=();
# some random string for UUID
my $uuid = todoist_genUUID();
# JSON String start- and endpoint
my $commandsStart="[{";
my $commandsEnd="}]";
my $tType;
my $argsStart = "{\"items\": [";
my $argsEnd = "]}";
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);
# get Task - IDs in order
my $tids = $cmd;
my @taskIds = split(",",$tids);
$tType = "item_reorder";
my $i=0;
foreach my $taskId (@taskIds) {
$i++;
$args .= "," if ($i>1);
$args .= "{\"id\":".$taskId.",\"child_order\":".$i."}";
}
$args = $argsStart.$args.$argsEnd;
Log3 $name,5, "todoist ($name): Data sent to todoist API: ".$args;
my $dataArr=$commandsStart.'"type":"'.$tType.'","uuid":"'.$uuid.'","args":'.$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);
my $method="POST";
$param = {
url => "https://api.todoist.com/sync/v8/sync",
data => $data,
method => $method,
wType => "reorder",
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;
}
# 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]);
$title = $h->{"title"} if ($h->{"title"});
$title = $h->{"TITLE"} if ($h->{"TITLE"});
$title = $h->{"Title"} if ($h->{"Title"});
$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 $uuid = todoist_genUUID();
# 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 = (
id => $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 = (
id => $taskId,
);
Log3 $name,5, "$name: Args: ".Dumper(%args);
$method="POST";
}
## move a task
elsif ($type eq "move") {
# we can avoid duplicates in FHEM. There may still come duplicates coming from another app
if ($h->{"parent_id"}) {
#if (AttrVal($name,"avoidDuplicates",0) == 1 && todoist_inArray(\@{$hash->{helper}{"TITS"}},$title)) {
# 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;
#}
}
$tType = "item_move";
%args = (
id => $taskId,
);
## 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"});
## project_id
$args{'project_id'} = $h->{"project_id"} if ($h->{"project_id"});
$args{'project_id'} = $h->{"projectID"} if ($h->{"projectID"});
$args{'project_id'} = $h->{"projectId"} if ($h->{"projectId"});
## section_id
$args{'section_id'} = $h->{"section_id"} if ($h->{"section_id"});
$args{'section_id'} = $h->{"sectionID"} if ($h->{"sectionID"});
$args{'section_id'} = $h->{"sectionId"} if ($h->{"sectionId"});
if ($args{'parent_id'}) {
my $pid=$args{'parent_id'};
}
}
## 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{'due'}{'string'} = $h->{"dueDate"} if($h->{'dueDate'});
$args{'due'}{'string'} = "" if ($h->{'dueDate'} && $h->{'dueDate'} =~ /(null|none|nix|leer|del)/);
## change dueDate (if someone uses due_date in stead of dueDate)
$args{'due'}{'string'} = $h->{"due_date"} if ($h->{'due_date'});
$args{'due'}{'string'} = "" if ($h->{'due_date'} && $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{'child_order'} = $h->{"order"} if ($h->{"order"});
## child order of the task
$args{'child_order'} = $h->{"child_order"} if ($h->{"child_order"});
## 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 "child_order") {
$args{'child_order'} = 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 = (
id => $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://api.todoist.com/sync/v8/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]);
$title = encode_utf8($h->{"title"}) if ($h->{"title"});
my $check=1;
# we can avoid duplicates in FHEM. There may still come 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"});
$data->{'date_string'} = encode_utf8($data->{'date_string'});
## 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"});
## child order of the task
$data->{'child_order'} = $h->{"child_order"} if ($h->{"child_order"});
Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".Dumper($data);
$param = {
url => "https://todoist.com/sync/v8/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 moveTask 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/sync/v8/projects/get_data";
## check if we get also the completed Tasks
if ($completed == 1) {
$url = "https://todoist.com/sync/v8/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()+0.3, "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);
my $prefix="Task_";
## Log possbile errors in callback
if ($err ne "") {
todoist_ErrorReadings($hash,$err);
}
else {
my $decoded_json="";
# check 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}=encode_utf8($project->{name});
$hash->{PROJECT_COLOR}=$project->{color};
$hash->{PROJECT_ORDER}=$project->{child_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});
}
#$prefix="cTask_" if ($param->{completed} == 1);
## 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};
$taskID = $task->{task_id} if ($param->{completed} == 1);
readingsBulkUpdate($hash, $prefix.$t,$title);
readingsBulkUpdate($hash, $prefix.$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}{"parent_id"}{$taskID}=$task->{parent_id}; # parent_id of item
$hash->{helper}{"section_id"}{$taskID}=$task->{section_id}; # section_id of item
$hash->{helper}{"child_order"}{$taskID}=$task->{child_order}; # order of task under parent
$hash->{helper}{"PRIORITY"}{$taskID}=$task->{priority}; # todoist Task priority
#push @{$hash->{helper}{"PARENTS"}{$task->{parent_id}}},$taskID; # ident for better widget
$hash->{helper}{"ORDER"}{$taskID}=$task->{item_order}; # todoist Task order
if ($param->{completed} != 1) {
push @{$hash->{helper}{"TIDS"}},$taskID; # simple ID list
push @{$hash->{helper}{"TITS"}},$title; # simple ID list
}
readingsBulkUpdate($hash, $prefix.$t."_parent_id",$task->{parent_id}) if (AttrVal($name,"showParent",0)==1);
readingsBulkUpdate($hash, $prefix.$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, $prefix.$t."_parentID",$task->{parent_id}) if (AttrVal($name,"showParent",1)==1);
$hash->{helper}{"PARENT_ID"}{$taskID}=$task->{parent_id};
}
## set section_id if not null
if (defined($task->{section_id}) && $task->{section_id} ne 'null') {
## if this task has a parent_id we set the reading
readingsBulkUpdate($hash, $prefix.$t."_sectionID",$task->{section_id}) if (AttrVal($name,"showSection",1)==1);
$hash->{helper}{"SECTION_ID"}{$taskID}=$task->{section_id};
}
## set completed_date if present
if (defined($task->{checked}) && $task->{checked}!=0) {
readingsBulkUpdate($hash, $prefix.$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, $prefix.$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, $prefix.$t."_completedAt",FmtDateTime(str2time($task->{completed_date})));
$hash->{helper}{"COMPLETED_AT"}{$taskID}=FmtDateTime(str2time($task->{completed_date}));
readingsBulkUpdate($hash, $prefix.$t."_completedById",$task->{user_id});
$hash->{helper}{"COMPLETED_BY_ID"}{$taskID}=$task->{user_id};
}
## set due_date if present
if (defined($task->{due}) && $task->{due}{date} ne 'null') {
## if there is a task with due date, we create a new reading
readingsBulkUpdate($hash, $prefix.$t."_dueDate",FmtDateTime(str2time($task->{due}{date})));
$hash->{helper}{"DUE_DATE"}{$taskID}=FmtDateTime(str2time($task->{due}{date}));
}
## 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, $prefix.$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, $prefix.$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, $prefix.$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, $prefix.$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);
}
}
}
readingsEndUpdate( $hash, 1 );
## list Text for TTS, Text-Message...
if ($param->{completed} != 1) {
$lText="-" if ($lText eq "");
readingsSingleUpdate($hash,"listText",$lText,1) if ($lText ne "");
}
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/sync/v8/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 );
todoist_ReloadTable($name);
}
}
# 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/sync/v8/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;
$title =~ s!\s!!g;
# 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_COLOR}=$project->{color};
$new_hash->{PROJECT_ORDER}=$project->{child_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;
}
#################################################
# 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.";
return undef;
}
################################################
# 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.";
return undef;
}
################################################
# 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.";
return undef;
}
## 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 =~ /(show(Priority|AssignedBy|Responsible|Order|DetailWidget|Section|Parent)|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, "moveTask";
push @sets, "clearList:noArg";
push @sets, "getTasks:noArg";
push @sets, "cChildProjects:noArg";
push @sets, "getUsers:noArg";
push @sets, "reorderTasks";
}
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" || $cmd eq "moveTask") {
my $term=$cmd eq "updateTask"?"update":"move";
my $count=@args;
if ($count!=0) {
my $exp=decode_utf8(join(" ",@args));
todoist_UpdateTask ($hash,$exp,$term);
}
return "in order to complete a task, we need it's ID" if ($count==0);
}
elsif ($cmd eq "reorderTasks") {
my $count=@args;
if ($count!=0) {
my $exp=$args[0];
todoist_ReorderTasks ($hash,$exp);
}
return "in order to delete 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 "todoist: 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") . "&nbsp;" if($icon);
#$html = '<div id="ddtable" class="makeTable wide"><table class="block wide"><tr class="odd">'.
# '<td width="300px"><div>'.$icon.'&nbsp;'.AttrVal($name,"alias",$name).'</div></td>'.
# '<td informId="'.$name.'"><div id="'.$name.'" title="Initialized" class="col2">'.InternalVal($devname,"STATE",ReadingsVal($devname,"state","-")).'</div></td>'.
# '</tr></table></div>';
$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 $eo="";
my $r=0;
my $width = 95;
my $count = @devs;
$width = $width/$count if ($count>=1);
# refresh request? don't return everything
if (!$refreshGet) {
# define global JS variables
$rot .= " <script type=\"text/javascript\">
todoist_tt={};
todoist_tt.refreshList='".$todoist_tt->{'refreshList'}."';
todoist_tt.clearList='".$todoist_tt->{'clearList'}."';
todoist_tt.clearconfirm='".$todoist_tt->{'clearconfirm'}."';
todoist_tt.delconfirm='".$todoist_tt->{'delconfirm'}."';
todoist_tt.check='".$todoist_tt->{'check'}."';
todoist_tt.delete='".$todoist_tt->{'delete'}."';
todoist_tt.today='".$todoist_tt->{'today'}."';
todoist_tt.tomorrow='".$todoist_tt->{'tomorrow'}."';
todoist_tt.dayaftertomorrow='".$todoist_tt->{'dayaftertomorrow'}."';
todoist_tt.yesterday='".$todoist_tt->{'yesterday'}."';
</script>";
# Javascript and CSS
$rot .= " <script type=\"text/javascript\" src=\"$FW_ME/www/pgm2/todoist.js?version=".$version."\"></script>
<style id=\"todoist_style\">
.todoist_container {
display: block;
padding: 0;
float:none;
}
.todoist_table {
float: left;
margin-right: 10px;
}
.sortable-placeholder {
background-color: grey;
}
.todoist_col1 {
width:32px;
text-align:right;
padding: 4px 4px 4px 1px!important;
}
.todoist_col1 input {
//margin-top:4px;
}
.todoist_move {
width:10px;
float:left;
}
.todoist_input {
}
.todoist_sortit_handler {
padding-top: 0px!important;
content: '....';
width: 5px;
height: 20px;
//display: inline-block;
overflow: hidden;
line-height: 5px;
padding: 0px 2px!important;
cursor: move;
vertical-align: middle;
margin-top: -.2em;
margin-right: 0!important;
font-size: 12px;
font-family: sans-serif;
letter-spacing: 1px;
color: #cccccc;
text-shadow: 1px 0 1px black;
}
.todoist_sortit_handler::after {
content: '.. .. .. ..';
}
\@media(max-device-width: 480px){
.todoist_move {
width:35px;
}
.todoist_sortit_handler {
width: 5px;
}
}
.todoist_delete {
padding-right:15px!important;
padding-left:15px!important;
}
tr.ui-sortable-helper {
background-color:#111111;
}
.todoist_ph td {
padding: 4px 8px;
}
div.todoist_devType {
padding: 4px!important;
}
div.todoist_icon {
cursor: pointer;
display: block;
float: right;
width: 1em;
height: 1em;
margin-left: 0.5em;
}
div.todoist_icon svg {
height: 12px!important;
width: 12px!important;
}
.todoist_dueDateButton svg,
.todoist_responsibleUidButton svg {
margin-top:-2px!important;
}
";
if (!$detail) {
$rot.=" .todoist_table {
margin-bottom: 15px;
}
";
}
$rot.=" </style>
";
$ret .= "<div class=\"todoist_container\">\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 .= "<table class=\"roomoverview todoist_table\">\n";
$ret .= "<tr class=\"devTypeTr\">\n".
" <td colspan=\"3\">\n".
" <div class=\"todoist_devType todoist_devType_".$name." col_header\" data-id=\"".$hash->{PID}."\">\n".
(!$FW_hiddenroom{detail}?"<a title=\"".$todoist_tt->{'gotodetail'}."\" href=\"/fhem?detail=".$name."\">":"").
AttrVal($name,"alias",$name).
(!$FW_hiddenroom{detail}?"</a>":"").
" </div>\n".
" </td>\n".
"</tr>\n";
$ret .= "<tr><td colspan=\"3\"><table class=\"block wide sortable\" id=\"todoistTable_".$name."\" data-project-name=\"".$name."\" data-project-id=\"".$hash->{PID}."\">\n";
}
my $i=1;
my $cs=3;
# show data
foreach (@{$hash->{helper}{TIDS}}) {
if ($i%2==0) {
$eo="even";
}
else {
$eo="odd";
}
my $ind=0;
my $dueDate = defined($hash->{helper}{DUE_DATE}{$_})?$hash->{helper}{DUE_DATE}{$_}:"";
my $responsibleUid = defined($hash->{helper}{RESPONSIBLE_UID}{$_})?$hash->{helper}{RESPONSIBLE_UID}{$_}:"";
$responsibleUid = $hash->{helper}{USER}{NAME}{$responsibleUid} if ($responsibleUid ne "" && defined($hash->{helper}{USER}{NAME}{$responsibleUid}));
my $dueDateClass = $dueDate ne ""?" todoist_dueDate":"";
my $responsibleUidClass = $responsibleUid ne ""?" todoist_responsibleUid":"";
$ret .= "<tr id=\"".$name."_".$_."\" data-due-date=\"".$dueDate."\" data-data=\"true\" data-project-name=\"".$name."\" data-project-id=\"".$hash->{PID}."\" data-line-id=\"".$_."\" class=\"sortit todoist_data ".$eo."\">\n".
" <td class=\"col1 todoist_col1\">\n".
" <div class=\"todoist_move\"></div>\n".
" <input title=\"".$todoist_tt->{'check'}."\" class=\"todoist_checkbox_".$name."\" type=\"checkbox\" id=\"check_".$_."\" data-id=\"".$_."\" />\n".
" </td>\n".
" <td class=\"col1 todoist_input".$dueDateClass.$responsibleUidClass."\">\n".
" <span class=\"todoist_task_text\" data-id=\"".$_."\">".$hash->{helper}{TITLE}{$_}."</span>\n".
" <input type=\"text\" data-id=\"".$_."\" style=\"display:none;\" class=\"todoist_input_".$name."\" value=\"".$hash->{helper}{TITLE}{$_}."\" />\n".
" <div class='todoist_dueDateButton todoist_icon' title='".$dueDate."'> </div>".
" <div class='todoist_responsibleUidButton todoist_icon' title='".$responsibleUid."'> </div>".
" </td>\n";
$ret .= "<td class=\"col2 todoist_delete\">\n".
" <a title=\"".$todoist_tt->{'delete'}."\" href=\"#\" class=\"todoist_delete_".$name."\" data-id=\"".$_."\">\n".
" x\n".
" </a>\n".
"</td>\n";
$ret .= "</tr>\n";
$i++;
}
# refresh request? don't return everything
if (!$refreshGet) {
my $showPH = 0;
$showPH = 1 if ($i==1);
$ret .= "<tr class=\"sortit odd todoist_ph\"".($showPH!=1?" style=\"display:none;\"":"").">";
$ret .= " <td colspan=\"".$cs."\">".
" ".$todoist_tt->{'nolistdata'}.".\n".
" </td>";
$ret .= "</tr>";
$ret .= "<tr class=\"".$eo."\">";
$ret .= "<td colspan=\"".$cs."\">".
" <input type=\"hidden\" class=\"todoist_name\" id=\"todoist_name_".$name."\" value=\"".$name."\" />\n".
" <input title=\"".$todoist_tt->{'newentry'}."\" type=\"text\" id=\"newEntry_".$name."\" />\n".
"</td>";
$ret .= "</tr>";
$ret .= "</table></td></tr>\n";
$ret .= "</table>\n";
}
}
}
# refresh request? don't return everything
if (!$refreshGet) {
$ret .= "</div>\n";
$ret .= "<br style=\"clear:both;\" />";
}
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;
}
sub todoist_genUUID() {
srand(gettimeofday()) if(!$srandUsed);
$srandUsed = 1;
my $uuid = sprintf("%08x-f33f-%s-%s-%s", time(), substr(getUniqueId(),-4),
join("",map { unpack "H*", chr(rand(256)) } 1..2),
join("",map { unpack "H*", chr(rand(256)) } 1..8));
return $uuid;
}
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
<a name="todoist"></a>
<h3>todoist</h3>
<ul>
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.
<br /><br />
As preparation to use this module, you need to get your API Symbol (API ID) from the
preferences of your account.
<br /><br />
Notes:<br />
<ul>
<li>JSON, Data::Dumper, Date::Parse, Digest::MD5, have to be installed on the FHEM host.</li>
</ul>
<br /><br />
<a name="todoist_Define"></a>
<h4>Define</h4>
<ul>
<code>define &lt;name&gt; todoist &lt;PROJECT-ID&gt;</code><br />
<br />
<b>PROJECT-ID:</b> The ID of your project (=list).<br />
<br /><br />
Example:
<ul>
<code>define Einkaufsliste todoist 257528237</code><br />
</ul>
</ul><br />
<a name="todoist_Set"></a>
<h4>Set</h4>
<ul>
<li><b>accessToken</b> - set the API Symbol for your todoist app</li><br />
<li><b>active</b> - set the device active (starts the timer for reading task-list periodically)</li><br />
<li><b>inactive</b> - set the device inactive (deletes the timer, polling is off)</li><br />
<li><b>newAccessToken</b> - replace the saved token with a new one.</li><br />
<li><b>getTasks</b> - get the task list immediately, reset timer.</li><br />
<li><b>getUsers</b> - get the projects users immediately.</li><br />
<li><b>addTask</b> - create a new task. Needs title as parameter.</li><br /><br />
<code>set &lt;DEVICE&gt; addTask &lt;TASK_TITLE&gt;[:&lt;DUE_DATE&gt;]</code><br ><br />
Additional Parameters are:<br />
<ul>
<li>title="&lt;TITLE&gt;" (string)</li>
<li>dueDate (due_date)=&lt;DUE_DATE&gt; (can be free form text or format: YYYY-MM-DDTHH:MM)</li>
<li>priority=the priority of the task (a number between 1 and 4, 4 for very urgent and 1 for natural).</li>
<li>responsibleUid=the todoist-ID of the user who is responsible for accomplishing the current task</li>
<li>assignedByUid=the todoist-ID of the user who assigned the current task</li>
<li>order=the order of the task inside a project (the smallest value would place the task at the top)</li>
<li>parentID=parent_id of the parent task.</li>
<li>sectionID=section_id of the parent task.</li>
</ul><br />
Examples: <br /><br />
<code>set &lt;DEVICE&gt; addTask &lt;TASK_TITLE&gt; dueDate=2017-01-15 priority=2</code><br /><br />
<code>set &lt;DEVICE&gt; addTask &lt;TASK_TITLE&gt; dueDate=morgen</code><br /><br />
<li><b>updateTask</b> - update a task. Needs Task-ID or todoist-Task-ID as parameter<br /><br />
Possible additional parameters are:<br />
<ul>
<li>dueDate (due_date)=&lt;DUE_DATE&gt; (can be free form text or format: YYYY-MM-DDTHH:MM)</li>
<li>priority=(1..4) (string)</li>
<li>title=&lt;TITLE&gt; (string)</li>
<li>responsibleUid=the todoist-ID of the user who is responsible for accomplishing the current task</li>
<li>assignedByUid=the todoist-ID of the user who assigned the current task</li>
<li>order=the order of the task inside a project (the smallest value would place the task at the top)</li>
<li>remove=&lt;TYPE&gt; (comma seperated list of attributes which should be removed from the task)</li>
</ul><br />
Examples: <br /><br />
<code>set &lt;DEVICE&gt; updateTask ID:12345678 dueDate=2017-01-15 priority=1</code><br />
<code>set &lt;DEVICE&gt; updateTask 1 dueDate=übermorgen</code><br />
<code>set &lt;DEVICE&gt; updateTask TITLE:Brot dueDate=übermorgen</code><br /><br /></li>
<li><b>moveTask</b> - move a task to another parent, section or project.
Expects Task-ID or todoist-Task-ID as parameter<br /><br />
Possible additional parameters are:<br />
<ul>
<li>parentID=todoist-ID of the new parent task.</li>
<li>projectID=todoist-ID of the receiving project.</li>
<li>sectionID=todoist-ID of the receiving section.</li>
</ul><br /><br /></li>
<li><b>completeTask</b> - completes a task. Needs number of task (reading 'Task_NUMBER'), the title (TITLE:&lt;TITLE&gt;) or the
todoist-Task-ID (ID:&lt;ID&gt;) as parameter<br /><br />
<code>set &lt;DEVICE&gt; completeTask &lt;TASK-ID&gt;</code> - completes a task by number<br >
<code>set &lt;DEVICE&gt; completeTask ID:&lt;todoist-TASK-ID&gt;</code> - completes a task by todoist-Task-ID<br/>
<code>set &lt;DEVICE&gt; completeTask TITLE:&lt;Task title&gt;</code> - completes a task by title (one word)<br />
<code>set &lt;DEVICE&gt; completeTask title=&lt;Task title&gt;</code> - completes a task by title (multiple words)<br />
<br /></li>
<li><b>closeTask</b> - closes a task. Needs number of task (reading 'Task_NUMBER')m the title (TITLE:&lt;TITLE&gt;) or the
todoist-Task-ID (ID:<ID>) as parameter<br />
Difference to complete is: regular task is completed and moved to history, subtask is checked (marked as done, but not moved to history),<br />
recurring task is moved forward (due date is updated).<br /><br />
<code>set &lt;DEVICE&gt; closeTask &lt;TASK-ID&gt;</code> - completes a task by number<br />
<code>set &lt;DEVICE&gt; closeTask ID:&lt;todoist-TASK-ID&gt;</code> - completes a task by todoist-Task-ID<br />
<code>set &lt;DEVICE&gt; closeTask TITLE:&lt;Task title&gt;</code> - completes a task by title (one word)<br />
<code>set &lt;DEVICE&gt; closeTask title=&lt;Task title&gt;</code> - completes a task by title (multiple words)<br /><br /></li>
<li><b>uncompleteTask</b> - uncompletes a Task. Use it like complete.<br /><br /></li>
<li><b>deleteTask</b> - deletes a task. Needs number of task (reading 'Task_NUMBER'), title (TITLE:&lt;TITLE&gt;) or the todoist-Task-ID (ID:&lt;ID&gt;) as parameter<br /><br />
<code>set &lt;DEVICE&gt; deleteTask &lt;TASK-ID&gt;</code> - deletes a task by number<br />
<code>set &lt;DEVICE&gt; deleteTask ID:&lt;todoist-TASK-ID&gt;</code> - deletes a task by todoist-Task-ID<br />
<code>set &lt;DEVICE&gt; deleteTask TITLE:&lt;Task title&gt;</code> - deletes a task by title (one word)<br />
<code>set &lt;DEVICE&gt; deleteTask title=&lt;Task title&gt;</code> - completes a task by title (multiple words)<br /><br /></li>
<li><b>sortTasks</b> - sort Tasks alphabetically<br /><br /></li>
<li><b>clearList</b> - <b><u>deletes</u></b> all Tasks from the list (only FHEM listed Tasks can be deleted)<br /><br /></li>
<li><b>cChildProjects</b> - searches for children and defines them if possible, deletes lists that are deleted
in todoist (Attribut delDeletedLists)<br /><br /></li>
<li><b>reorderTasks</b> - expects a comma seperated list of todoist-IDs. Tasks will be reorders in the given order<br />
Example: <br /><br />
<code>set &lt;DEVICE&gt; reorderTasks 12345678,23456789,34567890</code><br /><br />
</li>
</ul>
<br />
<a name="todoist_Attributes"></a>
<h4>Attributes</h4>
<ul>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
<li><a href="#do_not_notify">do_not_notify</a></li>
<li><a name="#disable">disable</a></li>
<li>pollInterval
get the list every pollInterval seconds. Default is 1800. Smallest possible value is 20.<br /><br /></li>
<br />
<li>hideId
<ul>
<li>0: show todoist-Task-ID (default)</li>
<li>1: hide the todoist-Task-ID</li>
</ul></li>
<br />
<li>hideListIfEmpty
<ul>
<li>0: don't hide list in widget, if empty (default)</li>
<li>1: hide list in widget, if empty</li>
</ul></li>
<br />
<li>showPriority
<ul>
<li>0: don't show priority (default)</li>
<li>1: show priority</li>
</ul></li>
<br />
<li>showAssignedBy
<ul>
<li>0: don't show assignedByUid (default)</li>
<li>1: show assignedByUid</li>
</ul></li>
<br />
<li>showOrder
<ul>
<li>0: don't show order no. of the task (default)</li>
<li>1: show order number</li>
</ul></li>
<br />
<li>showParent
<ul>
<li>0: don't show parent_id of the task</li>
<li>1: show parent_id (default)</li>
</ul></li>
<br />
<li>showSection
<ul>
<li>0: don't show section_id of the task</li>
<li>1: show section_id (default)</li>
</ul></li>
<br />
<li>showChecked
<ul>
<li>0: don't show if a task is checked (for tasks with parent_id)</li>
<li>1: show if a task is checked (default)</li>
</ul></li>
<br />
<li>showDeleted
<ul>
<li>0: don't show if a task is deleted (for tasks with parent_id)</li>
<li>1: show if a task is deleted (default)</li>
</ul></li>
<br />
<li>showResponsible
<ul>
<li>0: don't show responsibleUid (default)</li>
<li>1: show responsibleUid</li>
</ul></li>
<br />
<li>showDetailWidget
<ul>
<li>0: don't show widget in detail view of device</li>
<li>1: show widget in detail view of device (default)</li>
</ul></li>
<br />
<li>getCompleted
<ul>
<li>0: don't get completet tasks (default)</li>
<li>1: get completed tasks</li>
</ul><br />
<b>ATTENTION: Only premium users have access to completed tasks!</b></li>
<br /><br />
<li>autoGetUsers
<ul>
<li>0: don't get users automatically</li>
<li>1: get users after every "getTasks" (default)</li>
</ul></li>
<br />
<li>avoidDuplicates
<ul>
<li>0: don't check for duplicates (default)</li>
<li>1: check for duplicates, deny new entries if task exists (exactly)</li>
</ul></li>
<br />
<li>delDeletedLists
<ul>
<li>0: don't delete deleted childrens (default)</li>
<li>1: delete deleted childrens</li>
</ul></li>
<br />
<li>listDivider<br />
set the divider for the Reading listText. Default is ", ".</li>
</ul>
<a name="todoist_Readings"></a>
<h4>Readings</h4>
<ul>
<li>Task_XXX<br />
the tasks are listet as Task_000, Task_001 [...].</li>
<li>Task_XXX_parentID<br />
parent ID of task XXX if not null</li>
<li>Task_XXX_sectionID<br />
section ID of task XXX if not null</li>
<li>Task_XXX_checked<br />
1 when a task with parent_id is checked</li>
<li>Task_XXX_isDeleted<br />
1 when a task with parent_id is deleted</li>
<li>Task_XXX_dueDate<br />
if a task has a due date, this reading should be filled with the date.</li>
<li>Task_XXX_priority<br />
the priority of your task.</li>
<li>Task_XXX_ID<br />
the todoist ID of Task_X.</li>
<li>Task_XXX_completedAt<br />
only for completed Tasks (attribute getCompleted).</li>
<li>Task_XXX_completedById<br />
only for completed Tasks (attribute getCompleted).</li>
<li>Task_XXX_assignedByUid<br />
the user this task was assigned by.</li>
<li>Task_XXX_responsibleUid<br />
the user this task was assigned to.</li>
<li>Task_XXX_order<br />
shows the order no. of the task (attribute showOrder).</li>
<li>User_XXX<br />
the lists users are listet as User_000, User_001 [...].</li>
<li>User_XXX_ID<br />
the users todoist ID.</li>
<li>listText<br />
a comma seperated list of tasks in the specified list. This may be used for TTS, Messages etc.</li>
<li>count<br />
number of Tasks in list.</li>
<li>error<br />
current error. Default is none.</li>
<li>lastCompletedTask<br />
title of the last completed task.</li>
<li>lastCreatedTask<br />
title of the last created task.</li>
<li>lastDeletedTask<br />
title of the last deleted task.</li>
<li>lastError<br />
last Error.</li>
<li>state<br />
state of the todoist-Device</li>
</ul><br />
<a name="todoist_Weblink"></a>
<h4>Weblinks</h4>
FHEMWEB weblink as frontend for one or more. There are 3 different ways to setup a todoist-weblink:<br /><br />
<ul>
<li>Define a simple weblink for a Task list:
<br /><br />
<code>define &lt;NAME&gt; weblink htmlCode {todoist_Html("&lt;TODOIST-DEVCICENAME&gt;")}</code><br /><br /></li>
<li>Define a simple weblink for all active Task lists:
<br /><br />
<code>define &lt;NAME&gt; weblink htmlCode {todoist_Html()}</code><br /><br /></li>
<li>Define a simple weblink for a group of active Task lists:
<br /><br />
<code>define &lt;NAME&gt; weblink htmlCode {todoist_Html('&lt;REGEX-FILTER&gt;')}</code><br /><br /></li>
</ul>
Examples:<br /><br />
<code>define EinkaufslisteWeb weblink htmlCode {todoist_Html("Einkaufsliste")}</code> - single list<br />
<code>define EinkaufslistenWeb weblink htmlCode {todoist_Html()}</code> - all active lists<br />
<code>define PapaLists weblink htmlCode {todoist_Html("Papa_list.*")}</code> - all active lists beginning with "Papa_list"
<br /><br />
<ul>
<li>Tasks can be sorted, updated and added inside the widget. You can reload a list and delete all entries of a list.</li>
<li>To check (close) a task, click the checkbox in front of the completed task.</li>
<li>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<br />
adds it to the new one.</li>
<li>The input field under the actual list can be used for adding tasks to this list. Leaving the field or pressing enter triggers addTask.</li>
<li>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.</li>
<li>You can use parameters like "dueDate" in the input fields like this:
<ul>
<code>Milch dueDate=morgen</code>
</ul>
</li>
<li>Clicking the cross behind every task deletes this one for good.</li>
<li>Clicking the trashcan symbol in the title bar of every list deletes all tasks from the list.</li>
<li>A list can be reloaded by clicking the round arrow symbol in the title bar of every list.</li>
<li>A list is refreshed if a getTasks for this is fired (by interval or manually).</li>
</ul>
</ul>
=end html
=cut