In preparation for a future migration to openHAB 4 where I understand UOM requirements will be more rigorously enforced, I have been reviewing how I am using UOM in some of my rules. I use the Location from the iCloud binding in Blockly with the Distance From block in the Geographic information system library. I also use the Location Accuracy from the binding.
At one time my wife’s iphone Location accuracy would fluctuate, so my rules add the Location Accuracy to a designated “at home” radius, which is obtained from a Number:Length Item that is set every time openHAB starts to 75 m (it usually is restored by the persistence).
The Location Accuracy Channel in the binding is a “number”, not a Number:Length, according to the binding documentation and the UI.
I tried linking a new Number:Length Item to the channel in the Thing UI. Since I am in the USA, I expected the units would default to Feet. But after I linked it (and before I did anything to the UOM Metadata or State Description, I was surprised to see that the state of the item was in m:
To make my original rule work, I had to set variables to the states of the items, then I could add them:
(The make block is because I am doing the same thing on 6 different iCloud devices, and get information from the triggering item to set the parts of the item name).
I stick a " m" on the end of the variable, and when I post it to a Number:Length item, everything works. If I set the State Description to us US Customary Units (ft), the conversion happens as expected.
While what I have has worked, I feel like I’m living on the edge and it could very well not work in openHAB 4.
I prefer to use UOM whenever possible. Remember that NASA’s Mars Climate Orbiter crashed into Mars because NASA used SI units and the Jet Propulsion Laboratory used US Customary Units, and someone failed to convert when they should have.
I think that I can probably use my new Location Accuracy Number:Length Item and change how I am using the Distance From block. If I post the results of the distance from (which although in meters is dimensionless) to a Number:Length Item that has an openHAB 4 UoM Metadata or openHAB 3 State Description of “m”, then I should be able to use the items, now both with UOM, and not have to use variables, and the calculations and comparison operations should all work as expected.
While I believe the underlying Java DistanceFrom method returns a dimensionless number (that is in meters), perhaps the Blockly Distance From Block could have a dropdown where one could choose, dimensionless or specify a unit.
I’m open to doing things in a better way that is more likely to stand the test of time than my current approach.
Here is the code generated by Blockly for the entire rule.
var List_of_Item_Name_Components, User, Device, Device_Distance_From_Ossineke, iCloud_Device_Presence_Radius, Device_Accuracy, iCloud_Device_Accuracy_Plus_Presence_Radius, New_Presence_State, iPhone_Location, Watch_Location;
var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
function geo_distance(position1, position2) {
var lat1 = position1.latitude;
var lat2 = position2.latitude;
var lon1 = position1.longitude;
var lon2 = position2.longitude;
var R = 6371e3; // metres
var o1 = lat1 * Math.PI/180;
var o2 = lat2 * Math.PI/180;
var Ao = (lat2-lat1) * Math.PI/180;
var Ab = (lon2-lon1) * Math.PI/180;
var a = Math.sin(Ao/2) * Math.sin(Ao/2) +
Math.cos(o1) * Math.cos(o2) *
Math.sin(Ab/2) * Math.sin(Ab/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
function geo_position(latitude, longitude) {
return {'latitude': latitude, 'longitude': longitude};
}
var chronoUnit = Java.type("java.time.temporal.ChronoUnit");
var dtf = Java.type("java.time.format.DateTimeFormatter");
var zdt = Java.type("java.time.ZonedDateTime");
function getZonedDateTime(datetime) {
datetime = String(datetime).replace('T', ' ')
var regex_time_min = /^\d{2}:\d{2}$/;
var regex_time_sec = /^\d{2}:\d{2}:\d{2}$/;
var regex_date = /^\d{4}-\d{2}-\d{2}$/;
var regex_date_time_min = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/;
var regex_date_time_sec = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
var regex_date_time_sec_tz = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/;
var regex_date_time_ms = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{3}$/;
var regex_date_time_us = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{6}$/;
var regex_date_time_ms_tz = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/;
var regex_date_time_us_tz = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{6}[+-]\d{2}:\d{2}$/;
var regex_oh = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4}$/;
var now = zdt.now();
var now_year = now.getYear();
var now_month = now.getMonthValue();
var now_day = now.getDayOfMonth();
var today = '' + now_year;
today += '-' + ('0' + now_month).slice(-2);
today += '-' + ('0' + now_day).slice(-2)+' ';
switch (true) {
case regex_time_min.test(datetime): return zdt.parse(today + datetime + ':00+00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ssz'));
case regex_time_sec.test(datetime): return zdt.parse(today + datetime + '+00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ssz'));
case regex_date.test(datetime): return zdt.parse(datetime + ' 00:00:00+00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ssz'));
case regex_date_time_min.test(datetime): return zdt.parse(datetime + ':00+00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ssz'));
case regex_date_time_sec.test(datetime): return zdt.parse(datetime + '+00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ssz'));
case regex_date_time_sec_tz.test(datetime): return zdt.parse(datetime, dtf.ofPattern('yyyy-MM-dd HH:mm:ssz'));
case regex_date_time_ms.test(datetime): return zdt.parse(datetime + ' +00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ss.SSS z'));
case regex_date_time_us.test(datetime): return zdt.parse(datetime + ' +00:00', dtf.ofPattern('yyyy-MM-dd HH:mm:ss.SSSSSS z'));
case regex_date_time_ms_tz.test(datetime): return zdt.parse(datetime, dtf.ofPattern('yyyy-MM-dd HH:mm:ss.SSSSz'));
case regex_date_time_us_tz.test(datetime): return zdt.parse(datetime, dtf.ofPattern('yyyy-MM-dd HH:mm:ss.SSSSSSSz'));
case regex_oh.test(datetime): return zdt.parse(datetime.slice(0,26) + ':' + datetime.slice(26,28), dtf.ofPattern('yyyy-MM-dd HH:mm:ss.SSSSz'));
default: return zdt.parse(datetime);
}
}
function createZonedDateTime(year, month, day, hour, minute, second, nano, offsetString, timezoneString) {
stringToParse = '' + year;
stringToParse += '-' + ('0' + month).slice(-2);
stringToParse += '-' + ('0' + day).slice(-2);
stringToParse += 'T' + ('0' + hour).slice(-2);
stringToParse += ':' + ('0' + minute).slice(-2);
stringToParse += ':' + ('0' + second).slice(-2);
stringToParse += '.' + nano + offsetString + '[' + timezoneString + ']';
return zdt.parse(stringToParse, dtf.ISO_ZONED_DATE_TIME);
}
List_of_Item_Name_Components = event.itemName.split('_');
User = List_of_Item_Name_Components[2];
Device = List_of_Item_Name_Components[3];
logger.info((['Rule: ',ctx.ruleUID,' triggered by Item: ',event.itemName,' User: ',User,' Device: ',Device].join('')));
if (event.itemState != 'UNDEF') {
Device_Distance_From_Ossineke = String(geo_distance(event.itemState, geo_position(XX.093643, -YY.337313))) + ' m';
events.postUpdate(([User, Device, 'iCloud_Distance_From_Ossineke'].join('_')), Device_Distance_From_Ossineke);
}
iCloud_Device_Presence_Radius = itemRegistry.getItem('iCloudHomeRadius').getState();
Device_Accuracy = itemRegistry.getItem((['Most_Recent', User, Device, 'Accuracy'].join('_'))).getState();
iCloud_Device_Accuracy_Plus_Presence_Radius = String(Device_Accuracy + iCloud_Device_Presence_Radius) + ' m';
logger.info(iCloud_Device_Accuracy_Plus_Presence_Radius);
events.postUpdate(([User, Device, 'iCloud_Presence_Radius_Plus_Accuracy'].join('_')), iCloud_Device_Accuracy_Plus_Presence_Radius);
New_Presence_State = itemRegistry.getItem(([User, Device, 'iCloud_Distance_From_Ossineke'].join('_'))).getState() <= itemRegistry.getItem(([User, Device, 'iCloud_Presence_Radius_Plus_Accuracy'].join('_'))).getState() ? 'ON' : 'OFF';
if (itemRegistry.getItem(([User, Device, 'iCloud_Is_At_Ossineke'].join('_'))).getState() != New_Presence_State) {
events.postUpdate(([User, Device, 'iCloud_Is_At_Ossineke'].join('_')), New_Presence_State);
}
iPhone_Location = itemRegistry.getItem((['Most_Recent', User, 'iPhone', 'Location'].join('_'))).getState();
Watch_Location = itemRegistry.getItem((['Most_Recent', User, 'Watch', 'Location'].join('_'))).getState();
logger.info((['Time between iphone and watch location updates: ',chronoUnit.SECONDS.between((itemRegistry.getItem((['Most_Recent', User, 'iPhone', 'Last_Location_Update'].join('_'))).getState().getZonedDateTime()),(itemRegistry.getItem((['Most_Recent', User, 'Watch', 'Last_Location_Update'].join('_'))).getState().getZonedDateTime())),' for user: ',User,' with iPhone source: ',itemRegistry.getItem((['Most_Recent', User, 'iPhone', 'Source'].join('_'))).getState(),' and Watch iPhone source: ',itemRegistry.getItem((['Most_Recent', User, 'iPhone', 'Source'].join('_'))).getState(),' and Watch and iPhone are separated by: ',geo_distance(iPhone_Location, Watch_Location),' Watch home state: ',itemRegistry.getItem(([User, 'Watch', 'iCloud_Is_At_Ossineke'].join('_'))).getState(),' iPhone home state: ',itemRegistry.getItem(([User, 'iPhone', 'iCloud_Is_At_Ossineke'].join('_'))).getState(),' m/s: ',(chronoUnit.SECONDS.between((itemRegistry.getItem((['Most_Recent', User, 'iPhone', 'Last_Location_Update'].join('_'))).getState().getZonedDateTime()),(itemRegistry.getItem((['Most_Recent', User, 'Watch', 'Last_Location_Update'].join('_'))).getState().getZonedDateTime()))) != 0 ? (geo_distance(iPhone_Location, Watch_Location)) / (chronoUnit.SECONDS.between((itemRegistry.getItem((['Most_Recent', User, 'iPhone', 'Last_Location_Update'].join('_'))).getState().getZonedDateTime()),(itemRegistry.getItem((['Most_Recent', User, 'Watch', 'Last_Location_Update'].join('_'))).getState().getZonedDateTime()))) : 0].join('')));
if (iPhone_Location != 'UNDEF' && Watch_Location != 'UNDEF') {
events.postUpdate(([User, 'Watch_Distance_From_iPhone'].join('_')), (String(geo_distance(iPhone_Location, Watch_Location)) + ' m'));
if (itemRegistry.getItem(([User, 'Watch_Distance_From_iPhone'].join('_'))).getState() <= itemRegistry.getItem('iCloudHomeRadius').getState()) {
if (itemRegistry.getItem(([User, 'Watch_Near_iPhone'].join('_'))).getState() != 'ON') {
events.postUpdate(([User, 'Watch_Near_iPhone'].join('_')), 'ON');
}
} else {
if (itemRegistry.getItem(([User, 'Watch_Near_iPhone'].join('_'))).getState() != 'OFF') {
events.postUpdate(([User, 'Watch_Near_iPhone'].join('_')), 'ON');
}
}
} else {
events.postUpdate(([User, 'Watch_Distance_From_iPhone'].join('_')), 'UNDEF');
}
logger.info((['Verify Watch distance from iPhone Calculation. Watch Distance from iPhone: ',itemRegistry.getItem(([User, 'Watch_Distance_From_iPhone'].join('_'))).getState(),' Radius: ',itemRegistry.getItem('iCloudHomeRadius').getState(),' Are close? ',itemRegistry.getItem(([User, 'Watch_Near_iPhone'].join('_'))).getState()].join('')));
And a little about my set up:
runtimeInfo:
version: 3.4.4
buildString: Release Build
locale: en-US
systemInfo:
configFolder: /etc/openhab
userdataFolder: /var/lib/openhab
logFolder: /var/log/openhab
javaVersion: 17.0.6
javaVendor: Raspbian
osName: Linux
osVersion: 6.1.21-v8+
osArchitecture: arm
availableProcessors: 4
freeMemory: 9454608
totalMemory: 259522560
startLevel: 100
bindings:
- amazonechocontrol
- androidtv
- astro
- denonmarantz
- exec
- gpstracker
- icalendar
- icloud
- ipcamera
- irobot
- logreader
- mail
- mqtt
- myq
- network
- networkupstools
- ntp
- openweathermap
- remoteopenhab
- ring
- roku
- tplinksmarthome
- tuya
- zwave
clientInfo:
device:
ios: false
android: false
androidChrome: false
desktop: true
iphone: false
ipod: false
ipad: false
edge: false
ie: false
firefox: false
macos: false
windows: true
cordova: false
phonegap: false
electron: false
nwjs: false
webView: false
webview: false
standalone: false
os: windows
pixelRatio: 1.5
prefersColorScheme: light
isSecureContext: false
locationbarVisible: true
menubarVisible: true
navigator:
cookieEnabled: true
deviceMemory: N/A
hardwareConcurrency: 8
language: en-US
languages:
- en-US
- en
onLine: true
platform: Win32
screen:
width: 1707
height: 1067
colorDepth: 24
support:
touch: false
pointerEvents: true
observer: true
passiveListener: true
gestures: false
intersectionObserver: true
themeOptions:
dark: light
filled: true
pageTransitionAnimation: default
bars: filled
homeNavbar: default
homeBackground: default
expandableCardAnimation: default
userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/114.0.0.0 Safari/537.36
timestamp: 2023-06-26T21:36:54.509Z
Continuing to experiment, I was able to directly post the Distance From output to the Number:Length item, with the expected result. But I am unable to add the two Number:Length items using the Blockly Math operations box. I have tried dragging the State of Item, but it won’t let me attach it to the expected math input.