Skip to content

Commit f8b5589

Browse files
authored
Merge pull request #680 from seamapi/add-climate-preset-support
feat: Add Thermostat Climate Preset Management
2 parents 0ebccd6 + 574a362 commit f8b5589

26 files changed

+1644
-39
lines changed

.storybook/seed-fake.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,34 @@ export const seedFake = (db) => {
412412
image_url:
413413
'https://connect.getseam.com/assets/images/devices/ecobee_3-lite_front.png',
414414
image_alt_text: 'Placeholder Lock Image',
415+
available_climate_presets: [
416+
{
417+
climate_preset_key: 'occupied',
418+
name: 'Occupied',
419+
display_name: 'Occupied',
420+
fan_mode_setting: 'auto',
421+
hvac_mode_setting: 'heat_cool',
422+
cooling_set_point_celsius: 25,
423+
heating_set_point_celsius: 20,
424+
cooling_set_point_fahrenheit: 77,
425+
heating_set_point_fahrenheit: 68,
426+
can_edit: true,
427+
can_delete: true,
428+
},
429+
{
430+
climate_preset_key: 'unoccupied',
431+
name: 'Unoccupied',
432+
display_name: 'Unoccupied',
433+
fan_mode_setting: 'auto',
434+
hvac_mode_setting: 'heat_cool',
435+
cooling_set_point_celsius: 30,
436+
heating_set_point_celsius: 15,
437+
cooling_set_point_fahrenheit: 86,
438+
heating_set_point_fahrenheit: 59,
439+
can_edit: false,
440+
can_delete: false,
441+
},
442+
],
415443
},
416444
errors: [],
417445
})

assets/icons/trash.svg

Lines changed: 5 additions & 0 deletions
Loading

package-lock.json

Lines changed: 27 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
}
128128
},
129129
"dependencies": {
130+
"@floating-ui/react": "^0.27.5",
130131
"@seamapi/http": "^1.20.0",
131132
"@tanstack/react-query": "^5.27.5",
132133
"classnames": "^2.3.2",
@@ -143,7 +144,7 @@
143144
"@rollup/plugin-replace": "^5.0.5",
144145
"@rxfork/r2wc-react-to-web-component": "^2.4.0",
145146
"@seamapi/fake-devicedb": "^1.6.1",
146-
"@seamapi/fake-seam-connect": "^1.69.1",
147+
"@seamapi/fake-seam-connect": "^1.76.0",
147148
"@seamapi/types": "^1.344.3",
148149
"@storybook/addon-designs": "^7.0.1",
149150
"@storybook/addon-essentials": "^7.0.2",

src/lib/icons/Trash.tsx

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import { useHeatCoolThermostat } from 'lib/seam/thermostats/use-heat-cool-thermo
1414
import { useHeatThermostat } from 'lib/seam/thermostats/use-heat-thermostat.js'
1515
import { useSetThermostatFanMode } from 'lib/seam/thermostats/use-set-thermostat-fan-mode.js'
1616
import { useSetThermostatOff } from 'lib/seam/thermostats/use-set-thermostat-off.js'
17+
import { Button } from 'lib/ui/Button.js'
1718
import { AccordionRow } from 'lib/ui/layout/AccordionRow.js'
1819
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
1920
import { DetailRow } from 'lib/ui/layout/DetailRow.js'
2021
import { DetailSection } from 'lib/ui/layout/DetailSection.js'
2122
import { DetailSectionGroup } from 'lib/ui/layout/DetailSectionGroup.js'
2223
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
2324
import { ClimateModeMenu } from 'lib/ui/thermostat/ClimateModeMenu.js'
25+
import { ClimatePresets } from 'lib/ui/thermostat/ClimatePresets.js'
2426
import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
2527
import { FanModeMenu } from 'lib/ui/thermostat/FanModeMenu.js'
2628
import { TemperatureControlGroup } from 'lib/ui/thermostat/TemperatureControlGroup.js'
@@ -40,16 +42,37 @@ export function ThermostatDeviceDetails({
4042
className,
4143
onEditName,
4244
}: ThermostatDeviceDetailsProps): JSX.Element | null {
45+
const [temperatureUnit, setTemperatureUnit] = useState<
46+
'fahrenheit' | 'celsius'
47+
>('fahrenheit')
48+
const [climateSettingsVisible, setClimateSettingsVisible] = useState(false)
49+
4350
if (device == null) {
4451
return null
4552
}
4653

54+
if (climateSettingsVisible) {
55+
return (
56+
<ClimatePresets
57+
device={device}
58+
temperatureUnit={temperatureUnit}
59+
onBack={() => {
60+
setClimateSettingsVisible(false)
61+
}}
62+
/>
63+
)
64+
}
65+
4766
return (
4867
<div className={classNames('seam-device-details', className)}>
4968
<ContentHeader title={t.thermostat} onBack={onBack} />
5069

5170
<div className='seam-body'>
52-
<ThermostatCard device={device} onEditName={onEditName} />
71+
<ThermostatCard
72+
onTemperatureUnitChange={setTemperatureUnit}
73+
device={device}
74+
onEditName={onEditName}
75+
/>
5376

5477
<div className='seam-thermostat-device-details'>
5578
<DetailSectionGroup>
@@ -58,6 +81,12 @@ export function ThermostatDeviceDetails({
5881
tooltipContent={t.currentSettingsTooltip}
5982
>
6083
<ClimateSettingRow device={device} />
84+
<ClimatePresetRow
85+
onClickManage={() => {
86+
setClimateSettingsVisible(true)
87+
}}
88+
device={device}
89+
/>
6190
<FanModeRow device={device} />
6291
</DetailSection>
6392

@@ -299,16 +328,38 @@ function ClimateSettingRow({
299328
)
300329
}
301330

331+
interface ClimatePresetRowProps {
332+
device: ThermostatDevice
333+
onClickManage: () => void
334+
}
335+
336+
function ClimatePresetRow({
337+
device,
338+
onClickManage,
339+
}: ClimatePresetRowProps): JSX.Element {
340+
return (
341+
<DetailRow label={t.climatePresets}>
342+
<Button onClick={onClickManage}>
343+
{t.manageNPresets(
344+
(device.properties.available_climate_presets ?? []).length
345+
)}
346+
</Button>
347+
</DetailRow>
348+
)
349+
}
350+
302351
const t = {
303352
thermostat: 'Thermostat',
304353
currentSettings: 'Current settings',
305354
currentSettingsTooltip:
306355
'These are the settings currently on the device. If you change them here, they change on the device.',
307356
climate: 'Climate',
357+
climatePresets: 'Climate presets',
308358
fanMode: 'Fan mode',
309359
none: 'None',
310360
fanModeSuccess: 'Successfully updated fan mode!',
311361
fanModeError: 'Error updating fan mode. Please try again.',
312362
climateSettingError: 'Error updating climate setting. Please try again.',
313363
saved: 'Saved',
364+
manageNPresets: (n: number) => `Manage (${n} Preset${n <= 1 ? '' : 's'})`,
314365
}

src/lib/seam/thermostats/thermostat-device.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type ThermostatDevice = Omit<Device, 'properties'> & {
1313
| 'available_hvac_mode_settings'
1414
| 'fan_mode_setting'
1515
| 'current_climate_setting'
16+
| 'available_climate_presets'
1617
>
1718
>
1819
}
@@ -36,3 +37,6 @@ export interface ClimateSetting {
3637
export const isThermostatDevice = (
3738
device: Device
3839
): device is ThermostatDevice => 'is_fan_running' in device.properties
40+
41+
export type ThermostatClimatePreset =
42+
ThermostatDevice['properties']['available_climate_presets'][number]

src/lib/seam/thermostats/unit-conversion.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import type { Device } from '@seamapi/types/connect'
22

3-
export const celsiusToFahrenheit = (t: number): number => (t * 9) / 5 + 32
3+
type ConversionReturn<T> = T extends NonNullable<number> ? number : T
44

5-
export const fahrenheitToCelsius = (t: number): number => (t - 32) * (5 / 9)
5+
export const celsiusToFahrenheit = <T>(t: T): ConversionReturn<T> =>
6+
(typeof t === 'number' ? (t * 9) / 5 + 32 : t) as ConversionReturn<T>
7+
8+
export const fahrenheitToCelsius = <T>(t: T): ConversionReturn<T> =>
9+
(typeof t === 'number' ? (t - 32) * (5 / 9) : t) as ConversionReturn<T>
610

711
export const getCoolingSetPointCelsius = (
812
variables: {
@@ -87,3 +91,9 @@ export const getHeatingSetPointFahrenheit = (
8791
undefined
8892
)
8993
}
94+
95+
export function getTemperatureUnitSymbol(
96+
type: 'fahrenheit' | 'celsius'
97+
): string {
98+
return type === 'fahrenheit' ? '°F' : '°C'
99+
}

0 commit comments

Comments
 (0)