Skip to content

Commit e01578f

Browse files
feat(clock): add auto unit detection and country-based measurement units (#681)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent fa5a804 commit e01578f

7 files changed

Lines changed: 97 additions & 57 deletions

File tree

edge-apps/clock/screenly.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,16 @@ settings:
5858
Select the visual theme for the app ('light' or 'dark').
5959
unit:
6060
type: string
61-
default_value: metric
61+
default_value: auto
6262
title: Measurement Unit
6363
optional: true
6464
help_text:
6565
properties:
66+
advanced: true
6667
help_text: Select the measurement unit for temperature display.
6768
options:
69+
- label: Auto (based on location)
70+
value: auto
6871
- label: Metric (°C)
6972
value: metric
7073
- label: Imperial (°F)

edge-apps/clock/screenly_qc.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,16 @@ settings:
5858
Select the visual theme for the app ('light' or 'dark').
5959
unit:
6060
type: string
61-
default_value: metric
61+
default_value: auto
6262
title: Measurement Unit
6363
optional: true
6464
help_text:
6565
properties:
66+
advanced: true
6667
help_text: Select the measurement unit for temperature display.
6768
options:
69+
- label: Auto (based on location)
70+
value: auto
6871
- label: Metric (°C)
6972
value: metric
7073
- label: Imperial (°F)

edge-apps/clock/src/main.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
getTimeZone,
66
getLocale,
77
signalReady,
8-
getCityName,
8+
getCityInfo,
99
} from '@screenly/edge-apps'
1010
// Import components to register them as custom elements
1111
// This registers <brand-logo>, <app-header>, <auto-scaler>, and <edge-app-devtools>
@@ -49,8 +49,9 @@ async function updateWeatherDisplay(
4949
latitude: number,
5050
longitude: number,
5151
tz: string,
52+
countryCode: string,
5253
) {
53-
const weatherData = await getWeatherData(latitude, longitude, tz)
54+
const weatherData = await getWeatherData(latitude, longitude, tz, countryCode)
5455

5556
if (!weatherData) {
5657
hideTemperatureSection()
@@ -109,14 +110,15 @@ document.addEventListener('DOMContentLoaded', async () => {
109110
timezone = await getTimeZone()
110111
locale = await getLocale()
111112

112-
// Get location name
113-
locationName = await getCityName(latitude, longitude)
113+
// Get location info (includes city name and country code)
114+
const { cityName, countryCode } = await getCityInfo(latitude, longitude)
115+
locationName = cityName
114116
if (locationEl) {
115117
locationEl.textContent = locationName
116118
}
117119

118120
// Get weather data (optional)
119-
await updateWeatherDisplay(latitude, longitude, timezone)
121+
await updateWeatherDisplay(latitude, longitude, timezone, countryCode)
120122

121123
// Update time immediately
122124
updateTime()
@@ -127,7 +129,7 @@ document.addEventListener('DOMContentLoaded', async () => {
127129
// Refresh weather every 15 minutes
128130
setInterval(
129131
() => {
130-
updateWeatherDisplay(latitude, longitude, timezone)
132+
updateWeatherDisplay(latitude, longitude, timezone, countryCode)
131133
},
132134
15 * 60 * 1000,
133135
)

edge-apps/clock/src/weather.test.ts

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ let mockGetSettingWithDefault: <T>(key: string, defaultValue: T) => T
2222

2323
const { mock } = await import('bun:test')
2424

25+
// Import the real implementation to delegate to it in the mock
26+
const { getMeasurementUnitByCountry: realGetMeasurementUnitByCountry } =
27+
await import('@screenly/edge-apps')
28+
2529
mock.module('@screenly/edge-apps', () => ({
2630
fetchCurrentWeatherData: (
2731
lat: number,
@@ -31,8 +35,44 @@ mock.module('@screenly/edge-apps', () => ({
3135
) => mockFetchCurrentWeatherData(lat, lng, tz, unit),
3236
getSettingWithDefault: <T>(key: string, defaultValue: T) =>
3337
mockGetSettingWithDefault(key, defaultValue),
38+
getMeasurementUnitByCountry: (countryCode: string) =>
39+
realGetMeasurementUnitByCountry(countryCode),
3440
}))
3541

42+
// Mock weather data objects
43+
const metricWeatherData = {
44+
temperature: 19,
45+
tempHigh: 22,
46+
tempLow: 15,
47+
weatherId: 800,
48+
description: 'clear sky',
49+
iconSrc: '/static/images/icons/clear.svg',
50+
iconAlt: 'clear sky',
51+
unit: 'metric' as const,
52+
}
53+
54+
const imperialWeatherData = {
55+
temperature: 66,
56+
tempHigh: 70,
57+
tempLow: 60,
58+
weatherId: 800,
59+
description: 'clear sky',
60+
iconSrc: '/static/images/icons/clear.svg',
61+
iconAlt: 'clear sky',
62+
unit: 'imperial' as const,
63+
}
64+
65+
const zeroTempWeatherData = {
66+
temperature: 0,
67+
tempHigh: 3,
68+
tempLow: -2,
69+
weatherId: 800,
70+
description: 'clear sky',
71+
iconSrc: '/static/images/icons/clear.svg',
72+
iconAlt: 'clear sky',
73+
unit: 'metric' as const,
74+
}
75+
3676
describe('getWeatherData', () => {
3777
test('should return null when fetchCurrentWeatherData returns null', async () => {
3878
mockGetSettingWithDefault = (key, defaultValue) => defaultValue
@@ -42,28 +82,21 @@ describe('getWeatherData', () => {
4282
37.3861,
4383
-122.0839,
4484
'America/Los_Angeles',
85+
'GB',
4586
)
4687

4788
expect(result).toBeNull()
4889
})
4990

5091
test('should return weather data with metric units', async () => {
5192
mockGetSettingWithDefault = (key, defaultValue) => defaultValue
52-
mockFetchCurrentWeatherData = async () => ({
53-
temperature: 19,
54-
tempHigh: 22,
55-
tempLow: 15,
56-
weatherId: 800,
57-
description: 'clear sky',
58-
iconSrc: '/static/images/icons/clear.svg',
59-
iconAlt: 'clear sky',
60-
unit: 'metric',
61-
})
93+
mockFetchCurrentWeatherData = async () => metricWeatherData
6294

6395
const result = await getWeatherData(
6496
37.3861,
6597
-122.0839,
6698
'America/Los_Angeles',
99+
'GB',
67100
)
68101

69102
expect(result?.temperature).toBe(19)
@@ -73,21 +106,13 @@ describe('getWeatherData', () => {
73106
test('should return weather data with imperial units', async () => {
74107
mockGetSettingWithDefault = <T>(_key: string, _defaultValue: T): T =>
75108
'imperial' as T
76-
mockFetchCurrentWeatherData = async () => ({
77-
temperature: 66,
78-
tempHigh: 70,
79-
tempLow: 60,
80-
weatherId: 800,
81-
description: 'clear sky',
82-
iconSrc: '/static/images/icons/clear.svg',
83-
iconAlt: 'clear sky',
84-
unit: 'imperial',
85-
})
109+
mockFetchCurrentWeatherData = async () => imperialWeatherData
86110

87111
const result = await getWeatherData(
88112
37.3861,
89113
-122.0839,
90114
'America/Los_Angeles',
115+
'US',
91116
)
92117

93118
expect(result?.temperature).toBe(66)
@@ -96,18 +121,14 @@ describe('getWeatherData', () => {
96121

97122
test('should handle temperature of 0 correctly', async () => {
98123
mockGetSettingWithDefault = (key, defaultValue) => defaultValue
99-
mockFetchCurrentWeatherData = async () => ({
100-
temperature: 0,
101-
tempHigh: 3,
102-
tempLow: -2,
103-
weatherId: 800,
104-
description: 'clear sky',
105-
iconSrc: '/static/images/icons/clear.svg',
106-
iconAlt: 'clear sky',
107-
unit: 'metric',
108-
})
109-
110-
const result = await getWeatherData(59.3293, 18.0686, 'Europe/Stockholm')
124+
mockFetchCurrentWeatherData = async () => zeroTempWeatherData
125+
126+
const result = await getWeatherData(
127+
59.3293,
128+
18.0686,
129+
'Europe/Stockholm',
130+
'SE',
131+
)
111132

112133
expect(result).not.toBeNull()
113134
expect(result?.temperature).toBe(0)

edge-apps/clock/src/weather.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
fetchCurrentWeatherData,
3-
getSettingWithDefault,
4-
type MeasurementUnit,
3+
resolveMeasurementUnit,
54
} from '@screenly/edge-apps'
65

76
export interface WeatherData {
@@ -17,8 +16,11 @@ export async function getWeatherData(
1716
lat: number,
1817
lng: number,
1918
tz: string,
19+
countryCode: string,
2020
): Promise<WeatherData | null> {
21-
const unit = getSettingWithDefault<MeasurementUnit>('unit', 'metric')
21+
// Get measurement unit from settings, or auto-detect based on location
22+
const unit = resolveMeasurementUnit(countryCode)
23+
2224
const raw = await fetchCurrentWeatherData(lat, lng, tz, unit)
2325
if (!raw) return null
2426

edge-apps/edge-apps-library/src/utils/settings.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ScreenlySettings } from '../types/index.js'
2+
import { getMeasurementUnitByCountry } from './locale.js'
23

34
/**
45
* Get all Screenly settings
@@ -76,3 +77,23 @@ export function signalReady(): void {
7677

7778
// Types
7879
export type MeasurementUnit = 'metric' | 'imperial'
80+
81+
/**
82+
* Resolve measurement unit from settings with auto-detection fallback
83+
* @param countryCode - Two-character ISO country code for auto-detection
84+
* @returns Resolved measurement unit ('metric' or 'imperial')
85+
*/
86+
export function resolveMeasurementUnit(countryCode: string): MeasurementUnit {
87+
const unitSetting = getSettingWithDefault<string>('unit', 'auto')
88+
89+
if (unitSetting === 'auto') {
90+
// Auto-detect based on country when setting is explicitly 'auto'
91+
return getMeasurementUnitByCountry(countryCode)
92+
} else if (unitSetting === 'metric' || unitSetting === 'imperial') {
93+
// Only accept known valid units
94+
return unitSetting
95+
} else {
96+
// Fallback for invalid/corrupted settings: auto-detect based on country
97+
return getMeasurementUnitByCountry(countryCode)
98+
}
99+
}

edge-apps/weather/src/main.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import {
66
getLocale,
77
signalReady,
88
getSetting,
9-
getSettingWithDefault,
109
getCityInfo,
11-
getMeasurementUnitByCountry,
10+
resolveMeasurementUnit,
1211
type MeasurementUnit,
1312
} from '@screenly/edge-apps'
1413
import '@screenly/edge-apps/components'
@@ -173,18 +172,7 @@ document.addEventListener('DOMContentLoaded', async () => {
173172
}
174173

175174
// Get measurement unit from settings, or auto-detect based on location
176-
const unitSetting = getSettingWithDefault<string>('unit', 'auto')
177-
178-
if (unitSetting === 'auto') {
179-
// Auto-detect based on country when setting is explicitly 'auto'
180-
measurementUnit = getMeasurementUnitByCountry(countryCode)
181-
} else if (unitSetting === 'metric' || unitSetting === 'imperial') {
182-
// Only accept known valid units; this narrows the generic string safely
183-
measurementUnit = unitSetting
184-
} else {
185-
// Fallback for invalid/corrupted settings: auto-detect based on country
186-
measurementUnit = getMeasurementUnitByCountry(countryCode)
187-
}
175+
measurementUnit = resolveMeasurementUnit(countryCode)
188176

189177
await updateWeatherDisplay(latitude, longitude, timezone, measurementUnit)
190178

0 commit comments

Comments
 (0)