Skip to content

Commit

Permalink
feat: Fine grained setpoint control
Browse files Browse the repository at this point in the history
Add a new `setpoints` config that will allow manually setting
single/dual setpoints, as well as hiding 1 or more setpoints.
Remove the `hide.setpoint` config
Fixes #216

BREAKING CHANGE: hide.setpoint is removed
  • Loading branch information
nervetattoo committed Mar 31, 2021
1 parent 6747a71 commit d221728
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 71 deletions.
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ resources:

- `entity` _string_: The thermostat entity id **required**
- `header` _false|Header object_: See section about header config
- `setpoints` _false|Setpoints object_: See section about header config
- `unit` _string|bool_: Override the unit to display. Set to false to hide unit
- `decimals` _number_: Specify number of decimals to use: 1 or 0
- `fallback` _string_: Specify a text to display if a valid set point can't be determined. Defaults to `N/A`
Expand All @@ -67,7 +68,6 @@ resources:
- `temperature`: _string_ Override Temperature label
- `state`: _string_ Override State label
- `hide` _object_: Control specifically information fields to show. Defaults to showing everything
- `setpoint`: _bool_ (Default to `false`)
- `temperature`: _bool_ (Default to `false`)
- `state`: _bool_ (Default to `false`)
- `control` _object|array_ (From 0.27)
Expand Down Expand Up @@ -129,6 +129,41 @@ header:
- `heat_cool`: _string_: Use this icon for state heat_cool. Default hass:fire
- `"off"`: _string_ Use this icon for state off. Default hass:power

## Setpoints config

> New in 2.0. Old ways of hiding setpoints is deprecated

If you specify setpoints manually you must include all setpoints you want included.
Normally there are only two possibilities here; `temperature` or `target_temp_high` + `target_temp_low`. Single or dual thermostats. But, theoretically there could be multiple setpoints and this aims to support any permutation.
The new feature in 2.0 is the ability to hide one of the two setpoints for dual thermostats.

To manually specify to use the `temperature` attribute as a setpoint you do:

```yaml
setpoints:
temperature:
```

For dual thermostats:

```yaml
setpoints:
target_temp_low:
target_temp_high:
```

To hide one of the dual setpoints:

```yaml
setpoints:
target_temp_low:
hide: true
target_temp_high:
```

For climate devices supporting more setpoints you can include as many as you like.
Automatic detection of set points only work for the single/dual cases.

## Usage of the control config

In 0.27, in order to both support changes in the climate domain and to support controlling all modes like hvac, preset, fan and swing modes the old `modes` configuration have been removed and replaced with a `control` config.
Expand Down
30 changes: 30 additions & 0 deletions src/config/setpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Setpoints } from '../types'
import getEntityType from '../getEntityType'
const DUAL = 'dual' as const

export default function parseSetpoints(
setpoints: Setpoints | false,
attributes: any
) {
if (setpoints) {
const def = Object.keys(setpoints)
return def.reduce((result, name: string) => {
const sp = setpoints[name]
if (sp?.hide) return result
return {
...result,
[name]: attributes?.[name],
}
}, {})
}
const entityType = getEntityType(attributes)
if (entityType === DUAL) {
return {
target_temp_low: attributes.target_temp_low,
target_temp_high: attributes.target_temp_high,
}
}
return {
temperature: attributes.temperature,
}
}
2 changes: 1 addition & 1 deletion src/formatNumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type Options = {
}

function formatNumber(
number: Input,
number,
{ decimals = 1, fallback = 'N/A' }: Options = {}
): string {
const type = typeof number
Expand Down
110 changes: 47 additions & 63 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import renderSensors from './components/sensors'
import renderModeType from './components/modeType'

import parseHeader, { HeaderData, MODE_ICONS } from './config/header'
import parseSetpoints from './config/setpoints'

import {
CardConfig,
Expand All @@ -22,6 +23,7 @@ import {
ControlMode,
ControlField,
LooseObject,
Setpoints,
ConfigSensor,
Sensor,
Fault,
Expand All @@ -31,7 +33,6 @@ import {
MODES,
} from './types'

const DUAL = 'dual'
const DEBOUNCE_TIMEOUT = 1000
const STEP_SIZE = 0.5
const DECIMALS = 1
Expand All @@ -53,7 +54,6 @@ type ModeIcons = {

const DEFAULT_HIDE = {
temperature: false,
setpoint: false,
state: false,
}

Expand Down Expand Up @@ -93,6 +93,10 @@ function getModeList(type: string, attributes: any, config: any = {}) {
})
}

type Values = {
[key: string]: number | string
}

export default class SimpleThermostat extends LitElement {
static get styles() {
return styles
Expand All @@ -109,16 +113,14 @@ export default class SimpleThermostat extends LitElement {
@property()
entity: LooseObject = {}
@property()
entityType: any
@property()
sensors: Array<Sensor> = []
@property()
showSensors: boolean = true
@property()
name: string | false = ''
_stepSize = STEP_SIZE
@property()
_values: EntityValue
_values: Values
@property()
_updatingValues = false
@property()
Expand Down Expand Up @@ -171,18 +173,7 @@ export default class SimpleThermostat extends LitElement {

const attributes = entity.attributes

this.entityType = getEntityType(attributes)
let values
if (this.entityType === DUAL) {
values = {
target_temp_low: attributes.target_temp_low,
target_temp_high: attributes.target_temp_high,
}
} else {
values = {
temperature: attributes.temperature,
}
}
let values = parseSetpoints(this.config?.setpoints ?? false, attributes)

// If we are updating the values, and they are now equal
// we can safely assume we've been able to update the set points
Expand Down Expand Up @@ -355,42 +346,38 @@ export default class SimpleThermostat extends LitElement {
openEntityPopover: this.openEntityPopover,
})
: ''}
${_hide.setpoint
? ''
: Object.entries(_values).map(([field, value]) => {
return html`
<div class="current-wrapper ${stepLayout}">
<ha-icon-button
?disabled=${maxTemp && value >= maxTemp}
class="thermostat-trigger"
icon=${row ? ICONS.PLUS : ICONS.UP}
@click="${() =>
this.setTemperature(this._stepSize, field)}"
>
</ha-icon-button>
<h3
@click=${() => this.openEntityPopover()}
class="current--value ${_updatingValues
? 'updating'
: ''}"
>
${formatNumber(value, config)}
${unit !== false
? html` <span class="current--unit">${unit}</span> `
: ''}
</h3>
<ha-icon-button
?disabled=${minTemp && value <= minTemp}
class="thermostat-trigger"
icon=${row ? ICONS.MINUS : ICONS.DOWN}
@click="${() =>
this.setTemperature(-this._stepSize, field)}"
>
</ha-icon-button>
</div>
`
})}
${Object.entries(_values).map(([field, value]) => {
return html`
<div class="current-wrapper ${stepLayout}">
<ha-icon-button
?disabled=${maxTemp && value >= maxTemp}
class="thermostat-trigger"
icon=${row ? ICONS.PLUS : ICONS.UP}
@click="${() => this.setTemperature(this._stepSize, field)}"
>
</ha-icon-button>
<h3
@click=${() => this.openEntityPopover()}
class="current--value ${_updatingValues
? 'updating'
: nothing}"
>
${formatNumber(value as number, config)}
${unit !== false
? html` <span class="current--unit">${unit}</span> `
: nothing}
</h3>
<ha-icon-button
?disabled=${minTemp && value <= minTemp}
class="thermostat-trigger"
icon=${row ? ICONS.MINUS : ICONS.DOWN}
@click="${() => this.setTemperature(-this._stepSize, field)}"
>
</ha-icon-button>
</div>
`
})}
</section>
${this.modes.map((mode) =>
Expand All @@ -415,17 +402,14 @@ export default class SimpleThermostat extends LitElement {
})
}

setTemperature(change: number, field = 'temperature') {
setTemperature(change: number, field: string) {
this._updatingValues = true
this._values = {
...this._values,
[field]: +formatNumber(this._values[field] + change, {
decimals: this.config.decimals,
}),
}
this._debouncedSetTemperature({
...this._values,
})
const previousValue = this._values[field] as number
const newValue = previousValue + change
const { decimals } = this.config

this._values[field] = +formatNumber(newValue, { decimals })
this._debouncedSetTemperature(this._values)
}

setMode = (type: string, mode: string) => {
Expand Down
43 changes: 43 additions & 0 deletions src/test/setpoints.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import parseSetpoints from '../config/setpoints'

test('single setpoint', () => {
const match = { temperature: 20 }

expect(
parseSetpoints(
{ temperature: null },
{ temperature: 20, target_temp_low: 19 }
)
).toEqual(match)

expect(
parseSetpoints(false, { temperature: 20, target_temp_low: 19 })
).toEqual(match)
})

test('dual setpoint', () => {
const result = parseSetpoints(
{ target_temp_low: null, target_temp_high: null },
{ target_temp_high: 20, target_temp_low: 19 }
)

expect(result).toEqual({ target_temp_high: 20, target_temp_low: 19 })
})

test('dual setpoint defaults', () => {
const result = parseSetpoints(false, {
target_temp_high: 20,
target_temp_low: 19,
})

expect(result).toEqual({ target_temp_high: 20, target_temp_low: 19 })
})

test('dual setpoint hide one', () => {
const result = parseSetpoints(
{ target_temp_low: { hide: true }, target_temp_high: null },
{ target_temp_high: 20, target_temp_low: 19 }
)

expect(result).toEqual({ target_temp_high: 20 })
})
25 changes: 20 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ export interface Entity extends LooseObject {
}

export interface ControlField {
name: string
icon: string
_name: string
_hide_when_off: boolean
[key: string]: string | boolean
icon: string
[key: string]:
| string
| boolean
| {
name: string | boolean
icon: string | boolean
}
}

export interface ControlObject {
_name: string
_names?: boolean
_icons?: boolean
_headings?: boolean
[key: string]: boolean | string | ControlField
}

Expand All @@ -50,13 +58,20 @@ export interface Fault {
hide_inactive?: boolean
}

export interface Setpoint {
hide?: boolean
}

export type Setpoints = Record<string, Setpoint>

export interface CardConfig {
entity?: string
header: false | HeaderConfig
control?: false | ControlObject | ControlList
sensors?: false | Array<ConfigSensor>
setpoints?: Setpoints
decimals?: number
step_size?: number
sensors?: false | Array<ConfigSensor>
step_layout?: 'row' | 'column'
unit?: boolean | string
fallback?: string
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es6",
"target": "es2017",
"declaration": true,
"moduleResolution": "node",
"resolveJsonModule": false,
Expand Down

0 comments on commit d221728

Please sign in to comment.