|
| 1 | +// Inspiration from https://github.com/awaescher/home-battery-widget/blob/main/widget.js |
| 2 | +// Simple Home Assistant (HASS) iOS Gauge Lock Screen Widget via Scriptable App |
| 3 | + |
| 4 | + |
| 5 | +const widget = new ListWidget(); |
| 6 | + |
| 7 | +// Get data from HASS |
| 8 | +let result = await loadValues(); |
| 9 | + |
| 10 | +// Check data quality |
| 11 | +let percent = result.value; |
| 12 | +let isValid = !Number.isNaN(percent); |
| 13 | + |
| 14 | +if (!isValid) { |
| 15 | + percent = 0; |
| 16 | +} |
| 17 | + |
| 18 | +// Build widget |
| 19 | +let progressStack = await progressCircle(widget, percent); |
| 20 | + |
| 21 | +// Tiny house or triangle |
| 22 | +const mainIconName = isValid ? "house.fill" : "exclamationmark.triangle"; |
| 23 | + |
| 24 | +let mainIcon = SFSymbol.named(mainIconName); |
| 25 | +mainIcon.applyFont(Font.regularSystemFont(26)); |
| 26 | +mainIcon = progressStack.addImage(mainIcon.image); |
| 27 | +const mainImageSize = 30; |
| 28 | +mainIcon.imageSize = new Size(mainImageSize, mainImageSize); |
| 29 | +mainIcon.tintColor = new Color("#fafafa"); |
| 30 | + |
| 31 | +// Tiny bolt (lightning strike) icon |
| 32 | +const badgeName = "bolt.fill"; |
| 33 | +let badgeIcon = SFSymbol.named(badgeName); |
| 34 | +badgeIcon.applyFont(Font.regularSystemFont(26)); |
| 35 | +badgeIcon = progressStack.addImage(badgeIcon.image); |
| 36 | +badgeIcon.imageSize = new Size(12, 12); |
| 37 | +badgeIcon.tintColor = new Color("#fafafa"); |
| 38 | + |
| 39 | +// iOS 16 gauge widget on lock screen |
| 40 | +widget.presentAccessoryCircular(); |
| 41 | +// or classical widget? You need to decide. |
| 42 | +widget.backgroundColor = new Color("#7c7c7c", 1.0); |
| 43 | +//widget.presentSmall(); |
| 44 | + |
| 45 | +Script.setWidget(widget); |
| 46 | +Script.complete(); |
| 47 | + |
| 48 | +// color = "hsl(0, 0%, 100%)", |
| 49 | +async function progressCircle( |
| 50 | + on, |
| 51 | + value = 50, |
| 52 | + color = "hsl(0, 0%, 100%)", |
| 53 | + background = "hsl(0, 0%, 10%)", |
| 54 | + size = 60, |
| 55 | + barWidth = 5.5 |
| 56 | +) { |
| 57 | + if (value > 1) { |
| 58 | + value /= 100 |
| 59 | + } |
| 60 | + if (value < 0) { |
| 61 | + value = 0 |
| 62 | + } |
| 63 | + if (value > 1) { |
| 64 | + value = 1 |
| 65 | + } |
| 66 | + |
| 67 | + // https://htmlcolors.com/hex-to-hsl |
| 68 | + if (value > 0.0 && value < 0.25) { |
| 69 | + color = "hsl(0, 100%, 50%)"; // red |
| 70 | + } else if (value >= 0.25 && value < 0.75) { |
| 71 | + color = "hsl(54, 100%, 50%)"; // yellow |
| 72 | + } else { |
| 73 | + color = "hsl(120, 100%, 50%)"; // green |
| 74 | + } |
| 75 | + |
| 76 | + // Change colors in dark mode |
| 77 | + async function isUsingDarkAppearance() { |
| 78 | + return !Color.dynamic(Color.white(), Color.black()).red; |
| 79 | + } |
| 80 | + let isDark = await isUsingDarkAppearance(); |
| 81 | + |
| 82 | + if (color.split("-").length > 1) { |
| 83 | + if (isDark) { |
| 84 | + color = color.split("-")[1]; |
| 85 | + } else { |
| 86 | + color = color.split("-")[0]; |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + if (background.split("-").length > 1) { |
| 91 | + if (isDark) { |
| 92 | + background = background.split("-")[1]; |
| 93 | + } else { |
| 94 | + background = background.split("-")[0]; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + let w = new WebView() |
| 99 | + await w.loadHTML('<canvas id="c"></canvas>'); |
| 100 | + |
| 101 | + // The magic gauge, filled with 'value' |
| 102 | + let base64 = await w.evaluateJavaScript( |
| 103 | + `let color = "${color}", |
| 104 | + background = "${background}", |
| 105 | + size = ${size}*3, |
| 106 | + lineWidth = ${barWidth}*3, |
| 107 | + percent = ${value * 100} |
| 108 | + let canvas = document.getElementById('c'), |
| 109 | + c = canvas.getContext('2d') |
| 110 | + canvas.width = size |
| 111 | + canvas.height = size |
| 112 | + let posX = canvas.width / 2, |
| 113 | + posY = canvas.height / 2, |
| 114 | + onePercent = 360 / 100, |
| 115 | + result = onePercent * percent |
| 116 | + c.lineCap = 'round' |
| 117 | + c.beginPath() |
| 118 | + c.arc( posX, posY, (size-lineWidth-1)/2, (Math.PI/180) * 270, (Math.PI/180) * (270 + 360) ) |
| 119 | + c.strokeStyle = background |
| 120 | + c.lineWidth = lineWidth |
| 121 | + c.stroke() |
| 122 | + c.beginPath() |
| 123 | + c.strokeStyle = color |
| 124 | + c.lineWidth = lineWidth |
| 125 | + c.arc( posX, posY, (size-lineWidth-1)/2, (Math.PI/180) * 270, (Math.PI/180) * (270 + result) ) |
| 126 | + c.stroke() |
| 127 | + completion(canvas.toDataURL().replace("data:image/png;base64,",""))`, true); |
| 128 | + const image = Image.fromData(Data.fromBase64String(base64)); |
| 129 | + |
| 130 | + // Add gauge to widget |
| 131 | + let stack = on.addStack(); |
| 132 | + stack.size = new Size(size, size); |
| 133 | + stack.backgroundImage = image; |
| 134 | + stack.centerAlignContent(); |
| 135 | + let padding = barWidth * 2; |
| 136 | + stack.setPadding(padding, padding, padding, padding); |
| 137 | + |
| 138 | + return stack; |
| 139 | +} |
| 140 | + |
| 141 | +// Get data from HASS |
| 142 | +async function loadValues() { |
| 143 | + |
| 144 | + let req = new Request("https://<HASS IP>/api/states"); |
| 145 | + req.headers = { "Authorization": "Bearer <HASS Long-Lived Access Token at https://<HASS IP>/profile", "content-type": "application/json" }; |
| 146 | + let json = await req.loadJSON(); |
| 147 | + |
| 148 | + // Edit this, add your sensor |
| 149 | + let result = { name: 'sensor.your_sensor_of_interest', value: -1 }; |
| 150 | + |
| 151 | + // Search through HASS data find sensor and its current value |
| 152 | + var i; |
| 153 | + for (i = 0; i < json.length; i++) { |
| 154 | + if (json[i]['entity_id'] == result.name) { |
| 155 | + result.value = json[i]['state']; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + // Testing / Debugging |
| 160 | + //result.value = 30; |
| 161 | + |
| 162 | + console.log(result); |
| 163 | + return result; |
| 164 | +} |
0 commit comments