From 5fe8705d0f5d80a2869e28f546f8e9192ac7488a Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:38:50 -0500 Subject: [PATCH 1/7] =?UTF-8?q?Add=20a=20temperature=20setting=20(auto,=20?= =?UTF-8?q?=C2=B0F,=20=C2=B0C)=20to=20the=20household=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- settings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings.js b/settings.js index e62521e..38cf2c3 100644 --- a/settings.js +++ b/settings.js @@ -14,10 +14,11 @@ const OPTIONS = { appearance: ['auto', 'light', 'dark'], // auto follows sunrise and sunset where the household lives screen: ['auto', 'bright', 'dim'], // backlight: with the sun, always bright, or always dim clock: ['auto', '12', '24'], // auto is the convention of the household's country; 12-hour until that is known + temperature: ['auto', 'F', 'C'], // auto is the household's country: Fahrenheit in the US and the few like it, Celsius elsewhere calendarView: ['week', 'agenda'], // the days ahead as six columns, or as one list grouped by day textSize: ['smaller', 'standard', 'larger'] // what is read scales, rows with it; the glance layer and the layout do not }; -const DEFAULTS = {rest: 'photos', restAfter: 5, mornings: 0, photoEvery: 60, appearance: 'auto', screen: 'auto', clock: 'auto', calendarView: 'week', textSize: 'standard'}; +const DEFAULTS = {rest: 'photos', restAfter: 5, mornings: 0, photoEvery: 60, appearance: 'auto', screen: 'auto', clock: 'auto', temperature: 'auto', calendarView: 'week', textSize: 'standard'}; function clean(input) { const out = {}; From 1cc53c451b183b4cc022edc65168921b46619ab5 Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:40:06 -0500 Subject: [PATCH 2/7] =?UTF-8?q?Show=20every=20temperature=20on=20the=20wal?= =?UTF-8?q?l=20in=20=C2=B0F=20or=20=C2=B0C,=20with=20a=20Temperature=20cho?= =?UTF-8?q?ice=20in=20Settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/app.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/dist/app.js b/dist/app.js index 6e55a87..be7e566 100644 --- a/dist/app.js +++ b/dist/app.js @@ -21,7 +21,7 @@ var calProblem = false, tasksProblem = false; var page = 0, mode = 'calendar', themeChoice = 'auto'; // Household preferences from the Settings view, stored on the server. These defaults are used until they load. - var PREFS = { rest: 'photos', restAfter: 5, mornings: 0, photoEvery: 60, appearance: 'auto', clock: 'auto', calendarView: 'week', textSize: 'standard' }; + var PREFS = { rest: 'photos', restAfter: 5, mornings: 0, photoEvery: 60, appearance: 'auto', clock: 'auto', temperature: 'auto', calendarView: 'week', textSize: 'standard' }; var prefs = withDefaults({}); // What the server sends, over these defaults, so a setting it does not send yet still has its value. function withDefaults(data) { var out = {}, k; for (k in PREFS) out[k] = PREFS[k]; for (k in data) out[k] = data[k]; return out; } @@ -87,6 +87,13 @@ } catch (e) {} return (clockConvention[country] = uses24); } + // Fahrenheit or Celsius, as Settings says; on Automatic, as the household's country: Fahrenheit in the US and the few + // places that still use it, Celsius everywhere else. A household whose country is not known yet keeps the Fahrenheit + // it always had. The server always asks Open-Meteo in Fahrenheit, so its cache and the fixtures stay one thing. + var FAHRENHEIT = ['US', 'BS', 'BZ', 'KY', 'LR', 'PW', 'FM', 'MH']; + function celsius() { return prefs.temperature === 'C' || (prefs.temperature !== 'F' && /^[A-Z]{2}$/.test(household.country || '') && FAHRENHEIT.indexOf(household.country) < 0); } + // A temperature from the server, in °F, as a whole number in the unit shown. (+ 0 turns a rounded -0 into 0.) + function deg(f) { return Math.round(celsius() ? (f - 32) * 5 / 9 : f) + 0; } // "4 PM", "4:30 PM" or "16:00", "16:30": the 12-hour form drops :00 as people say it, the 24-hour form never does. function clockTime(d) { var h = d.getHours(), m = d.getMinutes(); return hours24() ? pad(h) + ':' + pad(m) : (h % 12 || 12) + (m ? ':' + pad(m) : '') + (h < 12 ? ' AM' : ' PM'); } // When something was last true, for a time that may be from another day: "5:39 PM" today, then "yesterday 5:39 PM", @@ -346,13 +353,13 @@ $('now').hidden = false; var t = now().getTime(), sun = sunTimes(now()); drawIcon($('now-icon'), weather.code, t < sun.rise || t > sun.set); - $('now-temp').textContent = Math.round(weather.temperature) + '°'; + $('now-temp').textContent = deg(weather.temperature) + '°'; $('now-summary').textContent = describe(weather.code); // Apple's form: H and L first, then the one extra that matters today (rain, or a big feels-like gap). "H:104°" has // no space inside it to break at, so a label never wraps away from its number. - var feels = Math.round(weather.feelsLike), detail = 'H:' + Math.round(weather.high) + '° L:' + Math.round(weather.low) + '°'; + var feels = deg(weather.feelsLike), detail = 'H:' + deg(weather.high) + '° L:' + deg(weather.low) + '°'; if (weather.rain >= 20) detail += ' · ' + weather.rain + '% rain'; - else if (Math.abs(feels - Math.round(weather.temperature)) >= 4) detail += ' · Feels like ' + feels + '°'; + else if (Math.abs(feels - deg(weather.temperature)) >= (celsius() ? 2 : 4)) detail += ' · Feels like ' + feels + '°'; $('now-detail').textContent = detail + (weather.stale ? ' · not updating' : ''); } // A day's forecast in one short row: the icon, with the chance of rain under it where weather apps put it (so a wet day @@ -364,8 +371,8 @@ drawIcon(svg, fc.code); wx.appendChild(svg); if (fc.rain >= 30) wx.appendChild(node('small', '', fc.rain + '%')); row.appendChild(wx); - row.appendChild(node('b', '', Math.round(fc.high) + '°')); row.appendChild(node('span', '', Math.round(fc.low) + '°')); - row.setAttribute('aria-label', describe(fc.code) + ', high ' + Math.round(fc.high) + ', low ' + Math.round(fc.low) + (fc.rain >= 30 ? ', ' + fc.rain + ' percent chance of rain' : '')); + row.appendChild(node('b', '', deg(fc.high) + '°')); row.appendChild(node('span', '', deg(fc.low) + '°')); + row.setAttribute('aria-label', describe(fc.code) + ', high ' + deg(fc.high) + ', low ' + deg(fc.low) + (fc.rain >= 30 ? ', ' + fc.rain + ' percent chance of rain' : '')); return row; } function forecastFor(day) { @@ -724,8 +731,8 @@ if (!wx.hidden) { var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), rain = tomorrowSky.rain >= 30 ? ' · ' + tomorrowSky.rain + '% rain' : ''; svg.setAttribute('class', 'wx-icon'); svg.setAttribute('viewBox', '0 0 64 64'); svg.setAttribute('aria-hidden', 'true'); drawIcon(svg, tomorrowSky.code); - wx.textContent = ''; wx.appendChild(svg); wx.appendChild(node('b', '', Math.round(tomorrowSky.high) + '°')); wx.appendChild(node('span', '', Math.round(tomorrowSky.low) + '°' + rain)); - wx.setAttribute('aria-label', describe(tomorrowSky.code) + ', high ' + Math.round(tomorrowSky.high) + ', low ' + Math.round(tomorrowSky.low) + (rain ? ', ' + tomorrowSky.rain + ' percent chance of rain' : '')); + wx.textContent = ''; wx.appendChild(svg); wx.appendChild(node('b', '', deg(tomorrowSky.high) + '°')); wx.appendChild(node('span', '', deg(tomorrowSky.low) + '°' + rain)); + wx.setAttribute('aria-label', describe(tomorrowSky.code) + ', high ' + deg(tomorrowSky.high) + ', low ' + deg(tomorrowSky.low) + (rain ? ', ' + tomorrowSky.rain + ' percent chance of rain' : '')); } var nodes = coming.map(function (item) { var el = itemNode(item, false); @@ -1161,6 +1168,7 @@ photoEvery: [[30, '30 sec'], [60, '1 min'], [120, '2 min'], [300, '5 min']], appearance: [['auto', 'Automatic'], ['light', 'Light'], ['dark', 'Dark']], clock: [['auto', 'Automatic'], ['12', '12-hour'], ['24', '24-hour']], + temperature: [['auto', 'Automatic'], ['F', '°F'], ['C', '°C']], calendarView: [['week', 'Week'], ['agenda', 'Agenda']], textSize: [['smaller', 'Smaller'], ['standard', 'Standard'], ['larger', 'Larger']], screen: [['auto', 'With the sun'], ['bright', 'Bright'], ['dim', 'Dim']] From bd2af667b002bc468269ed0763ed444d15f492af Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:40:49 -0500 Subject: [PATCH 3/7] Add the Temperature choice to Settings, under Appearance --- dist/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/index.html b/dist/index.html index 817826d..2fd741f 100644 --- a/dist/index.html +++ b/dist/index.html @@ -88,6 +88,7 @@

Photos

Appearance

Calendar

Clock

+

Temperature

Text size

Theme

Brightness

From e288707c414f72f3467b0147e45bfb9e2f1d1600 Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:41:24 -0500 Subject: [PATCH 4/7] =?UTF-8?q?Mention=20=C2=B0F=20or=20=C2=B0C=20in=20the?= =?UTF-8?q?=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b13fde1..c5112f4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ copy holds that copy's secrets. - **Photos** from an iCloud shared album, straight from a phone, or from a folder. - **Text sized for the room.** Smaller, Standard or Larger, for the rows and the lists; the clock and the layout stay as they are. +- **°F or °C** as the household's country uses, or as you choose in Settings. - **It behaves itself.** Brightness follows sunrise and sunset. After a while it drifts to photos and comes back. After a power cut, or with the internet down, it shows the last good screen and says from when. From eaffac86a673be70aa905da73a04e9454a8b95b7 Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:44:13 -0500 Subject: [PATCH 5/7] Test the temperature setting --- test/settings.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/settings.test.js b/test/settings.test.js index da3a710..f87e5bb 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -54,6 +54,16 @@ test('Text size is a household setting: Standard unless chosen, one of three, ke assert.equal(settings.update({textSize: 'larger'}).textSize, 'larger'); }); +test('Temperature is a household setting: Automatic unless chosen, °F or °C once chosen, kept across a restart', () => { + const file = tempFile(), settings = createSettings({file}); + assert.equal(settings.read().temperature, 'auto'); + assert.equal(settings.update({temperature: 'K'}).temperature, 'auto', 'only the three choices are accepted'); + assert.equal(settings.update({temperature: 'c'}).temperature, 'auto', 'and exactly as written'); + assert.equal(settings.update({temperature: 'C'}).temperature, 'C'); + assert.equal(createSettings({file}).read().temperature, 'C', 'survives a restart'); + assert.equal(settings.update({temperature: 'F'}).temperature, 'F'); +}); + // The play page (DESIGN.md → "A play page, for one household"): set only in the household's settings.json, never through // the API, and dropped unless it is a plain https address. const {cleanPlayPage} = require('../settings'); From 6f1623c554ece2e70fa8a13c5225e0fc7b812fb7 Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:44:56 -0500 Subject: [PATCH 6/7] Include the temperature setting in the 0.1.0 upgrade test --- test/updates-server.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/updates-server.test.js b/test/updates-server.test.js index b863353..9f2204c 100644 --- a/test/updates-server.test.js +++ b/test/updates-server.test.js @@ -127,10 +127,11 @@ test('A household’s data from 0.1.0 reads cleanly in this version', () => { assert.equal(settings.clock, DEFAULTS.clock, 'the new setting takes its default'); assert.equal(settings.calendarView, 'week', 'and so does 0.1.2’s calendar view: the columns it always had'); assert.equal(settings.textSize, 'standard', 'and 0.1.3’s text size: the size it always had'); + assert.equal(settings.temperature, 'auto', 'and the temperature unit: Fahrenheit, as it always was, until a country is known'); assert.equal(settings.rest, 'calendar'); assert.equal(settings.screen, 'dim'); assert.equal(settings.mornings, 8); // An older version reading what this one writes: it drops what it does not know and keeps the rest. const written = home.settings.update({clock: '24'}); - const olderClean = input => Object.fromEntries(Object.entries(input).filter(([k]) => k !== 'clock' && k !== 'calendarView' && k !== 'textSize')); + const olderClean = input => Object.fromEntries(Object.entries(input).filter(([k]) => k !== 'clock' && k !== 'calendarView' && k !== 'textSize' && k !== 'temperature')); assert.deepEqual(olderClean(written), {rest: 'calendar', restAfter: 10, mornings: 8, photoEvery: 120, appearance: 'dark', screen: 'dim'}); assert.equal(createSettings({file: path.join(dir, 'settings.json')}).read().clock, '24'); fs.rmSync(data, {recursive: true, force: true}); From a736f715078ef90299b813ac412d0edea5da5aa8 Mon Sep 17 00:00:00 2001 From: pathik-vyas <351138787+pathik-vyas@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:45:41 -0500 Subject: [PATCH 7/7] =?UTF-8?q?Test=20that=20the=20wall=20shows=20=C2=B0F?= =?UTF-8?q?=20or=20=C2=B0C=20as=20the=20country=20uses,=20or=20as=20Settin?= =?UTF-8?q?gs=20says?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/wall.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/wall.test.js b/test/wall.test.js index 93c7db4..c495e90 100644 --- a/test/wall.test.js +++ b/test/wall.test.js @@ -849,3 +849,21 @@ test('A monogram\'s letter takes the ink that reads on its person\'s colour, and assert.match(css, /\.forecast small,\.agenda-wx small\{[^}]*color:var\(--rain-ink\)/, 'the rain chance is text in --rain-ink'); assert.match(css, /\.wx-icon \.drop\{stroke:var\(--rain\)\}/, 'the drops keep --rain'); }); + +test('Temperatures are in °F or °C as the household\'s country uses, or as Settings says', () => { + const house = country => ({'/api/household': {name: '', timezone: 'America/Chicago', place: 'Somewhere', ...(country ? {country} : {})}}); + const settings = temperature => ({'/api/settings': {rest: 'calendar', restAfter: 5, mornings: 0, photoEvery: 60, appearance: 'auto', screen: 'auto', clock: 'auto', temperature}}); + // The stress fixture is 101°F now, a high of 104°F and a low of 79°F, with an 80% chance of rain. + const shown = w => [w.$('now-temp').textContent, w.$('now-detail').textContent]; + // An American household, and one whose country is not known yet, keep Fahrenheit. + for (const country of ['US', '']) assert.deepEqual(shown(load({now: '2026-09-23T10:00:00', overrides: house(country)})), ['101°', 'H:104° L:79° · 80% rain']); + // A British household, on Automatic, is in Celsius, and so is the week's forecast. + let w = load({now: '2026-09-23T10:00:00', overrides: house('GB')}); + assert.deepEqual(shown(w), ['38°', 'H:40° L:26° · 80% rain']); + assert.ok(!/1\d\d°/.test(w.document.body.textContent), 'no Fahrenheit left anywhere on the wall'); + // Settings overrides the country either way, and offers the three choices. + assert.equal(load({now: '2026-09-23T10:00:00', overrides: {...house('US'), ...settings('C')}}).$('now-temp').textContent, '38°'); + assert.equal(load({now: '2026-09-23T10:00:00', overrides: {...house('GB'), ...settings('F')}}).$('now-temp').textContent, '101°'); + const choices = w.document.querySelectorAll('[data-setting]').find(el => el.getAttribute('data-setting') === 'temperature'); + assert.deepEqual(choices.children.map(b => b.textContent), ['Automatic', '°F', '°C']); +});