Suggested code changes from philipabbey

1. attribute is option, so needs a different template in the API call when absent.
2. Automatically derive the format string from the picker step value for any precision of step.
3. Changed all Lang.String representations of numbers to Lang.Number or Lang.Float. I'm keen to remove the use of strings to hold a numeric value.
4. Tidied up and completed some code comments.
5. Adjusted the JSON schema definition. This is still not finished as the 'picker' object is required for 'numeric' menu items and must not be present for the others. Additional schema changes are required for greater precision.
6. Moved fields over from 'data' to 'picker'.
This commit is contained in:
Philip Abbey
2025-10-29 14:26:02 +00:00
parent 2cd171637c
commit a5ddb65512
8 changed files with 223 additions and 207 deletions

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
// P A Abbey & J D Abbey & @thmichel, 13 October 2025
//
//-----------------------------------------------------------------------------------
@@ -17,7 +17,6 @@ using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
//! a Home Assistant Template.
//
@@ -28,9 +27,9 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
private var mExit as Lang.Boolean;
private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary?;
private var mValue as Lang.String?;
private var mFormatString as Lang.String="%.1f";
private var mPicker as Lang.Dictionary?;
private var mValue as Lang.Number or Lang.Float = 0;
private var mFormatString as Lang.String = "%d";
//! Class Constructor
//!
@@ -51,6 +50,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
template as Lang.String,
service as Lang.String?,
data as Lang.Dictionary?,
picker as Lang.Dictionary,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
@@ -62,33 +62,37 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
) {
mService = service;
mData = data;
mPicker = picker;
mExit = options[:exit];
mConfirm = options[:confirm];
mPin = options[:pin];
mLabel = label;
mHomeAssistantService = haService;
var val = data.get("display_format");
if (val != null) {
mFormatString = val.toString();
}
else {
mFormatString = "%.1f";
}
HomeAssistantMenuItem.initialize(
label,
template,
{
:alignment => options[:alignment],
:icon => options[:icon]
}
);
}
);
if (picker != null) {
var s = picker["step"];
if (s != null) {
var step = s.toFloat() as Lang.Float;
var dp = 0;
while (step < 1.0) {
step *= 10;
dp++;
// Assigned inside the loop and in each iteration to avoid clobbering the default '%d'.
mFormatString = "%." + dp.toString() + "f";
}
}
}
}
function callService() as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) {
@@ -98,10 +102,10 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
WatchUi.pushView(
pinConfirmationView,
new HomeAssistantPinConfirmationDelegate({
:callback => method(:onConfirm),
:pin => pin,
:state => false,
:view => pinConfirmationView,
:callback => method(:onConfirm),
:pin => pin,
:state => false,
:view => pinConfirmationView,
}),
WatchUi.SLIDE_IMMEDIATE
);
@@ -139,62 +143,58 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
}
}
//! Callback function after the menu items selection has been (optionally) confirmed.
//!
//! @param b Ignored. It is included in order to match the expected function prototype of the callback method.
//
function onConfirm(b as Lang.Boolean) as Void {
//mHomeAssistantService.call(mService, {"entity_id" => mData.get("entity_id").toString(),mData.get("valueLabel").toString() => mValue}, mExit);
var dataAttribute = mData.get("data_attribute");
var dataAttribute = mPicker["data_attribute"];
if (dataAttribute == null) {
//return without call service if no data attribute is set to avoid crash
WatchUi.popView(WatchUi.SLIDE_RIGHT);
return;
}
var entity_id = mData.get("entity_id");
var entity_id = mData["entity_id"];
if (entity_id == null) {
//return without call service if no entity_id is set to avoid crash
WatchUi.popView(WatchUi.SLIDE_RIGHT);
return;
}
mHomeAssistantService.call(mService, {"entity_id" => entity_id.toString(),dataAttribute.toString() => mValue}, mExit);
mHomeAssistantService.call(
mService,
{
"entity_id" => entity_id.toString(),
dataAttribute.toString() => mValue
},
mExit
);
WatchUi.popView(WatchUi.SLIDE_RIGHT);
}
//! Return a toggle menu item's state template.
//! Return a numeric menu item's fetch state template.
//!
//! @return A string with the menu item's template definition (or null).
//
function getNumericTemplate() as Lang.String? {
var entity_id = mData.get("entity_id");
if (entity_id != null) {
return "{{state_attr('" + entity_id.toString() + "','" + mData.get("attribute").toString() +"')}}";
}
return null;
}
function updateNumericState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
mValue="0";
return;
} else if(data instanceof Lang.String) {
mValue=data;
var entity_id = mData["entity_id"];
var attribute = (mPicker["attribute"] as Lang.String);
if (entity_id == null) {
return null;
} else {
// Catch possible error
mValue="0";
if (attribute == null) {
return "{{states('" + entity_id.toString() + "')}}";
} else {
return "{{state_attr('" + entity_id.toString() + "','" + attribute + "')}}";
}
}
}
//! Update the menu item's sub label to display the template rendered by Home Assistant.
//!
//! @param data The rendered template (typically a string) to be placed in the sub label. This may
//! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such.
//
function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
public function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
if (data == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(data instanceof Lang.Float) {
@@ -203,31 +203,45 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
} else if(data instanceof Lang.Number) {
var f = data.toFloat() as Lang.Float;
setSubLabel(f.format(mFormatString));
} else if (data instanceof Lang.String){
} else if (data instanceof Lang.String) {
// This should not happen
setSubLabel(data);
}
else {
} else {
// The template must return a Float on Numeric value, or the item cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
//! Set the mValue value.
//! Set the Picker's value. Needed to set new value via the Service call
//!
//! Needed to set new value via the Service call
//! @param value New value to set.
//
function setValue(value as Lang.String) as Void {
public function setValue(value as Lang.Number or Lang.Float) as Void {
mValue = value;
}
function getValue() as Lang.String {
//! Get the Picker's value.
//!
//! Needed to set new value via the Service call
//
public function getValue() as Lang.Number or Lang.Float {
return mValue;
}
function getData() as Lang.Dictionary {
//! Get the original 'data' field supplied by the JSON menu.
//!
//! @return Dictionary containing the 'data' field.
//
public function getData() as Lang.Dictionary {
return mData;
}
// Get the original 'picker' field supplied by the JSON menu.
//!
//! @return Dictionary containing the 'picker' field.
//
public function getPicker() as Lang.Dictionary {
return mPicker;
}
}