- 积分
- 747
- 金钱
- 699
- 威望
- 0
- 贡献
- 0
- HASS币
- 0
高级会员
- 积分
- 747
- 金钱
- 699
- HASS币
- 0
|
本帖最后由 myhades 于 2024-7-23 11:26 编辑
[2027.07.23更新] 稍微改了下自动化和发送的脚本,建议更新;同时加入了应对dyson风扇3年后关机摇头的通病的缓解措施,即手动关闭摇头后再关机。
几年前因为好看买了个Dyson AM-07风扇,发现完全没有办法接入HA,因为不能用智能插座直接开关(断电后通电风扇不会打开),个人也不能接受纯红外发射这种没有状态的接入,所以一直放弃。
后来想到,如果风扇的 10(风速档位)*2(摇头开关)+1(关)=21种状态各自有不重叠的功率区间的话,通过一个带功率计的插座就能获取风扇的所有状态,加上一个万能遥控器就完成了控制和状态反馈。
这样就不需要额外买esp板子(+搭电路/画板子/打壳子),然后写固件做基于红外收发的状态获取和控制,这样做了反馈也不准确,因为可能存在红外漏收/漏发,也不能获取风扇自身状态的改变(如直接用本体上的按键操作)。
我实现了以下机制:
- 通过三组数据来记录风扇状态,分别对应【操作状态】/【假定状态】/【读取状态】来实现的读写。(在第四步有说明)
- 读取状态通过多次匹配进行确认,例如功率计报告的状态在连续 n_1 (+1) 次相同才被视为正确状态。
- 通过低标准匹配次数+写后容忍来平衡反馈速度和反馈状态准确率。
- 风扇的操作通过失败重试确保完成,即给定一个新状态后(比如设置了新的风速)在没有超过重试限度 n_2 前尽可能获取状态并重试,保证完成状态更改(这一点尤其重要,比如自动触发的离家需要确保风扇关闭)。
- 会保存假定状态,下次启动风扇无需等待状态获取即可进行风速更改/摇头更改操作,因为会在上次关机前的状态基础上发送更改指令(这个要求风扇有记忆行为)。
以上保证了稳定的状态获取,以及没有间隙要求的随意设定风扇的能力,我认为可以叫做完美接入了。
在红外没有漏发漏收的话,没有多余发送行为,具体机制请看自动化和脚本设定。
总结就是,想要完美的集成一个非智能风扇的条件如下:
- 风扇支持红外控制,且各个档位有稳定的不重叠的功率区间
- 一个带功率计的智能插座(我手上这个米家的wifi插座local polling间距最小10s,足够用了)
- 一个wifi红外遥控器(比如一般的万能遥控器)
- 有耐心实现本文,自动化和脚本不能用现成的!!!本文更多提供思路,需要有一定英文基础阅读文档+大概看懂配置然后自己替换不少东西才能用(之后空了可以做一个只需要填入对应配置的版本)
现在最大的缺点就是为了状态正确牺牲了一点回馈速度,但这很大程度上取决于插座状态的轮询速度。
按照我的默认配置(默认1次匹配+启用写后容忍+最大2次失败重试+200ms操作间隙),使用博联和米家wifi插座,操作后确认状态改变的时间在5-60秒,不操作得到一个正确的状态更新时间在20-40秒。
同时这种方案不能用于睡眠/自然风模式,因为风速不定会导致功率不稳定。
有很多地方可能还需要在说明一下,自动化和脚本也有些小问题,之后慢慢修补吧,我也是才用这个方案。
最后免责声明,实现这个没查任何资料和帖子,自动化和脚本都是我自己的写,如果已经有相同思路的帖子这个就当个补充/验证吧,等之后空了想写一版英文的在Community,所以烦请不用搬运。
如果看到这还有兴趣的话就可以接着做了。
{第一步} 首先需要确保你的风扇功率区间不重叠,我说的区间指的是由于风扇本身功率不稳定或是由于功率计误差带来的功率波动范围,对于直流风扇和大部分插座这两点应该问题不大。
测量的话比较简单,切换所有可能模式的组合,等稳定后记录功率计读数,看看是否有功率特别相近或者不稳定的模式(例如dyson的2-3档不好区分),然后建立区间,比如2w,5w,7w就建0-3.5,3.5-6.5,6.5-9以此类推。
同用Dyson AM-07的朋友可以用我的数据:
功率区间 |
风速 |
摇头 |
00.00-02.33 |
0 |
off |
02.33-04.88 |
1 |
off |
04.88-06.15 |
2 |
off |
06.15-07.46 |
1 |
on |
07.46-08.27 |
2 |
on |
08.27-09.67 |
3 |
off |
09.67-11.16 |
3 |
on |
11.16-12.63 |
4 |
off |
12.63-14.96 |
4 |
on |
14.96-17.23 |
5 |
off |
17.23-19.10 |
5 |
on |
19.10-21.06 |
6 |
off |
21.06-23.54 |
6 |
on |
23.54-25.95 |
7 |
off |
25.95-28.84 |
7 |
on |
28.84-31.77 |
8 |
off |
31.77-35.19 |
8 |
on |
35.19-38.61 |
9 |
off |
38.61-42.71 |
9 |
on |
42.71-46.96 |
10 |
off |
46.96-60.00 |
10 |
on |
{第二步} 建立如下helpers,同时创建一个Template Fan(就是一个带风速/摇头控制的风扇实体),具体需要增减哪些helper取决于你风扇支持的功能以及该功能是否能被功率区分出来,我这的例子就是电源+风速+摇头开关:
名字 |
类型 |
目的 |
{Dyson} Power |
Toggle (input_boolean) |
电源开关 |
{Dyson} Oscillate |
Toggle (input_boolean) |
摇头开关 |
{Dyson} Speed |
Number (input_number) |
速度档位(此处根据风扇情况设定,步长为1,注意最小请设为1而不是0) |
Living Room - Dyson IO |
Select (input_select) |
所有控制数据的集合 |
然后在HA配置文件里添加Template Fan设置(非必须,但是这样就可以优雅的控制并接入homekit),下为我的示例配置,请根据自己风扇进行更改,官方文档在这
注意由于我的dyson只记忆速度而不记忆摇头状态,所以我的turn_off有做相应处理。Again,根据自己风扇情况改。
fan:
- platform: template
fans:
dyson_tower_fan:
friendly_name: "Dyson Tower"
unique_id: dyson_tower_fan_template_fan
value_template: "{{ states('input_boolean.dyson_power') }}"
turn_on:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.dyson_power
turn_off:
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.dyson_power
- input_boolean.dyson_oscillate
percentage_template: >
{{ states('input_number.dyson_speed')|int(default=0)*10 if is_state('input_boolean.dyson_power', 'on') else 0 }}
speed_count: 10
set_percentage:
- service: input_boolean.turn_{{ 'on' if percentage > 0 else 'off' }}
target:
entity_id: input_boolean.dyson_power
- service: input_number.set_value
target:
entity_id: input_number.dyson_speed
data:
value: >-
{% set new=(percentage/10)|int(default=1) %}
{% set current=states('input_number.dyson_speed')|int(default=1) %}
{{ new if percentage > 0 else current }}
oscillating_template: "{{ states('input_boolean.dyson_oscillate')=='on' }}"
set_oscillating:
- service: input_boolean.turn_{{ 'on' if oscillating else 'off' }}
target:
entity_id: input_boolean.dyson_oscillate
availability_template: >-
{{
states('sensor.chuangmi_212a01_8866_electric_power')|float(default=-1)!=-1 and
states('remote.bo_lian_rm4c_mini') not in ['unavailable','unknown',None]
}}
{第三步} 首先请安装依赖的小脚本
建立以下两个脚本(以下仅供参考,不能直接使用,请替换对应的实体/配置)
【1.写命令脚本】需要注意替换的有IO helper的entity_id,即我这里的"input_select.living_room_dyson_io",
"interval_ms" 请设置自己万能遥控器和接收设备能用的最小值,不改的话默认200ms,
对应的操作请改为发射对应命令的服务,红外怎么学习和发射就不涵盖了。
alias: "{Dyson} Send Command"
sequence:
- variables:
operate_power: "{{state_attr('input_select.living_room_dyson_io','operate_power')}}"
operate_speed: >-
{{state_attr('input_select.living_room_dyson_io','operate_speed')|int(default=5)}}
operate_oscillate: "{{state_attr('input_select.living_room_dyson_io','operate_oscillate')}}"
supposed_power: "{{state_attr('input_select.living_room_dyson_io','supposed_power')}}"
supposed_speed: >-
{{state_attr('input_select.living_room_dyson_io','supposed_speed')|int(default=5)}}
supposed_oscillate: "{{state_attr('input_select.living_room_dyson_io','supposed_oscillate')}}"
interval_ms: 350
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: "{{ interval_ms|int(default=1000) }}"
- if:
- condition: template
value_template: |-
{{
operate_oscillate != supposed_oscillate and
(supposed_power=="on" or operate_power=="on")
}}
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
supposed_oscillate: "{{operate_oscillate}}"
- service: remote.send_command
target:
entity_id:
- remote.bo_lian_rm4c_mini
data:
command: oscillate_toggle
device: dyson_tower_fan
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: "{{ interval_ms|int(default=1000) }}"
- if:
- condition: template
value_template: "{{ operate_power != supposed_power}}"
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
supposed_power: "{{operate_power}}"
- service: remote.send_command
target:
entity_id:
- remote.bo_lian_rm4c_mini
data:
command: power_toggle
device: dyson_tower_fan
- if:
- condition: template
value_template: "{{ supposed_power==\"off\" }}"
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
supposed_oscillate: "off"
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: "{{ interval_ms|int(default=1000) }}"
- if:
- condition: template
value_template: "{{ operate_power==\"on\" }}"
then:
- choose:
- conditions:
- condition: template
value_template: "{{ operate_speed > supposed_speed }}"
sequence:
- repeat:
count: "{{ operate_speed - supposed_speed }}"
sequence:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
supposed_speed: >-
{{state_attr('input_select.living_room_dyson_io','supposed_speed')|int(default=1)+1}}
- service: remote.send_command
target:
entity_id:
- remote.bo_lian_rm4c_mini
data:
command: speed_up
device: dyson_tower_fan
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: "{{ interval_ms|int(default=1000) }}"
- conditions:
- condition: template
value_template: "{{ operate_speed < supposed_speed }}"
sequence:
- repeat:
count: "{{ supposed_speed - operate_speed }}"
sequence:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
supposed_speed: >-
{{state_attr('input_select.living_room_dyson_io','supposed_speed')|int(default=1)-1}}
- service: remote.send_command
target:
entity_id:
- remote.bo_lian_rm4c_mini
data:
command: speed_down
device: dyson_tower_fan
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: "{{ interval_ms|int(default=1000) }}"
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
try_count: >-
{{state_attr('input_select.living_room_dyson_io','try_count')|int(default=0)+1}}
mode: restart
icon: fapro:dyson-fan
【2. 读状态脚本】这个要自己把功率列表和对应的状态按这个格式贴上去,然后改下helper的entity_id
alias: "{Dyson} Read"
sequence:
- variables:
readings: >-
{% set fan_presets = {
'00.00-02.33': (0, 'off'),
'02.33-04.88': (1, 'off'),
'04.88-06.15': (2, 'off'),
'06.15-07.46': (1, 'on'),
'07.46-08.27': (2, 'on'),
'08.27-09.67': (3, 'off'),
'09.67-11.16': (3, 'on'),
'11.16-12.63': (4, 'off'),
'12.63-14.96': (4, 'on'),
'14.96-17.23': (5, 'off'),
'17.23-19.10': (5, 'on'),
'19.10-21.06': (6, 'off'),
'21.06-23.54': (6, 'on'),
'23.54-25.95': (7, 'off'),
'25.95-28.84': (7, 'on'),
'28.84-31.77': (8, 'off'),
'31.77-35.19': (8, 'on'),
'35.19-38.61': (9, 'off'),
'38.61-42.71': (9, 'on'),
'42.71-46.96': (10, 'off'),
'46.96-60.00': (10, 'on')
} %}
{% set power = fan_power | float(default=0) %} {% set
fan_ns=namespace(power='off', speed=-1, oscillate='off') %}
{% for range, settings in fan_presets.items() %}
{% set lower = range.split('-')[0] | float(default=-1) %}
{% set upper = range.split('-')[1] | float(default=-1) %}
{% if lower <= power < upper %}
{% set fan_ns.speed = settings[0] %}
{% set fan_ns.oscillate = settings[1] %}
{% endif %}
{% endfor %}
{% if fan_ns.speed!=0 %}
{% set fan_ns.power='on' %}
{% endif %}
{% set data={
"power": fan_ns.power,
"speed": fan_ns.speed,
"oscillate": fan_ns.oscillate
} %}
{{data}}
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
read_power: "{{ readings.power }}"
read_speed: "{{ readings.speed }}"
read_oscillate: "{{ readings.oscillate }}"
reading_match_count: |-
{% set status_match=(
readings.power==state_attr('input_select.living_room_dyson_io','read_power') and
readings.speed==state_attr('input_select.living_room_dyson_io','read_speed') and
readings.oscillate==state_attr('input_select.living_room_dyson_io','read_oscillate')
) %} {% if status_match %}
{% set new_count=state_attr('input_select.living_room_dyson_io','reading_match_count')|int(default=0)+1 %}
{% else %}
{% set new_count=0 %}
{% endif %}
{{ min(match_count_threshold, new_count) }}
mode: single
icon: fapro:dyson-fan
{第四步} 建立以下自动化(以下仅供参考,不能直接使用,请替换对应的实体/配置)
定义的配置解释:
变量名 |
数值 |
解释 |
match_count_threshold |
1 |
正常读取匹配次数要求 |
max_try_count |
2 |
失败重发最大尝试次数 |
after_write_tolerance |
false |
写后容忍开关,即才进行发送后多等待一轮匹配 |
tolerance_s |
180 |
容忍时长(秒),不要太小了不然状态可能无法写回 |
要注意改的包括:
1) 功率获取方式,就我这里的"sensor.chuangmi_212a01_8866_electric_power"
2) 改所有helper的entity_id
3) [Deprecated] 改用于遥控和插座设备是否在线判断的entity_id三组数据的IO逻辑:
[md]
假定状态
- 写入:
- 由稳定的读取状态写入
- 与步进发送信号完全同步,每发送一个指令对应更新假定状态
读取状态
- 写入:
- 写出:(读数稳定且无操作时)
操作状态
- 写入:
- 写出:
alias: "[Device] Living Room - Dyson Tower Fan"
description: ""
trigger:
- platform: state
entity_id:
- sensor.chuangmi_212a01_8866_electric_power
id: POWER_READING
- platform: state
entity_id:
- sensor.chuangmi_212a01_8866_electric_power
id: STABLE_READING
for:
hours: 0
minutes: 0
seconds: 35
not_to:
- unavailable
- unknown
- None
- platform: state
entity_id:
- input_boolean.dyson_oscillate
- input_boolean.dyson_power
- input_number.dyson_speed
id: USER_OPERATE
- platform: state
entity_id:
- input_select.living_room_dyson_io
attribute: reading_match_count
id: READING_MATCH_COUNT
condition:
- condition: or
conditions:
- condition: and
conditions:
- condition: trigger
id:
- POWER_READING
- STABLE_READING
- condition: template
value_template: |-
{{
states('script.dyson_send_command')!='on' and
(now()|as_timestamp-state_attr('input_select.living_room_dyson_io','last_operation')|as_timestamp(default=0))>2
}}
- condition: and
conditions:
- condition: trigger
id:
- USER_OPERATE
- condition: template
value_template: |-
{{
state_attr('input_select.living_room_dyson_io','last_write_back')|as_timestamp(default=0)!=0 and
(now()|as_timestamp-state_attr('input_select.living_room_dyson_io','last_write_back')|as_timestamp(default=0))>3
}}
- condition: trigger
id:
- READING_MATCH_COUNT
action:
- variables:
match_count_threshold: 1
max_try_count: 2
after_write_tolerance: false
tolerance_s: 180
- variables:
extra_count: >-
{% set
last_start=(now()|as_timestamp-state_attr('input_select.living_room_dyson_io','last_operation')|as_timestamp(default=0))
%} {% set
last_finish=(now()|as_timestamp-state_attr('script.dyson_send_command','last_triggered')|as_timestamp(default=0))
%} {{ 1 if min(last_start, last_finish)<tolerance_s and
after_write_tolerance else 0 }}
device_online: >-
{% set
measure_online=states('sensor.chuangmi_212a01_8866_electric_power')|float(default=-1)!=-1
%} {% set remote_online=states('remote.bo_lian_rm4c_mini') not in
['unavailable','unknown',None] %}
{{ measure_online and remote_online }}
- if:
- condition: trigger
id:
- POWER_READING
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
last_read: "{{ now() }}"
- service: script.dyson_read
data:
fan_power: >-
{{
states('sensor.chuangmi_212a01_8866_electric_power')|float(default=-1)
}}
match_count_threshold: "{{ match_count_threshold+extra_count }}"
- if:
- condition: trigger
id:
- USER_OPERATE
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
state: set
during_attempt: true
last_operation: "{{ now() }}"
reading_match_count: 0
try_count: 0
operate_power: "{{ states('input_boolean.dyson_power') }}"
operate_speed: "{{ states('input_number.dyson_speed')|int(default=1) }}"
operate_oscillate: "{{ states('input_boolean.dyson_oscillate') }}"
- service: script.turn_on
metadata: {}
data: {}
target:
entity_id: script.dyson_send_command
- if:
- condition: trigger
id:
- READING_MATCH_COUNT
- condition: template
value_template: >-
{{state_attr('input_select.living_room_dyson_io','reading_match_count')|int(default=0)>=(match_count_threshold+extra_count)}}
then:
- if:
- condition: template
value_template: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')=='on'
}}
then:
- if:
- condition: template
value_template: |-
{{
(state_attr('input_select.living_room_dyson_io','operate_power')!=state_attr('input_select.living_room_dyson_io','read_power') or
state_attr('input_select.living_room_dyson_io','operate_speed')!=state_attr('input_select.living_room_dyson_io','read_speed') or
state_attr('input_select.living_room_dyson_io','operate_oscillate')!=state_attr('input_select.living_room_dyson_io','read_oscillate')) and
state_attr('input_select.living_room_dyson_io','try_count')|int(default=99)<max_try_count and
state_attr('input_select.living_room_dyson_io','during_attempt')==true
}}
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
state: set
last_operation: "{{ now() }}"
reading_match_count: 0
supposed_power: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
supposed_speed: >-
{{
state_attr('input_select.living_room_dyson_io','read_speed')
}}
supposed_oscillate: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
- service: script.turn_on
metadata: {}
data: {}
target:
entity_id: script.dyson_send_command
else:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
state: read
during_attempt: false
last_write_back: "{{now()}}"
operate_power: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
operate_speed: >-
{{
state_attr('input_select.living_room_dyson_io','read_speed')
}}
operate_oscillate: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
supposed_power: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
supposed_speed: >-
{{
state_attr('input_select.living_room_dyson_io','read_speed')
}}
supposed_oscillate: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: 100
- parallel:
- service: python_script.set_state
data:
entity_id: input_boolean.dyson_power
state: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
- service: python_script.set_state
data:
entity_id: input_number.dyson_speed
state: |-
{{
state_attr('input_select.living_room_dyson_io','read_speed')|float(default=1.0)
}}
- service: python_script.set_state
data:
entity_id: input_boolean.dyson_oscillate
state: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
else:
- if:
- condition: template
value_template: |-
{{
state_attr('input_select.living_room_dyson_io','operate_power')!=state_attr('input_select.living_room_dyson_io','read_power') and
state_attr('input_select.living_room_dyson_io','try_count')|int(default=99)<max_try_count and
state_attr('input_select.living_room_dyson_io','during_attempt')==true
}}
then:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
state: set
last_operation: "{{ now() }}"
reading_match_count: 0
supposed_power: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
supposed_oscillate: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
- service: script.turn_on
metadata: {}
data: {}
target:
entity_id: script.dyson_send_command
else:
- service: python_script.set_state
data:
entity_id: input_select.living_room_dyson_io
state: read
during_attempt: false
last_write_back: "{{now()}}"
operate_power: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
operate_oscillate: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
supposed_power: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
supposed_oscillate: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: 100
- service: python_script.set_state
data:
entity_id: input_boolean.dyson_power
state: >-
{{
state_attr('input_select.living_room_dyson_io','read_power')
}}
- service: python_script.set_state
data:
entity_id: input_boolean.dyson_oscillate
state: >-
{{
state_attr('input_select.living_room_dyson_io','read_oscillate')
}}
mode: parallel
max: 3
最后补充个小细节,这个Dyson塔扇图标是我自己画的,我上传在这,请自取。
dyson-fan.zip
(1.21 KB, 下载次数: 4)
用了这个插件可以上传自己的.svg矢量图标,具体安装和怎么用请参考Repo。
Home Assistant 效果图:
HomeKit 效果图:
|
评分
-
查看全部评分
|