Skip to content

Commit 1910eb1

Browse files
authored
Create home-assistant-widget.js
1 parent 89787e8 commit 1910eb1

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed

source/home-assistant-widget.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Home Assistant Sensor Widget
2+
//
3+
// Copyright 2023 @olahellgren
4+
// https://github.com/olahellgren/home-assistant-sensor-widget-scriptable
5+
//
6+
//
7+
// INSTRUCTIONS
8+
//
9+
// Download Scriptable app and add THIS script to it. This will allow you to
10+
// add a small widget to your iOS background.
11+
//
12+
// Start by adding the URL (IP/host and port) e.g.
13+
// https://myinstance.ui.nabu.casa or
14+
// http://192.168.1.32:8123.
15+
//
16+
// Make sure you have craeted a long lived token
17+
// for one you your users, preferably a separete
18+
// user with no admin rights. Your token will be
19+
// used to access your home assistant.
20+
//
21+
// Add titles and sensors in the `widgetTitlesAndSensors` array. All sensors found
22+
// will be showed as sensors and sensors not found will be shown as titles. This way
23+
// it's pretty flexible to add what you want.
24+
25+
// Confguration
26+
// EDIT HERE
27+
28+
const hassUrl = "<hass base url>"
29+
const hassToken = "<your long lived Bearer token>"
30+
31+
const widgetTitlesAndSensors = [
32+
"Solar Energy",
33+
"sensor.kostal_inverter_output_power",
34+
"sensor.kostal_inverter_yield_today",
35+
"Weather",
36+
"sensor.oregonv1_0080_temp",
37+
"weather.hem"
38+
]
39+
40+
// The MIT License
41+
//
42+
// Permission is hereby granted, free of charge, to any person obtaining a
43+
// copy of this software and associated documentation files (the "Software"),
44+
// to deal in the Software without restriction, including without limitation
45+
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
46+
// and/or sell copies of the Software, and to permit persons to whom the
47+
// Software is furnished to do so, subject to the following conditions:
48+
//
49+
// The above copyright notice and this permission notice shall be included in
50+
// all copies or substantial portions of the Software.
51+
//
52+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
53+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
57+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
58+
// DEALINGS IN THE SOFTWARE.
59+
//
60+
// ==========================
61+
// Don't EDIT below this line
62+
// ==========================
63+
64+
const titleText = "Home Assistant"
65+
66+
const backgroundColorStart = '#049cdb'
67+
const backgroundColorEnd = '#0180c8'
68+
const textColor = '#ffffff'
69+
const sensorFontAndImageSize = 16
70+
const titleFontAndImageSize = 12
71+
const padding = 12
72+
const maxNoOfSensors = 4
73+
74+
const states = await fetchAllStates()
75+
const widget = new ListWidget()
76+
77+
const iconSymbolMap = {
78+
"mdi:calendar": "calendar"
79+
}
80+
81+
const deviceClassSymbolMap = {
82+
"default": "house.fill",
83+
"battery": "battery.75",
84+
"energy": "bolt.fill",
85+
"humidity": "humidity.fill",
86+
"moisture": "drop.triangle.fill",
87+
"power": "bolt.fill",
88+
"precipitation": "cloud.rain.fill",
89+
"temperature": "thermometer.medium",
90+
"timestamp": "clock.fill",
91+
"wind_speed": "wind"
92+
}
93+
94+
setupBackground(widget)
95+
const mainLayout = widget.addStack()
96+
mainLayout.layoutVertically()
97+
const titleStack = mainLayout.addStack()
98+
titleStack.topAlignContent()
99+
setupTitle(titleStack, titleText, deviceClassSymbolMap.default)
100+
mainLayout.addSpacer()
101+
const sensorStack = mainLayout.addStack()
102+
sensorStack.layoutVertically()
103+
sensorStack.bottomAlignContent()
104+
widgetTitlesAndSensors.forEach(entry => {
105+
if (getState(states, entry)) {
106+
addSensor(sensorStack, entry)
107+
} else {
108+
setupTitle(sensorStack, entry)
109+
}
110+
})
111+
112+
Script.setWidget(widget)
113+
Script.complete()
114+
widget.presentSmall()
115+
116+
117+
function setupBackground() {
118+
const bGradient = new LinearGradient()
119+
bGradient.colors = [new Color(backgroundColorStart), new Color(backgroundColorEnd)]
120+
bGradient.locations = [0,1]
121+
widget.backgroundGradient = bGradient
122+
widget.setPadding(padding, padding, padding, padding)
123+
widget.spacing = 4
124+
}
125+
126+
function setupTitle(widget, titleText, icon) {
127+
let titleStack = widget.addStack()
128+
titleStack.cornerRadius = 4
129+
titleStack.setPadding(3, 0, 0, 25)
130+
if (icon) {
131+
let wImage = titleStack.addImage(SFSymbol.named(icon).image)
132+
wImage.imageSize = new Size(titleFontAndImageSize, titleFontAndImageSize)
133+
titleStack.addSpacer(5)
134+
}
135+
let wTitle = titleStack.addText(titleText)
136+
wTitle.font = Font.semiboldRoundedSystemFont(titleFontAndImageSize)
137+
wTitle.textColor = Color.white()
138+
}
139+
140+
function addSensorValues(sensorStack, hassSensors) {
141+
hassSensors.forEach(sensor => {
142+
addSensor(sensorStack, sensor)
143+
})
144+
}
145+
146+
function getSymbolForSensor(sensor) {
147+
if (iconSymbolMap[sensor.attributes.icon]) {
148+
return iconSymbolMap[sensor.attributes.icon]
149+
} else if (deviceClassSymbolMap[sensor.attributes.device_class]) {
150+
return deviceClassSymbolMap[sensor.attributes.device_class]
151+
} else {
152+
return deviceClassSymbolMap.default
153+
}
154+
}
155+
156+
function addSensor(sensorStack, entityId) {
157+
const sensor = getState(states, entityId)
158+
159+
const row = sensorStack.addStack()
160+
row.setPadding(0, 0, 0, 0)
161+
row.layoutHorizontally()
162+
163+
const icon = row.addStack()
164+
icon.setPadding(0, 0, 0, 3)
165+
const sfSymbol = getSymbolForSensor(sensor)
166+
const sf = SFSymbol.named(sfSymbol)
167+
const imageNode = icon.addImage(sf.image)
168+
imageNode.imageSize = new Size(sensorFontAndImageSize, sensorFontAndImageSize)
169+
170+
const value = row.addStack()
171+
value.setPadding(0, 0, 0, 4)
172+
const valueText = setSensorText(value, sensor)
173+
valueText.font = Font.mediumRoundedSystemFont(sensorFontAndImageSize)
174+
valueText.textColor = new Color(textColor)
175+
176+
if (sensor.attributes.unit_of_measurement) {
177+
const unit = row.addStack()
178+
const unitText = unit.addText(sensor.attributes.unit_of_measurement)
179+
unitText.font = Font.mediumSystemFont(sensorFontAndImageSize)
180+
unitText.textColor = new Color(textColor)
181+
}
182+
183+
}
184+
185+
function setSensorText(value, sensor) {
186+
if (sensor.attributes.device_class === "moisture") {
187+
return sensor.state === "on" ? value.addText("Wet") : value.addText("Dry")
188+
} else {
189+
return value.addText(sensor.state)
190+
}
191+
}
192+
193+
function addEmptyRow() {
194+
const row = widget.addStack()
195+
row.layoutHorizontally()
196+
const t = row.addText(' ')
197+
t.font = Font.mediumSystemFont(sensorFontAndImageSize)
198+
}
199+
200+
async function fetchAllStates() {
201+
let req = new Request(`${hassUrl}/api/states`)
202+
req.headers = {
203+
"Authorization": `Bearer ${hassToken}`,
204+
"content-type": "application/json"
205+
}
206+
return await req.loadJSON();
207+
}
208+
209+
function getState(states, entityId) {
210+
return states.filter(state => state.entity_id === entityId)[0]
211+
}

0 commit comments

Comments
 (0)