Skip to content

Commit f82323a

Browse files
committed
feat: Add Thermostat Climate Preset Management
1 parent 0ebccd6 commit f82323a

20 files changed

+1015
-68
lines changed

.storybook/seed-fake.js

+24
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,30 @@ 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+
},
427+
{
428+
climate_preset_key: 'unoccupied',
429+
name: 'Unoccupied',
430+
display_name: 'Unoccupied',
431+
fan_mode_setting: 'auto',
432+
hvac_mode_setting: 'heat_cool',
433+
cooling_set_point_celsius: 30,
434+
heating_set_point_celsius: 15,
435+
cooling_set_point_fahrenheit: 86,
436+
heating_set_point_fahrenheit: 59,
437+
},
438+
],
415439
},
416440
errors: [],
417441
})

assets/icons/trash.svg

+5
Loading

package-lock.json

+4-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
"@rollup/plugin-replace": "^5.0.5",
144144
"@rxfork/r2wc-react-to-web-component": "^2.4.0",
145145
"@seamapi/fake-devicedb": "^1.6.1",
146-
"@seamapi/fake-seam-connect": "^1.69.1",
146+
"@seamapi/fake-seam-connect": "^1.76.0",
147147
"@seamapi/types": "^1.344.3",
148148
"@storybook/addon-designs": "^7.0.1",
149149
"@storybook/addon-essentials": "^7.0.2",

src/lib/icons/Trash.tsx

+28
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

+50-1
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,12 +328,32 @@ 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+
Manage ({(device.properties.available_climate_presets ?? []).length}{' '}
344+
Presets)
345+
</Button>
346+
</DetailRow>
347+
)
348+
}
349+
302350
const t = {
303351
thermostat: 'Thermostat',
304352
currentSettings: 'Current settings',
305353
currentSettingsTooltip:
306354
'These are the settings currently on the device. If you change them here, they change on the device.',
307355
climate: 'Climate',
356+
climatePresets: 'Climate presets',
308357
fanMode: 'Fan mode',
309358
none: 'None',
310359
fanModeSuccess: 'Successfully updated fan mode!',

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

+1
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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type {
2+
SeamHttpApiError,
3+
ThermostatsCreateClimatePresetBody,
4+
} from '@seamapi/http/connect'
5+
import {
6+
useMutation,
7+
type UseMutationResult,
8+
useQueryClient,
9+
} from '@tanstack/react-query'
10+
11+
import type { ThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js'
12+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
13+
14+
export type UseCreateThermostatClimatePresetParams = never
15+
export type UseCreateThermostatClimatePresetData = undefined
16+
17+
export type UseCreateThermostatClimatePresetVariables = ThermostatsCreateClimatePresetBody
18+
19+
const fhToCelsius = (t?: number): number | undefined => t == null ? undefined : (t - 32) * (5 / 9)
20+
21+
type ClimatePreset = ThermostatDevice['properties']['available_climate_presets'][number];
22+
23+
export function useCreateThermostatClimatePreset(): UseMutationResult<
24+
UseCreateThermostatClimatePresetData,
25+
SeamHttpApiError,
26+
UseCreateThermostatClimatePresetVariables
27+
> {
28+
const { client } = useSeamClient()
29+
const queryClient = useQueryClient()
30+
31+
return useMutation<
32+
UseCreateThermostatClimatePresetData,
33+
SeamHttpApiError,
34+
UseCreateThermostatClimatePresetVariables
35+
>({
36+
mutationFn: async (variables) => {
37+
if (client === null) throw new NullSeamClientError()
38+
await client.thermostats.createClimatePreset(variables)
39+
},
40+
onSuccess: (_data, variables) => {
41+
const preset: ClimatePreset = {
42+
...variables,
43+
cooling_set_point_celsius: fhToCelsius(variables.cooling_set_point_fahrenheit),
44+
heating_set_point_celsius: fhToCelsius(variables.heating_set_point_fahrenheit),
45+
display_name: variables.name ?? variables.climate_preset_key,
46+
can_delete: true,
47+
can_edit: true,
48+
manual_override_allowed: true,
49+
};
50+
51+
queryClient.setQueryData<ThermostatDevice | null>(
52+
['devices', 'get', { device_id: variables.device_id }],
53+
(device) => {
54+
if (device == null) {
55+
return;
56+
}
57+
58+
return getUpdatedDevice(device, preset)
59+
}
60+
)
61+
62+
queryClient.setQueryData<ThermostatDevice[]>(
63+
['devices', 'list', { device_id: variables.device_id }],
64+
(devices): ThermostatDevice[] => {
65+
if (devices == null) {
66+
return []
67+
}
68+
69+
return devices.map((device) => {
70+
if (device.device_id === variables.device_id) {
71+
return getUpdatedDevice(device, preset)
72+
}
73+
74+
return device
75+
})
76+
}
77+
)
78+
},
79+
})
80+
}
81+
82+
83+
function getUpdatedDevice(device: ThermostatDevice, preset: ClimatePreset): ThermostatDevice {
84+
if (device == null) {
85+
return device;
86+
}
87+
88+
return {
89+
...device,
90+
properties: {
91+
...device.properties,
92+
available_climate_presets: [
93+
preset,
94+
...(device.properties.available_climate_presets ?? []),
95+
],
96+
},
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type {
2+
SeamHttpApiError,
3+
ThermostatsDeleteClimatePresetBody,
4+
} from '@seamapi/http/connect'
5+
import {
6+
useMutation,
7+
type UseMutationResult,
8+
useQueryClient,
9+
} from '@tanstack/react-query'
10+
11+
import type { ThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js'
12+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
13+
14+
export type UseDeleteThermostatClimatePresetParams = never
15+
16+
export type UseDeleteThermostatClimatePresetData = undefined
17+
18+
export type UseDeleteThermostatClimatePresetVariables = ThermostatsDeleteClimatePresetBody
19+
20+
export function useDeleteThermostatClimatePreset(): UseMutationResult<
21+
UseDeleteThermostatClimatePresetData,
22+
SeamHttpApiError,
23+
UseDeleteThermostatClimatePresetVariables
24+
> {
25+
const { client } = useSeamClient()
26+
const queryClient = useQueryClient()
27+
28+
return useMutation<
29+
UseDeleteThermostatClimatePresetData,
30+
SeamHttpApiError,
31+
UseDeleteThermostatClimatePresetVariables
32+
>({
33+
mutationFn: async (variables) => {
34+
if (client === null) throw new NullSeamClientError()
35+
await client.thermostats.deleteClimatePreset(variables)
36+
},
37+
onSuccess: (_data, variables) => {
38+
queryClient.setQueryData<ThermostatDevice | null>(
39+
['devices', 'get', { device_id: variables.device_id }],
40+
(device) => {
41+
if (device == null) {
42+
return
43+
}
44+
45+
return getUpdatedDevice(device, variables.climate_preset_key)
46+
}
47+
)
48+
49+
queryClient.setQueryData<ThermostatDevice[]>(
50+
['devices', 'list', { device_id: variables.device_id }],
51+
(devices): ThermostatDevice[] => {
52+
if (devices == null) {
53+
return []
54+
}
55+
56+
return devices.map((device) => {
57+
if (device.device_id === variables.device_id) {
58+
return getUpdatedDevice(device, variables.climate_preset_key)
59+
}
60+
61+
return device
62+
})
63+
}
64+
)
65+
},
66+
})
67+
}
68+
69+
70+
function getUpdatedDevice(device: ThermostatDevice, climatePresetKey: string): ThermostatDevice {
71+
return {
72+
...device,
73+
properties: {
74+
...device.properties,
75+
available_climate_presets: device.properties.available_climate_presets.filter(preset => preset.climate_preset_key !== climatePresetKey),
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)