mirror of
				https://github.com/house-of-abbey/GarminHomeAssistant.git
				synced 2025-11-04 08:58:13 +00:00 
			
		
		
		
	add TemplateMenuItem
This commit is contained in:
		@@ -25,15 +25,17 @@
 | 
			
		||||
    "template": {
 | 
			
		||||
      "type": "object",
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "entity": { "$ref": "#/$defs/entity" },
 | 
			
		||||
        "name": { "title": "Your familiar name", "type": "string" },
 | 
			
		||||
        "content": { "title": "What to display (template)", "type": "string" },
 | 
			
		||||
        "type": {
 | 
			
		||||
          "title": "Menu item type",
 | 
			
		||||
          "description": "One of 'tap', 'template', 'toggle' or 'group'.",
 | 
			
		||||
          "const": "template"
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
        "tap_action": { "$ref": "#/$defs/action" }
 | 
			
		||||
      },
 | 
			
		||||
      "required": ["name", "content", "type"],
 | 
			
		||||
      "required": ["name", "entity", "content", "type"],
 | 
			
		||||
      "additionalProperties": false
 | 
			
		||||
    },
 | 
			
		||||
    "tap": {
 | 
			
		||||
@@ -80,9 +82,9 @@
 | 
			
		||||
      "type": "array",
 | 
			
		||||
      "items": {
 | 
			
		||||
        "oneOf": [
 | 
			
		||||
          { "$ref": "#/$defs/tap" },
 | 
			
		||||
          { "$ref": "#/$defs/template" },
 | 
			
		||||
          { "$ref": "#/$defs/toggle" },
 | 
			
		||||
          { "$ref": "#/$defs/template" },
 | 
			
		||||
          { "$ref": "#/$defs/tap" },
 | 
			
		||||
          { "$ref": "#/$defs/menu" }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@
 | 
			
		||||
    <string id="UnhandledHttpErr">HTTP request returned error code = </string>
 | 
			
		||||
    <string id="TrailingSlashErr">API URL must not have a trailing slash '/'</string>
 | 
			
		||||
    <string id="WebhookFailed">Failed to register Webhook</string>
 | 
			
		||||
    <string id="TemplateError">Failed to render template</string>
 | 
			
		||||
    <string id="Available" scope="glance">Available</string>
 | 
			
		||||
    <string id="Checking" scope="glance">Checking...</string>
 | 
			
		||||
    <string id="Unavailable" scope="glance">Unavailable</string>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ using Toybox.Lang;
 | 
			
		||||
class Globals {
 | 
			
		||||
    // Enable printing of messages to the debug console (don't make this a Property
 | 
			
		||||
    // as the messages can't be read from a watch!)
 | 
			
		||||
    static const scDebug        = false;
 | 
			
		||||
    static const scDebug        = true;
 | 
			
		||||
    static const scAlertTimeout = 2000; // ms
 | 
			
		||||
    static const scTapTimeout   = 1000; // ms
 | 
			
		||||
    // Time to let the existing HTTP responses get serviced after a
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ class HomeAssistantMenuItemFactory {
 | 
			
		||||
            :locX  => WatchUi.LAYOUT_HALIGN_CENTER,
 | 
			
		||||
            :locY  => WatchUi.LAYOUT_VALIGN_CENTER
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        mHomeAssistantService = new HomeAssistantService();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -66,6 +67,24 @@ class HomeAssistantMenuItemFactory {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function template(
 | 
			
		||||
        label      as Lang.String or Lang.Symbol,
 | 
			
		||||
        identifier as Lang.Object or Null,
 | 
			
		||||
        template   as Lang.String or Null,
 | 
			
		||||
        service    as Lang.String or Null,
 | 
			
		||||
        confirm    as Lang.Boolean
 | 
			
		||||
    ) as WatchUi.MenuItem {
 | 
			
		||||
        return new HomeAssistantTemplateMenuItem(
 | 
			
		||||
            label,
 | 
			
		||||
            identifier,
 | 
			
		||||
            template,
 | 
			
		||||
            service,
 | 
			
		||||
            confirm,
 | 
			
		||||
            mMenuItemOptions,
 | 
			
		||||
            mHomeAssistantService
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function tap(
 | 
			
		||||
        label      as Lang.String or Lang.Symbol,
 | 
			
		||||
        identifier as Lang.Object or Null,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										198
									
								
								source/HomeAssistantTemplateMenuItem.mc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								source/HomeAssistantTemplateMenuItem.mc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
//-----------------------------------------------------------------------------------
 | 
			
		||||
//
 | 
			
		||||
// Distributed under MIT Licence
 | 
			
		||||
//   See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
 | 
			
		||||
//
 | 
			
		||||
//-----------------------------------------------------------------------------------
 | 
			
		||||
//
 | 
			
		||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
 | 
			
		||||
// 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, 12 January 2024
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// Description:
 | 
			
		||||
//
 | 
			
		||||
// Rendering a Home Assistant Template.
 | 
			
		||||
// 
 | 
			
		||||
// Reference:
 | 
			
		||||
//  * https://developers.home-assistant.io/docs/api/rest/
 | 
			
		||||
//  * https://www.home-assistant.io/docs/configuration/templating
 | 
			
		||||
//
 | 
			
		||||
//-----------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Toybox.Lang;
 | 
			
		||||
using Toybox.WatchUi;
 | 
			
		||||
using Toybox.Graphics;
 | 
			
		||||
 | 
			
		||||
class HomeAssistantTemplateMenuItem extends WatchUi.MenuItem {
 | 
			
		||||
    private var mHomeAssistantService as HomeAssistantService;
 | 
			
		||||
    private var mTemplate             as Lang.String;
 | 
			
		||||
    private var mService              as Lang.String or Null;
 | 
			
		||||
    private var mConfirm              as Lang.Boolean;
 | 
			
		||||
 | 
			
		||||
    function initialize(
 | 
			
		||||
        label      as Lang.String or Lang.Symbol,
 | 
			
		||||
        identifier as Lang.Object or Null,
 | 
			
		||||
        template   as Lang.String,
 | 
			
		||||
        service    as Lang.String or Null,
 | 
			
		||||
        confirm    as Lang.Boolean,
 | 
			
		||||
        options    as {
 | 
			
		||||
            :alignment as WatchUi.MenuItem.Alignment,
 | 
			
		||||
            :icon      as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
 | 
			
		||||
        } or Null,
 | 
			
		||||
        haService  as HomeAssistantService
 | 
			
		||||
    ) {
 | 
			
		||||
        WatchUi.MenuItem.initialize(
 | 
			
		||||
            label,
 | 
			
		||||
            null,
 | 
			
		||||
            identifier,
 | 
			
		||||
            options
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        mHomeAssistantService = haService;
 | 
			
		||||
        mTemplate             = template;
 | 
			
		||||
        mService              = service;
 | 
			
		||||
        mConfirm              = confirm;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function callService() as Void {
 | 
			
		||||
        if (mConfirm) {
 | 
			
		||||
            WatchUi.pushView(
 | 
			
		||||
                new HomeAssistantConfirmation(),
 | 
			
		||||
                new HomeAssistantConfirmationDelegate(method(:onConfirm)),
 | 
			
		||||
                WatchUi.SLIDE_IMMEDIATE
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            onConfirm();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onConfirm() as Void {
 | 
			
		||||
        if (mService != null) {
 | 
			
		||||
            mHomeAssistantService.call(mIdentifier as Lang.String, mService);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Callback function after completing the GET request to fetch the status.
 | 
			
		||||
    // Terminate updating the toggle menu items via the chain of calls for a permanent network
 | 
			
		||||
    // error. The ErrorView cancellation will resume the call chain.
 | 
			
		||||
    //
 | 
			
		||||
    function onReturnGetState(responseCode as Lang.Number, data as Lang.String) as Void {
 | 
			
		||||
        if (Globals.scDebug) {
 | 
			
		||||
            System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode);
 | 
			
		||||
            System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var status = RezStrings.getUnavailable();
 | 
			
		||||
        switch (responseCode) {
 | 
			
		||||
            case Communications.BLE_HOST_TIMEOUT:
 | 
			
		||||
            case Communications.BLE_CONNECTION_UNAVAILABLE:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getNoPhone() + ".");
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case Communications.BLE_QUEUE_FULL:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getApiFlood());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case Communications.NETWORK_REQUEST_TIMED_OUT:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getNoResponse());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getNoJson());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
 | 
			
		||||
                }
 | 
			
		||||
                var myTimer = new Timer.Timer();
 | 
			
		||||
                // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
 | 
			
		||||
                myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
 | 
			
		||||
                // Revert status
 | 
			
		||||
                status = getApp().getApiStatus();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 404:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getApiUrlNotFound());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 400:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 400, bad request. Template error.");
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getTemplateError());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 200:
 | 
			
		||||
                status = RezStrings.getAvailable();
 | 
			
		||||
                setSubLabel(data);
 | 
			
		||||
                requestUpdate();
 | 
			
		||||
                ErrorView.unShow();
 | 
			
		||||
                // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
 | 
			
		||||
                getApp().updateNextMenuItem();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                if (Globals.scDebug) {
 | 
			
		||||
                    System.println("HomeAssistantTemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
 | 
			
		||||
                }
 | 
			
		||||
                ErrorView.show(RezStrings.getUnhandledHttpErr() + responseCode);
 | 
			
		||||
        }
 | 
			
		||||
        getApp().setApiStatus(status);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getState() as Void {
 | 
			
		||||
        if (! System.getDeviceSettings().phoneConnected) {
 | 
			
		||||
            if (Globals.scDebug) {
 | 
			
		||||
                System.println("HomeAssistantTemplateMenuItem getState(): No Phone connection, skipping API call.");
 | 
			
		||||
            }
 | 
			
		||||
            ErrorView.show(RezStrings.getNoPhone() + ".");
 | 
			
		||||
            getApp().setApiStatus(RezStrings.getUnavailable());
 | 
			
		||||
        } else if (! System.getDeviceSettings().connectionAvailable) {
 | 
			
		||||
            if (Globals.scDebug) {
 | 
			
		||||
                System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
 | 
			
		||||
            }
 | 
			
		||||
            ErrorView.show(RezStrings.getNoInternet() + ".");
 | 
			
		||||
            getApp().setApiStatus(RezStrings.getUnavailable());
 | 
			
		||||
        } else {
 | 
			
		||||
            var url = Settings.getApiUrl() + "/template";
 | 
			
		||||
            if (Globals.scDebug) {
 | 
			
		||||
                System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'");
 | 
			
		||||
            }
 | 
			
		||||
            Communications.makeWebRequest(
 | 
			
		||||
                url,
 | 
			
		||||
                {
 | 
			
		||||
                    "template" => mTemplate
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    :method  => Communications.HTTP_REQUEST_METHOD_POST,
 | 
			
		||||
                    :headers => {
 | 
			
		||||
                        "Content-Type"  => Communications.REQUEST_CONTENT_TYPE_JSON,
 | 
			
		||||
                        "Authorization" => "Bearer " + Settings.getApiKey()
 | 
			
		||||
                    },
 | 
			
		||||
                    :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_TEXT_PLAIN
 | 
			
		||||
                },
 | 
			
		||||
                method(:onReturnGetState)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -51,6 +51,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
 | 
			
		||||
        for(var i = 0; i < items.size(); i++) {
 | 
			
		||||
            var type       = items[i].get("type")       as Lang.String     or Null;
 | 
			
		||||
            var name       = items[i].get("name")       as Lang.String     or Null;
 | 
			
		||||
            var content    = items[i].get("content")    as Lang.String     or Null;
 | 
			
		||||
            var entity     = items[i].get("entity")     as Lang.String     or Null;
 | 
			
		||||
            var tap_action = items[i].get("tap_action") as Lang.Dictionary or Null;
 | 
			
		||||
            var service    = items[i].get("service")    as Lang.String     or Null;
 | 
			
		||||
@@ -62,12 +63,16 @@ class HomeAssistantView extends WatchUi.Menu2 {
 | 
			
		||||
                    confirm = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (type != null && name != null && entity != null) {
 | 
			
		||||
                if (type.equals("toggle")) {
 | 
			
		||||
            if (type != null && name != null) {
 | 
			
		||||
                if (type.equals("toggle") && entity != null) {
 | 
			
		||||
                    var item = HomeAssistantMenuItemFactory.create().toggle(name, entity);
 | 
			
		||||
                    addItem(item);
 | 
			
		||||
                    mListToggleItems.add(item);
 | 
			
		||||
                } else if (type.equals("tap") && service != null) {
 | 
			
		||||
                } else if (type.equals("template") && content != null) {
 | 
			
		||||
                    var item = HomeAssistantMenuItemFactory.create().template(name, entity, content, service, confirm);
 | 
			
		||||
                    addItem(item);
 | 
			
		||||
                    mListToggleItems.add(item);
 | 
			
		||||
                } else if (type.equals("tap") && entity != null && service != null) {
 | 
			
		||||
                    addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, service, confirm));
 | 
			
		||||
                } else if (type.equals("group")) {
 | 
			
		||||
                    var item = HomeAssistantMenuItemFactory.create().group(items[i]);
 | 
			
		||||
@@ -155,6 +160,12 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
 | 
			
		||||
                System.println(haItem.getLabel() + " " + haItem.getId());
 | 
			
		||||
            }
 | 
			
		||||
            haItem.callService();
 | 
			
		||||
        } else if (item instanceof HomeAssistantTemplateMenuItem) {
 | 
			
		||||
            var haItem = item as HomeAssistantTemplateMenuItem;
 | 
			
		||||
            if (Globals.scDebug) {
 | 
			
		||||
                System.println(haItem.getLabel() + " " + haItem.getId());
 | 
			
		||||
            }
 | 
			
		||||
            haItem.callService();
 | 
			
		||||
        } else if (item instanceof HomeAssistantViewMenuItem) {
 | 
			
		||||
            var haMenuItem = item as HomeAssistantViewMenuItem;
 | 
			
		||||
            if (Globals.scDebug) {
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,8 @@ class RezStrings {
 | 
			
		||||
    private static var strNoJson            as Lang.String     or Null;
 | 
			
		||||
    private static var strUnhandledHttpErr  as Lang.String     or Null;
 | 
			
		||||
    private static var strTrailingSlashErr  as Lang.String     or Null;
 | 
			
		||||
    private static var strWebhookFailed     as Lang.String or Null;
 | 
			
		||||
    private static var strWebhookFailed     as Lang.String     or Null;
 | 
			
		||||
    private static var strTemplateError     as Lang.String     or Null;
 | 
			
		||||
    (:glance)
 | 
			
		||||
    private static var strAvailable         as Lang.String     or Null;
 | 
			
		||||
    (:glance)
 | 
			
		||||
@@ -100,6 +101,7 @@ class RezStrings {
 | 
			
		||||
        strUnhandledHttpErr  = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr);
 | 
			
		||||
        strTrailingSlashErr  = WatchUi.loadResource($.Rez.Strings.TrailingSlashErr);
 | 
			
		||||
        strWebhookFailed     = WatchUi.loadResource($.Rez.Strings.WebhookFailed);
 | 
			
		||||
        strTemplateError     = WatchUi.loadResource($.Rez.Strings.TemplateError);
 | 
			
		||||
        strAvailable         = WatchUi.loadResource($.Rez.Strings.Available);
 | 
			
		||||
        strChecking          = WatchUi.loadResource($.Rez.Strings.Checking);
 | 
			
		||||
        strUnavailable       = WatchUi.loadResource($.Rez.Strings.Unavailable);
 | 
			
		||||
@@ -184,6 +186,10 @@ class RezStrings {
 | 
			
		||||
        return strWebhookFailed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static function getTemplateError() as Lang.String {
 | 
			
		||||
        return strTemplateError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static function getAvailable() as Lang.String {
 | 
			
		||||
        return strAvailable;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,26 @@
 | 
			
		||||
//-----------------------------------------------------------------------------------
 | 
			
		||||
//
 | 
			
		||||
// Distributed under MIT Licence
 | 
			
		||||
//   See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
 | 
			
		||||
//
 | 
			
		||||
//-----------------------------------------------------------------------------------
 | 
			
		||||
//
 | 
			
		||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
 | 
			
		||||
// 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, 10 January 2024
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// Description:
 | 
			
		||||
//
 | 
			
		||||
// Home Assistant Webhook creation.
 | 
			
		||||
// 
 | 
			
		||||
// Reference:
 | 
			
		||||
//  * https://developers.home-assistant.io/docs/api/native-app-integration
 | 
			
		||||
//
 | 
			
		||||
//-----------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Toybox.Lang;
 | 
			
		||||
using Toybox.Communications;
 | 
			
		||||
using Toybox.System;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user