Compare commits

...

4 Commits

Author SHA1 Message Date
Joseph Abbey
72059fbd8f Improve web: dim disabled, icons, pin 2025-07-18 22:18:26 +01:00
__JosephAbbey
a6b4925ff7 257 pin dialog does not popup when using is servicetap (#258)
Need this urgently as it affects the use of the PIN for tap menu items.
2025-07-18 20:51:10 +01:00
Philip Abbey
3672a598fb Update HISTORY.md
Added v2.32
2025-07-18 20:49:10 +01:00
Philip Abbey
f6d0916315 Update HomeAssistantTapMenuItem.mc
Requirement for use of the PIN was not being correctly extracted from the options. Goes to show why the security features should not be relied on! A recent compilation fix created this breaking change.
2025-07-18 20:46:49 +01:00
9 changed files with 108 additions and 35 deletions

View File

@@ -2,5 +2,8 @@
"cSpell.words": [ "cSpell.words": [
"usbs", "usbs",
"Venu" "Venu"
] ],
"files.exclude": {
"resources-*": true
}
} }

View File

@@ -44,3 +44,4 @@
| 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. | | 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. |
| 2.30 | <img src="images/Venu2_glance_default.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. | | 2.30 | <img src="images/Venu2_glance_default.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. |
| 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. | | 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. |
| 2.32 | Bug fix for a breaking change extracting options caused by the need to rearrange function parameters for an [annoying compiler error](https://github.com/house-of-abbey/GarminHomeAssistant/issues/253). |

View File

@@ -21,7 +21,7 @@ rem
rem ----------------------------------------------------------------------------------- rem -----------------------------------------------------------------------------------
rem Check this path is correct for your Java installation rem Check this path is correct for your Java installation
set JAVA_PATH=C:\Program Files\Java\jdk-22\bin set JAVA_PATH=C:\Program Files\Java\jdk-21\bin
rem SDK_PATH should work for all users rem SDK_PATH should work for all users
set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg" set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg"
set SDK_PATH=%SDK_PATH:~0,-1%\bin set SDK_PATH=%SDK_PATH:~0,-1%\bin
@@ -107,7 +107,7 @@ rem Compile PRG for a single device for side loading
-jar %SDK_PATH%\monkeybrains.jar ^ -jar %SDK_PATH%\monkeybrains.jar ^
--output %SRC%\bin\HomeAssistant.prg ^ --output %SRC%\bin\HomeAssistant.prg ^
--jungles %SRC%\%JUNGLE% ^ --jungles %SRC%\%JUNGLE% ^
--private-key %SRC%\..\developer_key ^ --private-key C:\Users\josep\AppData\Roaming\Garmin\ConnectIQ\Key\developer_key ^
--device %DEVICE%_sim ^ --device %DEVICE%_sim ^
--warn ^ --warn ^
--release --release

View File

@@ -13,24 +13,24 @@
--> -->
<properties> <properties>
<property id="api_key" type="string"></property> <property id="api_key" type="string">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxYjM0NDA4OGM4ODI0OGQ1YWU5ZTBiZWMyYzVmOWQ2MyIsImlhdCI6MTc1Mjg2NTIyOCwiZXhwIjoyMDY4MjI1MjI4fQ.s4a1vmnj4IaBuFIcLk-LYyYqY1D94ATPHKRktU5TQdo</property>
<!-- <!--
Internal URL "https://homeassistant.local/api" Internal URL "https://homeassistant.local/api"
External URL "https://<dynamic DNS>/api" External URL "https://<dynamic DNS>/api"
--> -->
<property id="api_url" type="string"></property> <property id="api_url" type="string">https://home.abbey1.org.uk/api</property>
<!-- <!--
Best be a public URL in order to work away from your home LAN and have a Best be a public URL in order to work away from your home LAN and have a
trusted HTTPS certificate. trusted HTTPS certificate.
--> -->
<property id="config_url" type="string"></property> <property id="config_url" type="string">https://home.abbey1.org.uk/local/garmin/philip.json</property>
<!-- <!--
Decide if the menu configuration should be cached. Decide if the menu configuration should be cached.
--> -->
<property id="cache_config" type="boolean">false</property> <property id="cache_config" type="boolean">true</property>
<!-- <!--
Clear the menu configuration on next application start, and refetch, then Clear the menu configuration on next application start, and refetch, then
@@ -53,7 +53,7 @@
Poll delay adds a user configurable delay (in seconds) to each round of Poll delay adds a user configurable delay (in seconds) to each round of
status updates of all item in the device's menu that might be amended status updates of all item in the device's menu that might be amended
externally from the watch. A user has requested that it is possible to add externally from the watch. A user has requested that it is possible to add
this delayfor an "always open" mode of operation, which then drains the this delay for an "always open" mode of operation, which then drains the
watch battery from the additional API access activity. watch battery from the additional API access activity.
--> -->
<property id="poll_delay_combined" type="number">5</property> <property id="poll_delay_combined" type="number">5</property>

View File

@@ -69,7 +69,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
mData = data; mData = data;
mExit = options.get(:exit); mExit = options.get(:exit);
mConfirm = options.get(:confirm); mConfirm = options.get(:confirm);
mPin = options.get(:acospin); mPin = options.get(:pin);
} }
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry. //! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.

View File

@@ -37,7 +37,8 @@
margin-left: 0.5em; margin-left: 0.5em;
filter: grayscale() invert(); filter: grayscale() invert();
} }
.template, .info { .template,
.info {
background-image: url(../resources-icons-48/info_type.svg); background-image: url(../resources-icons-48/info_type.svg);
background-size: contain; background-size: contain;
margin-left: 0.5em; margin-left: 0.5em;
@@ -63,6 +64,13 @@
margin-left: 0.5em; margin-left: 0.5em;
filter: grayscale() invert(); filter: grayscale() invert();
} }
.mdi {
margin-left: 0.5em;
}
.disabled {
opacity: 0.5;
}
:root { :root {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@@ -285,6 +293,10 @@
background-color: var(--ctp-mocha-overlay1); background-color: var(--ctp-mocha-overlay1);
} }
</style> </style>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css"
defer />
</head> </head>
<body> <body>
<div id="settings"> <div id="settings">
@@ -441,9 +453,10 @@ http:
</div> </div>
</dialog> </dialog>
<script src="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script> <script src="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/loader.js"></script>
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script> <script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script> <script src="https://www.unpkg.com/toastify-js@1.12.4/src/toastify.js"></script>
<script src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script>
<script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
</body> </body>
</html> </html>

View File

@@ -11,7 +11,7 @@ let api_token = localStorage.getItem('api_token') ?? '';
/** /**
* Get all entities in HomeAssistant. * Get all entities in HomeAssistant.
* @returns {Promise<Record<string, string>>} [id, name] * @returns {Promise<Record<string, { name: string, icon?: string }>>} [id, name]
*/ */
async function get_entities() { async function get_entities() {
try { try {
@@ -21,7 +21,7 @@ async function get_entities() {
Authorization: `Bearer ${api_token}`, Authorization: `Bearer ${api_token}`,
}, },
mode: 'cors', mode: 'cors',
body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\",\\"{{ entity.attributes.icon }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
}); });
if (res.status == 401 || res.status == 403) { if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid'); document.querySelector('#api_token').classList.add('invalid');
@@ -29,8 +29,16 @@ async function get_entities() {
} }
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); const data = {};
} catch { for (const [id, name, icon] of await res.json()) {
data[id] = { name };
if (icon !== '') {
data[id].icon = icon;
}
}
return data;
} catch (e) {
console.error('Error fetching entities:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return {}; return {};
} }
@@ -57,7 +65,8 @@ async function get_devices() {
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch { } catch (e) {
console.error('Error fetching devices:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return {}; return {};
} }
@@ -84,7 +93,8 @@ async function get_areas() {
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch { } catch (e) {
console.error('Error fetching areas:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return {}; return {};
} }
@@ -119,7 +129,8 @@ async function get_services() {
} }
} }
return services; return services;
} catch { } catch (e) {
console.error('Error fetching services:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return []; return [];
} }
@@ -370,6 +381,9 @@ async function generate_schema(entities, devices, areas, services, schema) {
confirm: { confirm: {
$ref: '#/$defs/confirm', $ref: '#/$defs/confirm',
}, },
pin: {
$ref: '#/$defs/pin',
},
data: { data: {
type: 'object', type: 'object',
properties: {}, properties: {},
@@ -724,6 +738,7 @@ require(['vs/editor/editor.main'], async () => {
var decorations = editor.createDecorationsCollection([]); var decorations = editor.createDecorationsCollection([]);
/** @type {monaco.editor.IMarkerData[]} */
let markers = []; let markers = [];
const renderTemplate = editor.addCommand( const renderTemplate = editor.addCommand(
@@ -905,6 +920,7 @@ require(['vs/editor/editor.main'], async () => {
const ast = json.parse(model.getValue()); const ast = json.parse(model.getValue());
const data = JSON.parse(model.getValue()); const data = JSON.parse(model.getValue());
markers = []; markers = [];
/** @type {monaco.editor.IModelDeltaDecoration[]} */
const glyphs = []; const glyphs = [];
async function testToggle(range, entity) { async function testToggle(range, entity) {
const res = await fetch(api_url + '/states/' + entity, { const res = await fetch(api_url + '/states/' + entity, {
@@ -983,8 +999,10 @@ require(['vs/editor/editor.main'], async () => {
* @param {import('json-ast-comments').JsonAst | * @param {import('json-ast-comments').JsonAst |
* import('json-ast-comments').JsonProperty} node * import('json-ast-comments').JsonProperty} node
* @param {string[]} path * @param {string[]} path
* @param {import('json-ast-comments').JsonAst |
* import('json-ast-comments').JsonProperty | null} parent
*/ */
function recurse(node, path) { function recurse(node, path, parent = null) {
if (node.type === 'property') { if (node.type === 'property') {
if (node.key[0].value === 'content') { if (node.key[0].value === 'content') {
templates.push([ templates.push([
@@ -1010,11 +1028,39 @@ require(['vs/editor/editor.main'], async () => {
} }
trim++; trim++;
markers.push({ markers.push({
message: entities[node.value[0].value] ?? 'Entity not found', message: entities[node.value[0].value].name ?? 'Entity not found',
severity: monaco.MarkerSeverity.Hint, severity: monaco.MarkerSeverity.Hint,
...range, ...range,
startColumn: trim, startColumn: trim,
}); });
glyphs.push({
range,
options: {
isWholeLine: true,
glyphMarginClassName:
'mdi ' +
entities[node.value[0].value]?.icon?.replace(':', '-'),
},
});
} else if (
node.key[0].value === 'enabled' &&
node.value[0].type === 'boolean' &&
!node.value[0].value
) {
glyphs.push({
range: {
startLineNumber: parent.members[0].key[0].range.start.line + 1,
startColumn: 0,
endLineNumber:
parent.members[parent.members.length - 1].value[0].range.end
.line + 1,
endColumn: 10000,
},
options: {
isWholeLine: true,
inlineClassName: 'disabled',
},
});
} else if (node.key[0].value === 'type') { } else if (node.key[0].value === 'type') {
if (node.value[0].value === 'toggle') { if (node.value[0].value === 'toggle') {
toggles.push([ toggles.push([
@@ -1041,15 +1087,15 @@ require(['vs/editor/editor.main'], async () => {
}); });
} }
} else { } else {
recurse(node.value[0], [...path, node.key[0].value]); recurse(node.value[0], [...path, node.key[0].value], node);
} }
} else if (node.type === 'array') { } else if (node.type === 'array') {
for (let i = 0; i < node.members.length; i++) { for (let i = 0; i < node.members.length; i++) {
recurse(node.members[i], [...path, i]); recurse(node.members[i], [...path, i], node);
} }
} else if (node.type === 'object') { } else if (node.type === 'object') {
for (let member of node.members) { for (let member of node.members) {
recurse(member, path); recurse(member, path, node);
} }
} }
} }

View File

@@ -10,10 +10,11 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/toastify-js": "^1.12.3", "@types/toastify-js": "^1.12.4",
"@vscode/webview-ui-toolkit": "1.4.0", "@vscode/webview-ui-toolkit": "1.4.0",
"json-ast-comments": "1.1.1", "json-ast-comments": "1.1.1",
"monaco-editor": "0.45.0", "monaco-editor": "0.52.2",
"prettier": "^3.6.2",
"serve": "^14.2.1" "serve": "^14.2.1"
} }
} }

25
web/pnpm-lock.yaml generated
View File

@@ -6,8 +6,8 @@ settings:
devDependencies: devDependencies:
'@types/toastify-js': '@types/toastify-js':
specifier: ^1.12.3 specifier: ^1.12.4
version: 1.12.3 version: 1.12.4
'@vscode/webview-ui-toolkit': '@vscode/webview-ui-toolkit':
specifier: 1.4.0 specifier: 1.4.0
version: 1.4.0(react@18.2.0) version: 1.4.0(react@18.2.0)
@@ -15,8 +15,11 @@ devDependencies:
specifier: 1.1.1 specifier: 1.1.1
version: 1.1.1 version: 1.1.1
monaco-editor: monaco-editor:
specifier: 0.45.0 specifier: 0.52.2
version: 0.45.0 version: 0.52.2
prettier:
specifier: ^3.6.2
version: 3.6.2
serve: serve:
specifier: ^14.2.1 specifier: ^14.2.1
version: 14.2.1 version: 14.2.1
@@ -52,8 +55,8 @@ packages:
exenv-es6: 1.1.1 exenv-es6: 1.1.1
dev: true dev: true
/@types/toastify-js@1.12.3: /@types/toastify-js@1.12.4:
resolution: {integrity: sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==} resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
dev: true dev: true
/@vscode/webview-ui-toolkit@1.4.0(react@18.2.0): /@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):
@@ -415,8 +418,8 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true dev: true
/monaco-editor@0.45.0: /monaco-editor@0.52.2:
resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==} resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
dev: true dev: true
/ms@2.0.0: /ms@2.0.0:
@@ -460,6 +463,12 @@ packages:
resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==} resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
dev: true dev: true
/prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
dev: true
/punycode@1.4.1: /punycode@1.4.1:
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
dev: true dev: true