HomeTaskerHow to sync Android alarm with NodeRED

How to sync Android alarm with NodeRED

Sync your Android alarm with anything you want as long as it's NodeRED

I got the Zemismart motorised curtains some time ago (review) and ever since I wanted to open it not based on a schedule, but a time offset created by the Android alarm. I don’t think I have to explain the benefits of that over a fixed schedule. The curtains would open a couple of minutes before your alarm rather than at the fixed schedule. How to sync Android alarm with NodeRED? With Tasker!

From Android Alarm to NodeRED timer

My first prototype used Test Next Alarm action to get the information about the pending alarm, but since the method picks up all sorts of alarms from different apps (clock, calendar etc) it was proven unreliable.

Less useful than you may think

To add the insult to injury, the information about the alarm was inefficient and I wanted to have all alarms, labels, dates and days the alarm is active. Apart from that, I needed to know when the alarm had been updated, disabled or deleted. All this for each alarm instance, for multiple phones. You had no idea, how difficult this task was at first.

I’m pleased to say that I did all that in about 2 days, and now you can obtain the following information in NodeRED:

  • alarm time
  • alarm date
  • repeat schedule
  • alarm state (enabled or not)
  • alarm label*
  • support for unlimited alarms
  • support for multiple phones
  • values ready for NodeRED timers
  • update and delete actions
  • dynamic dashboard

*it’s impossible to obtain the label unless the notification is issued so the closest thing I could extract was how many characters the label has.

If you think the list is impressive and you feel particularly grateful that I got all these grey hair not you, consider my Patreon page, or you can top up my Coffee Jar (PayPal). I would like to thank Jim from Hackspace for helping me out with troubleshooting one nasty issue and vastly improving one of the scripts.

Turns out that LogCat function is pretty handy for that. The new event let me extract the information about the alarms on Android phones and use it to whatever I want to. While the Tasker part of this tutorial is easy, the NodeRED part is probably aimed at advanced users. The good news is, you can download it all, and use it with only minor modifications.

How to sync Android alarm with NodeRED with Tasker

The new, magical option in Tasker is the ability to spy on LogCat entries in Android, these messages reveal what happens in the Android system-wise and we can use it as triggers.

The principle applies to alarms. When an alarm is created, modified or deleted appropriate messages are shared system-wise. The problem with LogCat messages is, that they don’t look even remotely useful in the raw shape.

LogCat – create alarm
1578328399.047  4795  4822 I AlarmClock: Created new alarm instance: AlarmInstance{alarmId=1, id=1, state=SCHEDULED, time=01-07-2020 16:33, vibrate=true, ringtone=content://settings/system/alarm_alert, labelLength=0}
LogCat – update alarm
1578399781.660 23509 23532 I AlarmClock: Updated alarm from: Alarm {id=1, enabled=true, hour=9, minute=20, daysOfWeek=[M T Th F Sa Su], vibrate=true, ringtone=content://settings/system/alarm_alert, labelLength=0, hasWorkflow=false, externalUuid=null, deleteAfterUse=false, instanceIds=[1]} to: Alarm {id=1, enabled=true, hour=9, minute=20, daysOfWeek=[M T Th Sa Su], vibrate=true, ringtone=content://settings/system/alarm_alert, labelLength=0, hasWorkflow=false, externalUuid=null, deleteAfterUse=false, instanceIds=[1]}
LogCat – delete alarm
1578328392.061  4795  4822 I AlarmClock: Removed alarm: Alarm {id=1, enabled=false, hour=16, minute=12, daysOfWeek=[], vibrate=true, ringtone=content://settings/system/alarm_alert, labelLength=0, hasWorkflow=false, externalUuid=null, deleteAfterUse=false, instanceIds=[]}

While the data is there, there is a long way to go before we can actually use it. The exact LogCat entry filer will depend on what app is used to create the alarm. I used Google Clock app, but with small modifications, this would work with 3rd party apps too!

Also if you want to intercept alarm voice commands, double-check the entries too, as my filters work on Pixel 3 but not on Xiaomi Mi9 without modifications.

Proposed Filters

#Alarm Created
AlarmClock: Created new alarm instance
#Alarm Updated
AlarmClock: Updated alarm from: Alarm 
#Alarm Deleted
AlarmClock: Removed alarm: Alarm 

These filters will trigger 3 corresponding actions in Tasker (I could use one task and set of IF filters but it’s more transparent this way). This profile aims to process the raw data on NodeRED, so I’m passing unprocessed data stored in %lc_text.

To sync Android alarm with NodeRED I will send an HTTP POST request. I have avoided hardcoding my server information thanks to my credentials project and this variable method. I would also strongly recommend you to check the 5 min guide to NodeRED security so you don’t leave your server vulnerable to attacks.

https://%HTTPuser:%HTTPpass@%HOMESERVER:1880/alarm

Each action will have a slightly different body, as I want to let my NodeRED know what action took place on my phone, and which phone is reporting:

#Alarm Created
{"time":"%lc_text", "phone": "%PHONE", "type": "create"}
#Alarm Updated
{"time":"%lc_text", "phone": "%PHONE", "type": "update"}
#Alarm Deleted
{"time":"%lc_text", "phone": "%PHONE", "type": "delete"}
TASKER PROJECT: Alarm Sync
Profile: AS Create Alarm 
	Restore: no
	Event: Logcat Entry [ Output Variables:* 
			Component:AlarmClock 
			Filter:Created new alarm instance: ]
Enter: AS Create Alarm 
	
	A1: HTTP Request [  Method:POST URL:https://%HTTPuser:%HTTPpass@%HOMESERVER:1880/alarm 
		Headers: 
		Query Parameters: 
		Body:{"time":"%lc_text", "phone": "%PHONE", "type": "create"} 
		File To Send: 
		File/Directory To Save With Output: 
		Timeout (Seconds):30 
		Trust Any Certificate:On 
		Automatically Follow Redirects:Off ] 

Profile: AS Update Alarm 
	Restore: no
	Event: Logcat Entry [ Output Variables:* 
			Component:AlarmClock 
			Filter:Updated alarm from: ]
Enter: AS Update Alarm 
	
	A1: HTTP Request [  Method:POST URL:https://%HTTPuser:%HTTPpass@%HOMESERVER:1880/alarm 
		Headers: 
		Query Parameters: 
		Body:{"time":"%lc_text", "phone": "%PHONE", "type": "update"} 
		File To Send: 
		File/Directory To Save With Output: 
		Timeout (Seconds):30 
		Trust Any Certificate:On 
		Automatically Follow Redirects:Off ] 

Profile: AS Remove Alarm 
	Restore: no
	Event: Logcat Entry [ Output Variables:* 
			Component:AlarmClock 
			Filter:Removed alarm: Alarm ]
Enter: AS Delete Alarm 
	
	A1: HTTP Request [  Method:POST URL:https://%HTTPuser:%HTTPpass@%HOMESERVER:1880/alarm 
		Headers: 
		Query Parameters: 
		Body:{"time":"%lc_text", "phone": "%PHONE", "type": "delete"} 
		File To Send: 
		File/Directory To Save With Output: 
		Timeout (Seconds):30 
		Trust Any Certificate:On 
		Automatically Follow Redirects:Off ]

NodeRED

This is where the advanced section starts. I learned new things in JavaScript thanks to this tutorial. I hope you will too! If you are new to NodeRED, consider reading this guide to beginners first. Otherwise, things will get confusing.

Because of the %lc_text data is different for each action, I need 3 flows to process the raw input into a useful format. To sync Android alarm with NodeRED easier, I want the values to be also available in separate variables so I can use it any way I want, not just for the dashboard.

To make things complicated, Android will change and update the id of existing alarms, which made my initial approach buggy. There is another issue I had to address. When an alarm id gets deleted, and the id of that deleted alarm was smaller than the biggest id in the array, Android system not only reshuffles the array assigning new id, but creates a new alarm when the id is updated. Long story short, this is a bit of a mess to track.

LogCat text sent contains different data then update, which is confusing, but I can extract and format a timestamp of the alarm. This is very useful, as I can get a date of the next alarm and make the conversions across the timezones. You can see how much “fun” dealing with time is in my sun tracker and personal take on alarm.

Other fields are the alarm ID which I will use to organise the data in my array and label length. The label name is not available, but I can still use the count of the character to distinguish alarms.

FUNCTION NODE: Create Process Data
var x = msg.payload.time;
var phone = msg.payload.phone;

// alarm ID
var z1 = /id=(.*?),/.exec(x);
var id = parseInt(z1[1]);

//alarm time
var z2 = /time=(.*?),/.exec(x);
var reg = /(\d{2})-(\d{2})-(\d{4}) (\d{2}):(\d{2})/;
var alarm = reg.exec(z2);
var dateObject = new Date(  (+alarm[3]),
                            (+alarm[1])-1, // Careful, month starts at 0!
                            (+alarm[2]),
                            (+alarm[4]),
                            (+alarm[5]));
                            
var year    = (+alarm[3]);
var month   = (+alarm[1]);
var day     = (+alarm[2]);
var hours   = (+alarm[4]);
var minutes = (+alarm[5]);

// label length
var z5 = /labelLength=(.*?)\}/.exec(x);
var labellength = parseInt(z5[1]);


msg.payload = { "id": id, 
                "alarm":   dateObject, 
                "label":   labellength, 
                "year":    year, 
                "month":   month, 
                "day":     day, 
                "hours":   hours, 
                "minutes": minutes,
                "phone":   phone,
                "enabled": true};

return msg;

A custom object is sent as JSON (learn more about JSON) to be added to the array. Each phone will have a separated array in which the alarms are stored. This array is also stored as a flow variable, so make sure to preserve the variables if you want that data to survive the reboot. Tasker is also passing information about the phone.

Lastly, I had to hardcode the status of the alarm to "enabled": true as this method doesn’t set that option (something that is done in the update).

A completely formatted payload is sent as JSON to the next node. Before I can store the alarm in a corresponding array, I have to check which phone is sending the data and if the alarm id already exists (remember when I said that a new alarm can be created during the update?). I also used a pad() function to provide a human-readable time (as string) with leading zeros without affecting the integers in minutes and hours. If you are dealing with time in seconds, I already have you covered in this article.

FUNCTION NODE: Create alarm
var alarmsMi9= flow.get("alarmsMi9");
var alarmsPixel3= flow.get("alarmsPixel3");

var hours = msg.payload.hours;
var minutes = msg.payload.minutes;
var hoursDisplay = pad(hours);
var minutesDisplay = pad(minutes);

var year = msg.payload.year;
var month =msg.payload.month;
var day = msg.payload.day;
var daysofweek = msg.payload.daysofweek;
var alarmtime = msg.payload.alarm;

var label = msg.payload.label;
var phone = msg.payload.phone
var enabled = msg.payload.enabled;
var id = msg.payload.id;



if(alarmsMi9 === undefined){
    alarmsMi9 = [];
}
if(alarmsPixel3 === undefined){
    alarmsPixel3 = [];
}

//leading zeros
function pad(n) {return (n < 10) ? ("0" + n) : n;}

if(phone === "Xiaomi Mi 9"){
    var pos = alarmsMi9.map(function(e) { return e.id; }).indexOf(id);
    if(pos === -1){
        var a1 = {"id": id, 
                  "alarm":{ 
                    "hours":        hours,
                    "minutes":      minutes,
                    "daysofweek":   daysofweek,
                    "label":        label,
                    "enabled":      enabled,
                    "alarmDisplay": hoursDisplay +":"+ minutesDisplay,
                    "date":         pad(day) +"-"+ pad(month) +"-"+ year,
                    "alarmtime":    alarmtime}
        };
        alarmsMi9.push(a1);
        flow.set("alarmsMi9", alarmsMi9);
    }
}

if(phone === "Google Pixel 3"){
    var pos = alarmsPixel3.map(function(e) { return e.id; }).indexOf(id);
    if(pos === -1){
        var a2 = {"id": id, 
                  "alarm":{ 
                    "hours":        hours,
                    "minutes":      minutes,
                    "daysofweek":   daysofweek,
                    "label":        label,
                    "enabled":      enabled,
                    "alarmDisplay": hoursDisplay +":"+ minutesDisplay,
                    "date":         pad(day) +"-"+ pad(month) +"-"+ year,
                    "alarmtime":    alarmtime}
        };
        alarmsPixel3.push(a2);
        flow.set("alarmsPixel3", alarmsPixel3);
    }
}

return msg;

In order to add the alarm to the array, I check the index of the id of my alarm (var pos = alarmsMi9.map(function(e) { return e.id; }).indexOf(id)) and I push the alarm object to the alarm array.

When changes are made to the alarm, the update call is made and a new set of details are sent via LogCat. These contain more detailed information about the alarm and need to be processed differently.

This is the part where Jim was super helpful and reduced my regex searches with... a reduce function (that pun, right!?). This very short regex function captures both states of the alarm (I need before and after as id of the alarm changes in specific circumstances) and saves it as JSON formatted object.

//capture 2 update outcomes in regex groups
 var data = x.match(/[^{]{(.?)}[^{]{(.?)}/);
 var extractJson = x => x
             .split(', ')
             .map(x => x.split('='))
             .reduce((p,c) => {
                 p[c[0]]=c[1]; 
                 return p;
             }, {});

I asked Jim, to also create a separate array for daysofweek, so you could check programmatically what days the alarm should go off just by testing its bool value.

FUNCTION NODE: Update Process Data
var x = msg.payload.time;
var phone = msg.payload.phone;

//capture 2 update outcomes in regex groups
var data = x.match(/[^{]*\{(.*?)\}[^{]*\{(.*?)\}/);
var extractJson = x => x
            .split(', ')
            .map(x => x.split('='))
            .reduce((p,c) => {
                p[c[0]]=c[1]; 
                return p;
            }, {});
            
var prev = extractJson(data[1]);
var current = extractJson(data[2]);    

//process daysofweek as an array
var days = {
    'M':  false,
    'T':  false,
    'W':  false,
    'Th': false,
    'F':  false,
    'Sa': false,
    'Su': false,};
    
current.daysOfWeek.replace(/[\[\]]/g, '').split(' ').forEach(x => days[x] = true);

//compose the payload
msg.payload = { "enabled":    current.enabled,
                "id":         parseInt(current.id, 10),
                "prevId":     parseInt(prev.id, 10),
                "hours":      parseInt(current.hour, 10),
                "minutes":    parseInt(current.minute, 10),
                "daysofweek": days,
                "label":      parseInt(current.labelLength, 10),
                "phone":      phone};

return msg;

I'm ending up with string type values, so to make the data usable I will pass the relevant details as integers. Because this update doesn't have information about the date, I'm saving the date before I will override the element of the array: var date1 = alarmsMi9[arraystart].alarm.date. The same functions are in use as in the previous paragraph to add leading 0 and display the time in a better format.

FUNCTION NODE: Update Alarm
var alarmsMi9    = flow.get("alarmsMi9");
var alarmsPixel3 = flow.get("alarmsPixel3");

var hours          = msg.payload.hours;
var minutes        = msg.payload.minutes;
var hoursDisplay   = pad(hours);
var minutesDisplay = pad(minutes);

var id         = msg.payload.id;
var prevId     = msg.payload.prevId;
var daysofweek = msg.payload.daysofweek;
var label      = msg.payload.label;
var phone      = msg.payload.phone;
var enabled    = msg.payload.enabled;

//leading zeros
function pad(n) {return (n < 10) ? ("0" + n) : n;}

if(alarmsMi9 === undefined){
    alarmsMi9 = [];
}
if(alarmsPixel3 === undefined){
    alarmsPixel3 = [];
}
// get daysofweek processed as string
var daysAsString = Object.keys(daysofweek).filter(x => daysofweek[x]).join(' ');

if(phone === "Xiaomi Mi 9"){
    var pos = alarmsMi9.map(function(e) { return e.id; }).indexOf(prevId);
    var date1 = alarmsMi9[pos].alarm.date;
    var alarmtime = alarmsMi9[pos].alarm.alarmtime; 
    var a1 = {"id": id, 
              "alarm":{"hours":             hours,
                       "minutes":           minutes,
                       "daysofweek":        daysofweek,
                       "daysofweekDisplay": daysAsString,
                       "label":             label,
                       "enabled":           enabled,
                       "alarmDisplay":      hoursDisplay +":"+ minutesDisplay,
                       "date":              date1,
                       "alarmtime":         alarmtime}};
                       
    alarmsMi9[pos]= a1;
    flow.set("alarmsMi9", alarmsMi9);
}

if(phone === "Google Pixel 3"){
    var pos = alarmsPixel3.map(function(e) { return e.id; }).indexOf(prevId);
    var date2 = alarmsPixel3[pos].alarm.date;
    var alarmtime = alarmsPixel3[pos].alarm.alarmtime; 
    var a2 = {"id": id, 
              "alarm":{"hours":             hours,
                       "minutes":           minutes,
                       "daysofweek":        daysofweek,
                       "daysofweekDisplay": daysAsString,
                       "label":             label,
                       "enabled":           enabled,
                       "alarmDisplay":      hoursDisplay +":"+ minutesDisplay,
                       "date":              date2,
                       "alarmtime":         alarmtime}};

    alarmsPixel3[pos] = a2;
    flow.set("alarmsPixel3", alarmsPixel3);
}

return msg;

Bear in mind that deleting the alarm is different from deactivating it in the Clock app. The message issued by the LogCat is different, but I only need the phone type to access the correct array and the id to delete the correct entry.

FUNCTION NODE: Delete Process Data
var x1 = msg.payload.time;
var phone = msg.payload.phone;

// alarm ID
var z1 = /id=(.*?),/.exec(x1);
var id = parseInt(z1[1]);

msg.payload = {"id": id, "phone": phone};

return msg;

Before I can delete the right entry, I have to search my array for the correct alarm id and save the index. Once the alarm is identified, I can remove the element from the array and update the array. This array will load again when a new alarm instance is added.

FUNCTION NODE: Delete alarm
var alarmsMi9= flow.get("alarmsMi9");
var alarmsPixel3= flow.get("alarmsPixel3");

var id = msg.payload.id;
var phone = msg.payload.phone


if(phone === "Xiaomi Mi 9"){
    var pos = alarmsMi9.map(function(e) { return e.id; }).indexOf(id);
    delete alarmsMi9[pos];
    flow.set("alarmsMi9", alarmsMi9);
}

if(phone === "Google Pixel 3"){
    var pos = alarmsPixel3.map(function(e) { return e.id; }).indexOf(id);
    delete alarmsPixel3[pos];
    flow.set("alarmsPixel3", alarmsPixel3);
}

return msg;

Process Array & Template node

I found a table template online which I can update with my array, but before I can feed the data to the template node, I have to make sure there are no empty elements and then pass the array as msg.object (I kept as much as I could from the original template).

A neat way to clear any elements that are not needed is to filer the array with this: var arr = array.filter(function(e){return e}); - just be aware "0" elements will fall into that filter too. I'm not planning on having any zeros there so I'm good to go.

FUNCTION NODE: Pass Arrays
var phone =  msg.payload.phone;

//output 1
if(phone === "Xiaomi Mi 9"){
    //array clean up
    var array = flow.get("alarmsMi9");
    var arr = array.filter(function(e){return e});
    flow.set("alarmsMi9", arr);
    msg.options = arr;
    return[msg, null];
}

//output 2
if(phone === "Google Pixel 3"){
    //array clean up
    var array = flow.get("alarmsPixel3");
    var arr = array.filter(function(e){return e});
    flow.set("alarmsPixel3", arr);
    msg.options = arr;
    return[null, msg];
}

The actual template node is:

TEMPLATE NODE:

<style>

table {
color: #333;
font-family: Helvetica, Arial, sans-serif;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
td, th {
border: 1px solid transparent;
/* No more visible border */
height: 30px;
transition: all 0.3s;
/* Simple transition for hover effect */
}
th {
background: #DFDFDF;
/* Darken header a bit */
font-weight: bold;
}
td {
background: #FAFAFA;
}

/* Cells in even rows (2,4,6...) are one color */

tr:nth-child(even) td {
background: #F1F1F1;
}

/* Cells in odd rows (1,3,5...) are another (excludes header cells) */

tr:nth-child(odd) td {
background: #FEFEFE;
}
tr td:hover {
background: #666;
color: #FFF;
}

/* Hover cell effect! */

.animate-enter,
.animate-leave
{
-webkit-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
-moz-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
-ms-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
-o-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
position: relative;
display: block;
}

.animate-enter.animate-enter-active,
.animate-leave {
opacity: 1;
top: 0;
height: 30px;
}

.animate-leave.animate-leave-active,
.animate-enter {
opacity: 0;
top: -50px;
height: 0px;
}

.container
{
max-height: 800px;
overflow-y: scroll;
overflow-x: hidden;
}
</style>

<div>

<div class="container" ng-app="sortApp">

<table>
<thead>
<tr style="width:100%">
<td>
<a href="#">
Alarm #
</a>
</td>
<td>
<a href="#" ng-click="sortType = '(alarm.alarmDisplay - 0)'; sortReverse = !sortReverse">
Time
<span ng-show="sortType == '(alarm.alarmDisplay - 0)' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == '(alarm.alarmDisplay - 0)' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td>
<a href="#" ng-click="sortType = '(alarm.date - 0)'; sortReverse = !sortReverse">
Date of next alarm
<span ng-show="sortType == '(alarm.date - 0)' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == '(alarm.date - 0)' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td>
<a href="#" ng-click="sortType = '(alarm.date -0)'; sortReverse = !sortReverse">
Repeats on
<span ng-show="sortType == '(alarm.date -0)' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == '(alarm.date -0)' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td>
<a href="#" ng-click="sortType = '(alarm.label -0)'; sortReverse = !sortReverse">
Label
<span ng-show="sortType == '(alarm.label -0)' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == '(alarm.label -0)' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td>
<a href="#" ng-click="sortType = 'alarm.enabled'; sortReverse = !sortReverse">
Status
<span ng-show="sortType == 'alarm.enabled' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'alarm.enabled' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>

</tr>
</thead>
<tbody>
<tr ng-repeat="user in msg.options | orderBy:sortType:sortReverse | filter:search track by $index" ng-click="msg.payload = user;send(msg);" style="width:100%" flex>
<td><b ng-bind="$index+1"></b></td>
<td ng-bind="user.alarm.alarmDisplay"></td>
<td ng-bind="user.alarm.date"></td>
<td ng-bind="user.alarm.daysofweekDisplay"></td>
<td ng-bind="user.alarm.label"></td>
<td ng-bind="user.alarm.enabled"></td>
</tr>
</tbody>
</table>

</div>
</div>
</body></html>

 

Conclusion

What's not to like? I learned a lot from this project and I'm really happy I was able to overcome all the issues. The process is instant, gives you relevant information to create timed events in NodeRED! What's next? I will consider making the same subset of data available locally to Tasker, as there is no simple way to obtain the alarm information, but that's the subject for the next tutorial. In the meantime, you can check this article out, it's about tracking who is home. Consider following me on social media if you want to know when this happens. If you have any questions about this project, feel free to leave a comment in this Reddit thread.

Project Download

Download project files here. Bear in mind that Patreon supporters have early access to project files and videos.

PayPal

Nothing says "Thank you" better than keeping my coffee jar topped up!

Patreon

Support me on Patreon and get an early access to tutorial files and videos.

image/svg+xml

Bitcoin (BTC)

Use this QR to keep me caffeinated with BTC: 1FwFqqh71mUTENcRe9q4s9AWFgoc8BA9ZU

New to Tasker?

Tasker Quick Start – Getting started with Tasker

0
From newb to not so newbie in 10 min

Best Tasker Projects

How to use Raspberry PI as WOL (wake on lan) server

0
While you could wake up your PC from a mobile directly, having a dedicated server capable of doing so is the best solution. The reason is simple. You can hook up as many devices as you wish with a single endpoint. This is why Raspberry Pi is perfect for this.

How to wake on LAN computers and put it to sleep with Power Menu,...

0
How to Wake on LAN properly via Android, Alexa, Google Assistant and Web

7 awesome Bluetooth keyboard shortcuts for Android

0
7 unique Android shortcuts that you can add to any Bluetooth keyboard.

Smart overnight charging with Tasker

0
Still keeping your phone plugged in overnight? Try smarter overnight charging with this profile

One thing that Join app can’t do and how to fix it with Tasker

0
It's not possible to share the clipboard automatically between join accounts registered to 2 different emails. But you can fix this with tasker.

Essential Guides

Tasker: Seconds into DD:HH:MM:SS (dynamic)

0
It's time to.... ok it's a pun, but I will show you how to master time and convert seconds to DD:HH:MM:SS dynamically

4 ways to organise Tasker projects

0
Keep your Tasker tidy!

A better way to store Tasker credentials

0
The more clever way of managing credentials

Annoyed with dozens of AutoApps populating your app drawer? Here is a fix!

0
Clear your app drawer from the clutter in seconds

Putting AutoTools pie chart to a good use – SSID logger

0
Who wants a piece of the pie (chart)?