Skip to content

Commit

Permalink
fix: Potential issues with HVAC modes and include
Browse files Browse the repository at this point in the history
Make control mode parsing and associated types a lot clearer to reason about
Fix a potential bug around how fields like control.hvac.heat.include
were parsed.
  • Loading branch information
nervetattoo committed Apr 7, 2021
1 parent 21d649e commit e388d21
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 55 deletions.
37 changes: 24 additions & 13 deletions src/config/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,40 @@ export enum MODES {
SWING = 'swing',
}

type ControlItem =
| boolean
| {
name?: string | false
icon?: string | false
}
export type ModeValue = {
name?: string | false
icon?: string | false
include?: boolean
}

export type ControlField = Record<string, ControlItem> & {
/**
* Represents the available mode values for a mode
*
*/
export type ModeControlObject = Record<string, boolean | ModeValue> & {
_name: string
_hide_when_off: boolean
}

type ControlObject = {
hvac: boolean | ControlField
fan: boolean | ControlField
preset: boolean | ControlField
swing: boolean | ControlField
/**
* Modes (hvac, fac, preset, swing)
* that might exist as attributes on a climate entity.
* Modes can be set to a value based on a list of options
* that are provided in the attributes of the entity.
*
*/
export type ModeControlValue = boolean | ModeControlObject
type ModeControl = {
hvac: ModeControlValue
fan: ModeControlValue
preset: ModeControlValue
swing: ModeControlValue
}

interface CardConfig {
entity?: string
header: false | HeaderConfig
control?: false | ControlObject | string[]
control?: false | ModeControl | string[]
sensors?: false | Array<ConfigSensor>
setpoints?: Setpoints
decimals?: number
Expand Down
76 changes: 36 additions & 40 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ import parseHeader, { HeaderData, MODE_ICONS } from './config/header'
import parseSetpoints from './config/setpoints'
import parseService, { Service } from './config/service'

import { CardConfig, ControlField, MODES } from './config/card'
import { CardConfig, ModeValue, ModeControlObject, MODES } from './config/card'

import { ControlMode, LooseObject, Sensor, HASS, HVAC_MODES } from './types'
import {
ControlMode,
ControlModeOption,
LooseObject,
Sensor,
HASS,
HVAC_MODES,
} from './types'

const DEBOUNCE_TIMEOUT = 1000
const STEP_SIZE = 0.5
Expand Down Expand Up @@ -50,42 +57,30 @@ const DEFAULT_HIDE = {
state: false,
}

function isIncluded(key: string, values: any) {
if (typeof values === 'undefined') {
return true
}

if (Array.isArray(values)) {
return values.includes(key)
}

const type = typeof values[key]
if (type === 'boolean') {
return values[key]
} else if (type === 'object') {
return values[key].include !== false
function shouldShowModeControl(
modeOption: string,
config: Partial<ModeControlObject>
) {
if (typeof config[modeOption] === 'object') {
const obj = config[modeOption] as ModeValue
return obj.include !== false
}

return true
return config?.[modeOption] ?? true
}

function getModeList(
type: string,
attributes: LooseObject,
config: LooseObject = {}
specification: Partial<ModeControlObject> = {}
) {
return attributes[`${type}_modes`]
.filter((name) => isIncluded(name, config))
.map((name) => {
// Grab all values sans the possible include prop
// and stuff it into an object
const values = typeof config[name] === 'object' ? config[name] : {}
delete values.include
.filter((modeOption) => shouldShowModeControl(modeOption, specification))
.map((modeOption) => {
return {
icon: MODE_ICONS[name],
value: name,
name,
...values,
icon: MODE_ICONS[modeOption],
value: modeOption,
name: modeOption,
}
})
}
Expand Down Expand Up @@ -187,11 +182,12 @@ export default class SimpleThermostat extends LitElement {
const buildBasicModes = (items: any) => {
return items.filter(supportedModeType).map((type: string) => ({
type,
list: getModeList(type, attributes, {}),
hide_when_off: false,
list: getModeList(type, attributes),
}))
}

let controlModes: Array<ControlMode> = []
let controlModes: Array<Partial<ControlMode>> = []
if (this.config.control === false) {
controlModes = []
} else if (Array.isArray(this.config.control)) {
Expand All @@ -201,13 +197,13 @@ export default class SimpleThermostat extends LitElement {
if (entries.length > 0) {
controlModes = entries
.filter(([type]) => supportedModeType(type))
.map(([type, definition]: [string, ControlField]) => {
const { _name, _hide_when_off, ...config } = definition
.map(([type, definition]: [string, ModeControlObject]) => {
const { _name, _hide_when_off, ...controlField } = definition
return {
type,
hide_when_off: _hide_when_off,
name: _name,
list: getModeList(type, attributes, config),
list: getModeList(type, attributes, controlField),
}
})
} else {
Expand All @@ -220,20 +216,20 @@ export default class SimpleThermostat extends LitElement {
// Decorate mode types with active value and set to this.modes
this.modes = controlModes.map((values) => {
if (values.type === MODES.HVAC) {
const sortedList: Array<any> = []
const hvacModeValues = Object.values(HVAC_MODES)
values.list.forEach((item: LooseObject) => {
const sortedList: Array<Partial<ControlMode>> = []
const hvacModeValues = Object.values(HVAC_MODES) as Array<string>
values.list.forEach((item: ControlModeOption) => {
const index = hvacModeValues.indexOf(item.value)
sortedList[index] = item
})
return {
...values,
list: sortedList,
mode: entity.state,
}
} as ControlMode
}
const mode = attributes[`${values.type}_mode`]
return { ...values, mode }
return { ...values, mode } as ControlMode
})

if (this.config.step_size) {
Expand Down Expand Up @@ -416,10 +412,10 @@ export default class SimpleThermostat extends LitElement {
return 3
}

getUnit(): string | boolean | undefined {
getUnit(): string | boolean {
if (['boolean', 'string'].includes(typeof this.config.unit)) {
return this.config?.unit
}
return this._hass.config.unit_system.temperature
return this._hass.config?.unit_system?.temperature ?? false
}
}
9 changes: 7 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ export enum HVAC_MODES {
FAN_ONLY = 'fan_only',
}

export interface ControlModeOption {
value: string
name: string
icon: string
}
export interface ControlMode {
type: string
mode?: any
mode: any
name?: string | boolean
hide_when_off?: boolean
list: Array<Record<string, any>>
list: Array<ControlModeOption>
}

0 comments on commit e388d21

Please sign in to comment.