diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0d27f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +export/ +**/Thumbs.db diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..070367e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "monkeyc", + "request": "launch", + "name": "Run App", + "stopAtLaunch": false, + "device": "${command:GetTargetDevice}" + }, + { + "type": "monkeyc", + "request": "launch", + "name": "Run Tests", + "runTests": true, + "device": "${command:GetTargetDevice}" + }, + { + "type": "monkeyc", + "request": "launch", + "name": "Run Complication Apps", + "stopAtLaunch": false, + "complicationSubscriberFolder": "${command:GetComplicationSubscriberFolder}", + "complicationPublisherFolder": "${command:GetComplicationPublisherFolder}", + "device": "${command:GetTargetDevice}" + } + ] +} \ No newline at end of file diff --git a/manifest.xml b/manifest.xml new file mode 100644 index 0000000..9028ea1 --- /dev/null +++ b/manifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + eng + + + + + \ No newline at end of file diff --git a/monkey.jungle b/monkey.jungle new file mode 100644 index 0000000..87796c7 --- /dev/null +++ b/monkey.jungle @@ -0,0 +1 @@ +project.manifest = manifest.xml diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml new file mode 100644 index 0000000..a22c33c --- /dev/null +++ b/resources/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources/drawables/launcher_icon.png b/resources/drawables/launcher_icon.png new file mode 100644 index 0000000..8e302cc Binary files /dev/null and b/resources/drawables/launcher_icon.png differ diff --git a/resources/layouts/layout.xml b/resources/layouts/layout.xml new file mode 100644 index 0000000..e0bb1b2 --- /dev/null +++ b/resources/layouts/layout.xml @@ -0,0 +1,6 @@ + + diff --git a/resources/menus/menu.xml b/resources/menus/menu.xml new file mode 100644 index 0000000..6537eba --- /dev/null +++ b/resources/menus/menu.xml @@ -0,0 +1,4 @@ + + + + diff --git a/resources/settings/properties.xml b/resources/settings/properties.xml new file mode 100644 index 0000000..5d2bccb --- /dev/null +++ b/resources/settings/properties.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml new file mode 100644 index 0000000..664d902 --- /dev/null +++ b/resources/settings/settings.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml new file mode 100644 index 0000000..f425222 --- /dev/null +++ b/resources/strings/strings.xml @@ -0,0 +1,8 @@ + + HomeAssistant + + Click the menu button + + Item 1 + Item 2 + diff --git a/source/Globals.mc b/source/Globals.mc new file mode 100644 index 0000000..75adb8f --- /dev/null +++ b/source/Globals.mc @@ -0,0 +1,15 @@ +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 debug = false; + static const updateInterval = 5; // seconds +// static hidden const apiUrl = "https://homeassistant.local/api"; + static hidden const apiUrl = "https://home.abbey1.org.uk/api"; + + static function getApiUrl() { + return apiUrl; + } + +} diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc new file mode 100644 index 0000000..3aad550 --- /dev/null +++ b/source/HomeAssistantApp.mc @@ -0,0 +1,28 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class HomeAssistantApp extends Application.AppBase { + + function initialize() { + AppBase.initialize(); + } + + // onStart() is called on application start up + function onStart(state as Dictionary?) as Void { + } + + // onStop() is called when your application is exiting + function onStop(state as Dictionary?) as Void { + } + + // Return the initial view of your application here + function getInitialView() as Array? { + return [ new HomeAssistantView(), new HomeAssistantViewDelegate() ] as Array; + } + +} + +function getApp() as HomeAssistantApp { + return Application.getApp() as HomeAssistantApp; +} diff --git a/source/HomeAssistantMenuItem.mc b/source/HomeAssistantMenuItem.mc new file mode 100644 index 0000000..2f3b484 --- /dev/null +++ b/source/HomeAssistantMenuItem.mc @@ -0,0 +1,72 @@ +import Toybox.Lang; +import Toybox.WatchUi; +import Toybox.Graphics; +using Toybox.Application.Properties; + +class HomeAssistantMenuItem extends WatchUi.MenuItem { + hidden var api_key = Properties.getValue("api_key"); + + function initialize( + label as Lang.String or Lang.Symbol, + subLabel as Lang.String or Lang.Symbol or Null, + identifier as Lang.Object or Null, + options as { + :alignment as MenuItem.Alignment, + :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol + } or Null + ) { + WatchUi.MenuItem.initialize( + label, + subLabel, + identifier, + options + ); + } + + // Callback function after completing the POST request to call a script. + // + function onReturnExecScript(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + if (Globals.debug) { + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode); + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data); + } + if (responseCode == 200) { + var d = data as Lang.Array; + for(var i = 0; i < d.size(); i++) { + if ((d[i].get("entity_id") as Lang.String).equals(mIdentifier)) { + if (Globals.debug) { + System.println("HomeAssistantMenuItem Note - onReturnExecScript(): Correct script executed."); + } + } + } + } + } + + function execScript() as Void { + var options = { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Authorization" => "Bearer " + api_key + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }; + if (System.getDeviceSettings().phoneConnected && System.getDeviceSettings().connectionAvailable) { + var url = Globals.getApiUrl() + "/services/" + mIdentifier.substring(0, mIdentifier.find(".")) + "/" + mIdentifier.substring(mIdentifier.find(".")+1, null); + if (Globals.debug) { + System.println("URL=" + url); + System.println("mIdentifier=" + mIdentifier); + } + Communications.makeWebRequest( + url, + null, + options, + method(:onReturnExecScript) + ); + } else { + if (Globals.debug) { + System.println("HomeAssistantMenuItem Note - executeScript(): No Internet connection, skipping API call."); + } + } + } + +} diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc new file mode 100644 index 0000000..342a9e1 --- /dev/null +++ b/source/HomeAssistantToggleMenuItem.mc @@ -0,0 +1,142 @@ +import Toybox.Lang; +import Toybox.WatchUi; +import Toybox.Graphics; +using Toybox.Application.Properties; + +class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { + hidden var api_key = Properties.getValue("api_key"); + hidden var menu; + + function initialize( + menu as HomeAssistantView, + label as Lang.String or Lang.Symbol, + subLabel as Lang.String or Lang.Symbol or { + :enabled as Lang.String or Lang.Symbol or Null, + :disabled as Lang.String or Lang.Symbol or Null + } or Null, + identifier, + enabled as Lang.Boolean, + options as { + :alignment as MenuItem.Alignment, + :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol + } or Null + ) { + WatchUi.ToggleMenuItem.initialize(label, subLabel, identifier, enabled, options); + api_key = Properties.getValue("api_key"); + self.menu = menu; + } + + private function setUiToggle(state as Null or Lang.String) as Void { + if (state != null) { + if (state.equals("on") && !isEnabled()) { + setEnabled(true); + } else if (state.equals("off") && isEnabled()) { + setEnabled(false); + } + WatchUi.requestUpdate(); + } + } + + // Callback function after completing the GET request to fetch the status. + // + function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + if (Globals.debug) { + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode); + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data); + } + if (responseCode == 200) { + var state = data.get("state") as Lang.String; + if (Globals.debug) { + System.println((data.get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); + } + if (getLabel().equals("...")) { + setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String); + } + setUiToggle(state); + } + } + + function getState() as Void { + var options = { + :method => Communications.HTTP_REQUEST_METHOD_GET, + :headers => { + "Authorization" => "Bearer " + api_key + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }; + if (System.getDeviceSettings().phoneConnected && System.getDeviceSettings().connectionAvailable) { + var url = Globals.getApiUrl() + "/states/" + mIdentifier; + if (Globals.debug) { + System.println("URL=" + url); + } + Communications.makeWebRequest( + url, + null, + options, + method(:onReturnGetState) + ); + } else { + if (Globals.debug) { + System.println("HomeAssistantToggleMenuItem Note - getState(): No Internet connection, skipping API call."); + } + } + } + + // Callback function after completing the POST request to set the status. + // + function onReturnSetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + if (Globals.debug) { + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode); + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data); + } + if (responseCode == 200) { + var state; + var d = data as Lang.Array; + for(var i = 0; i < d.size(); i++) { + if ((d[i].get("entity_id") as Lang.String).equals(mIdentifier)) { + state = d[i].get("state") as Lang.String; + if (Globals.debug) { + System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); + } + setUiToggle(state); + } + } + } + } + + function setState(s as Lang.Boolean) as Void { + var options = { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, + "Authorization" => "Bearer " + api_key + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }; + if (System.getDeviceSettings().phoneConnected && System.getDeviceSettings().connectionAvailable) { + var url; + if (s) { + url = Globals.getApiUrl() + "/services/" + mIdentifier.substring(0, mIdentifier.find(".")) + "/turn_on"; + } else { + url = Globals.getApiUrl() + "/services/" + mIdentifier.substring(0, mIdentifier.find(".")) + "/turn_off"; + } + if (Globals.debug) { + System.println("URL=" + url); + System.println("mIdentifier=" + mIdentifier); + } + Communications.makeWebRequest( + url, + { + "entity_id" => mIdentifier + }, + options, + method(:onReturnSetState) + ); + } else { + if (Globals.debug) { + System.println("HomeAssistantToggleMenuItem Note - setState(): No Internet connection, skipping API call."); + } + } + } + +} diff --git a/source/HomeAssistantView.mc b/source/HomeAssistantView.mc new file mode 100644 index 0000000..6de07a4 --- /dev/null +++ b/source/HomeAssistantView.mc @@ -0,0 +1,154 @@ +import Toybox.Lang; +import Toybox.Graphics; +import Toybox.WatchUi; + +class HomeAssistantView extends WatchUi.Menu2 { + hidden var timer; + + function initialize() { + timer = new Timer.Timer(); + + var toggle_obj = { + :enabled => "On", + :disabled => "Off" + }; + + WatchUi.Menu2.initialize({ + :title => "Entities" + }); + addItem( + new HomeAssistantToggleMenuItem( + self, + "Bedroom Light", + toggle_obj, + "light.philip_s_bedside_light_switch", + false, + null + ) + ); + addItem( + new HomeAssistantToggleMenuItem( + self, + "Lounge Lights", + toggle_obj, + "light.living_room_ambient_lights_all", + false, + null + ) + ); + addItem( + new HomeAssistantMenuItem( + "Food is Ready!", + null, + "script.food_is_ready", + null + ) + ); + // addItem( + // new HomeAssistantMenuItem( + // "Test Script", + // null, + // "script.test", + // null + // ) + // ); + addItem( + new HomeAssistantToggleMenuItem( + self, + "Bookcase USBs", + toggle_obj, + "switch.bookcase_usbs", + false, + null + ) + ); + addItem( + new HomeAssistantToggleMenuItem( + self, + "Corner Table USBs", + toggle_obj, + "switch.corner_table_usbs", + false, + null + ) + ); + } + + // Load your resources here + function onLayout(dc as Dc) as Void { + setLayout(Rez.Layouts.MainLayout(dc)); + } + + // Called when this View is brought to the foreground. Restore + // the state of this View and prepare it to be shown. This includes + // loading resources into memory. + function onShow() as Void { + timer.start( + method(:timerUpdate), + Globals.updateInterval * 1000, + true + ); + for(var i = 0; i < mItems.size(); i++) { + if (mItems[i] instanceof HomeAssistantToggleMenuItem) { + var toggleItem = mItems[i] as HomeAssistantToggleMenuItem; + toggleItem.getState(); + if (Globals.debug) { + System.println("HomeAssistantView Note: " + toggleItem.getLabel() + " ID=" + toggleItem.getId() + " Enabled=" + toggleItem.isEnabled()); + } + } + } + } + + // Update the view + function onUpdate(dc as Dc) as Void { + View.onUpdate(dc); + } + + // Called when this View is removed from the screen. Save the + // state of this View here. This includes freeing resources from + // memory. + function onHide() as Void { + timer.stop(); + } + + function timerUpdate() as Void { + for(var i = 0; i < mItems.size(); i++) { + if (mItems[i] instanceof HomeAssistantToggleMenuItem) { + var toggleItem = mItems[i] as HomeAssistantToggleMenuItem; + toggleItem.getState(); + if (Globals.debug) { + System.println("HomeAssistantView Note: " + toggleItem.getLabel() + " ID=" + toggleItem.getId() + " Enabled=" + toggleItem.isEnabled()); + } + } + } + } + +} + +class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { + + function initialize() { + Menu2InputDelegate.initialize(); + } + + function onSelect(item as WatchUi.MenuItem) as Void { + if (item instanceof HomeAssistantToggleMenuItem) { + var haToggleItem = item as HomeAssistantToggleMenuItem; + if (Globals.debug) { + System.println(haToggleItem.getLabel() + " " + haToggleItem.getId() + " " + haToggleItem.isEnabled()); + } + haToggleItem.setState(haToggleItem.isEnabled()); + } else if (item instanceof HomeAssistantMenuItem) { + var haItem = item as HomeAssistantMenuItem; + if (Globals.debug) { + System.println(haItem.getLabel() + " " + haItem.getId()); + } + haItem.execScript(); + } else { + if (Globals.debug) { + System.println(item.getLabel() + " " + item.getId()); + } + } + } + +} \ No newline at end of file