本帖最后由 relliky 于 2024-1-18 05:54 编辑
以前设备少的时候还能一个个维护一下,现在设备越买越多,家里智能设备的数量已经200+了,如果想给全家加一个新功能,用以前的办法维护起来还是比较痛苦的。
自上次向大家请教这个问题后 https://bbs.hassbian.com/thread-17467-1-1.html 已经过了大半年,阶段性的展示一下我的成果吧。(源码在底部)
-----------------------------------------基本原理(已更新)--------------------------------------------
先说说HA官方的蓝图, 它的 功能是以自动化为单位编写容易维护和复用的代码,然后让每个使用这个蓝图的自动化调用这个蓝图的代码。不了解HA官方蓝图 (blueprint) 的朋友 可以看这个贴 , 同样HA支持的复用代码思路的功能是 HA官方的脚本(script)
用这个帖子里的描述HA官方自动化蓝图的 一句话
你有五个人体传感器,你希望将这个五个人体传感器和五个灯联动,蓝图可以让你写一个自动化的模板,套用五次以后得到你期望的自动化功能
但我还想跟进一步,以房间为单位,来给写一套灵活的代码配置给各个房间,写一个房间蓝图 ,然后每个房间只需要在实例化时配置这个房间最基本的属性,就可以调用/ 复用 整个房间蓝图 的代码了。(以下我都会称为 房间蓝图 ,但每个房间蓝图还需要根据每个房间来实例化才能给每个房间个性化设置,就像我们调用HA官方的蓝图实现自己的自动化一样,以下我会称为房间蓝图实例化 )
举个类似的例子解释
房间蓝图的思路也是一样的,你有五个房间,每个房间里有十几个设备(灯光,冷暖,电视,无线开关,窗帘),然后要你希望每个房间里面的这些设备进行自动化联动,场景设置和控制面板设置等。房间蓝图可以让你写一个房间的package模板,每个package模板里有这个房间里所有10个设备的声明( 见正片第三个部分:重命名实体 ),十几个自动化,十几个场景,几个控制面板。然后你套用五次后,就得到了五个房间里所有设备的自动联动了。
这个房间蓝图可以非常的灵活,比如每次套用的时候,你可以给不同的房间不同的 参数用它去把客厅的自动化和厕所的自动化产生不同的效果。甚至如果两个部分代码冲突了,可以自定义那个房间的特色自动化和特色参数( 见正片第六个部分:最后展示一段一个厕所的房间蓝图实例化的全部配置代码 )
尽可能的去运用变量来复用代码的思路确定了,具体的实现很灵活。语言我选择用的python加上面向对象的思路,不过其他的大多数编程语言都能够写这个(包括C语言也可以),不用面向对象的思路只用普通函数进行代码复用也是可以的。
房间蓝图的代码 部分 都是python写的,每个房间都会把房间蓝图 给实例化,最后直接把python里的数据写入yaml文件,生成每个房间的yaml配置文件,然后用HA里面的package系统来读取这个文件。我用的读取方式是这个 。
homeassistant:
packages: !include_dir_merge_named packages/
复制代码
python房间蓝图 里的父类我取名叫RoomBase,然后每个房间为子类继承这个RoomBase(比如我的主卧叫MasterRoom),再根据需求进行定制,这样的话,每个房间几乎只需要100行以内的python配置代码(例子在最后),可以生成2,000行左右的yaml代码。
所有python源代码加起来近2,000行,但生成的yaml代码在20,000行左右。虽然复杂度提升了,但近十倍之差还是让很多维护工作轻松了很多。
-----------------------------------------正片时间 (已更新) --------------------------------------------
那这样可以实现以下几个功能:
1. 生成房间lovelace的代码
可以根据每个房间的设备名字,生成部分lovelace配置文件。我只需要写一个房间卡片的yaml/python代码
代码在下图,其中有很多关键字都和房间里面的传感器相关,不以这种蓝图的思路去写很难维护
然后房间蓝图可以自动生成每个房间的卡片。而且这里system卡片我需要其他的数据,我的房间蓝图还可以支持覆盖掉原来的卡片数据。
2. 生成房间的自动化开关的代码
我每个房间支持的自动化大概在20个,并且在每个房间的lovelace界面能开关他们。我的房间蓝图可以自动把他们放到一个group下,lovelace的配置只需要group卡片就能实例化这些。
type: custom:group-card
card:
type: entities
title: Automations
group: group.master_room_auto_gen_automations
复制代码
实际效果
3. 重命名实体
在每个房间里的房间蓝图实例化里面需要添加mac地址和智能设备的产品名即可,这是需要的python代码
self.add_mac_device('0x00158d000460247b', self.room_name + ' Dressing Room', 'Motion Sensor', 'Aqara Motion and Illuminance Sensor', integration='Z2M')
复制代码
我用group和template新增实体来实现重命名,这是生成的yaml代码。这样可以把自动化和实体名字和具体的智能设备分隔开。比如我要换个灯泡/传感器,直接在python里面改个mac即可。
binary_sensor:
- platform: group
name: Master Toilet Dressing Room Motion Sensor Motion
entities: binary_sensor.0x00158d000460247b_occupancy
template:
- sensor:
- name: Master Toilet Dressing Room Motion Sensor Light
unit_of_measurement: lx
state: '{{states.sensor["0x00158d000460247b_illuminance_lux"].state}}'
- sensor:
- name: Master Toilet Dressing Room Motion Sensor Battery
unit_of_measurement: '%'
state: '{{states.sensor["0x00158d000460247b_battery"].state}}'
复制代码
4. 配置每个房间需要的功能, 以及这个功能需要的自动化和实体
在每个房间里的房间蓝图实例化里面需要配置需要的自动化。下面两个是配置这些功能的python代码
self.cfg_motion_light = True -- 生成自动化根据人在自动开灯,关灯
self.cfg_temp_control = True -- 生成自动化和所需的input_boolean去支持根据人在自动开关暖气,并且支持在人不在的时候覆盖人不在的状态强制开启
复制代码
生成自动化的yaml需要的实体和自动化文件。这个可以很复杂,这里我举个简单的例子。我的每个房间里都有以下代码,这是其中一个房间生成的代码。
input_boolean:
master_room_heating_override:
name: Master Room Heating Override
initial: 'off'
复制代码
- alias: ZH-MR Heating Manual Override Timeout in Few Hours-Master Room
trigger:
- platform: state
entity_id: input_boolean.master_room_heating_override
from: 'off'
to: 'on'
action:
- delay:
hours: 2
- service: homeassistant.turn_off
entity_id: input_boolean.master_room_heating_override
id: automation.zh_mr_heating_manual_override_timeout_in_few_hours_master_room
复制代码
当然父类RoomBase里面需要写一个类似于蓝图的配置和自动化python代码,需要考虑各个房间的共同性,比如命名规范。这里我所有的手动开暖气都是以“房间名+手动暖气”的格式起名的。
self.room_heating_override = "input_boolean." + self.room_entity + "_heating_override"
self.input_boolean_dict |= {
self.getPostfix(self.room_heating_override) : {
"name" : self.getName(self.room_heating_override),
"initial": "off",
"configured": self.cfg_temp_control
}
}
复制代码
self.automations += [
{
"alias" : "ZH-" + self.automation_room_name + "Heating Manual Override Timeout in Few Hours" + "-" + self.room_name,
"configured": self.cfg_temp_control,
"trigger": [
{
"platform": "state",
"entity_id": self.room_heating_override,
"from": "off",
"to": "on"
}
],
"action": [
{
"delay": {
"hours": 2
}
},
{
"service": "homeassistant.turn_off",
"entity_id": self.room_heating_override
}
]
}
]
复制代码
5. 编写复杂的自动化
最后,一个最不起眼但是出乎意料的功能 - python可以写很复杂的自动化。比起yaml的自动化和脚本功能,python灵活很多,可以在各种地方调用和嵌套,我用小米的无线开关(小圆饼)实现了每按一次,循环不同的灯光场景。我一共设计了7个灯光场景,考虑到房间的各种状态,并且给使用者多种选择,比如:是否晚上开窗帘,白天是否强制开灯,天热时在白天是否关窗帘但开灯。这些自动化最长有的超过了1000行代码。给大家展示一下HA里面这个自动化的部分图示 (因为全部显示不下)
说说具体的实现: 因为用的函数/方法,其实每个部分都抽象化了,生成的yaml重复代码很多。python的实际代码都挺简单的。
self.automations += [{
"alias" : "ZL-" + self.automation_room_name + "Applies Different Scenes Based on Scene Selections (State Execution)" + "-" + self.room_name,
"configured": self.cfg_scene,
"trigger": [
{
"platform": "state",
"entity_id": self.room_scene_ctl,
}
],
"mode":"restart", # using restart to improve responsiveness of a scene execution
"action": [
{
"choose": [
self.callSceneServiceIfSelected("All White"),
self.callSceneServiceIfSelected("Ceiling Light White"),
self.callSceneServiceIfSelected("Lamp LED White"),
self.callSceneServiceIfSelected("LED White"),
self.callSceneServiceIfSelected("Hue"),
self.callSceneServiceIfSelected("Night Mode"),
self.callSceneServiceIfSelected("Dark Night Mode"),
self.callSceneServiceIfSelected("All Off"),
self.callSceneServiceIfSelected("Lights on in hot sunshine"),
self.callSceneServiceIfSelected("Lights on when bright outdoor"),
self.callSceneServiceIfSelected("Lights on when dark outdoor")
]
}
]
}]
复制代码
callSceneServiceIfSelected 再调用具体的callSceneService的实现7种不同场景。
def callSceneService(self, scene_name):
parallel_enable = True
scene_service = []
if scene_name == 'All White':
scene_service += [self.turn(self.lamps, "on"),
self.turn(self.ceiling_lights, "on"),
self.turn(self.leds, "on"),
self.turn(self.tvs, tv_brightness=3)]
elif scene_name == 'Ceiling Light White':
scene_service += [self.turn(self.leds + self.lamps, "off"),
self.turn(self.ceiling_lights, "on"),
self.turn(self.tvs, tv_brightness=3)]
#elif scene_name == 'Ceiling Light White with Curtain Open':
# scene_service += [self.turn(self.leds + self.lamps, "off"),
# self.turn(self.ceiling_lights, "on"),
# self.turn(self.tvs, tv_brightness=3),
# self.turn(self.curtains, 'on')]
elif scene_name == 'Lamp LED White':
scene_service += [self.turn(self.ceiling_lights, "off"),
self.turn(self.lamps, "on"),
self.turn(self.leds, "on"),
self.turn(self.tvs, tv_brightness=3)]
elif scene_name == 'LED White':
scene_service += [self.turn(self.ceiling_lights, "off"),
self.turn(self.lamps, "off"),
self.turn(self.leds, "on"),
self.turn(self.tvs, tv_brightness=2)]
elif scene_name == 'Hue':
parallel_enable = False
scene_service += [# turn on lights to let adaptive lighting run
self.turn(self.lamps + self.leds, "on"),
# overwrite adaptive lighting brightiness and set it to max
self.turn(self.lamps + self.leds, "on", light_brightness=100),
# apply colours
{ "service" : "pyscript.set_rgb_light_list",
"data": {"light_list": self.lamps + self.ceiling_lights+ self.leds}},
self.turn(self.tvs, tv_brightness=2)]
elif scene_name == 'Night Mode':
scene_service += [{ "service": "homeassistant.turn_on",
"entity_id": "scene." + self.room_entity + "_night_mode" },
self.turn(self.tvs, tv_brightness=2)]
elif scene_name == 'Dark Night Mode':
scene_service += [{ "service": "homeassistant.turn_on",
"entity_id": "scene." + self.room_entity + "_dark_night_mode" },
self.turn(self.tvs, tv_brightness=1)]
elif scene_name == 'All Off':
scene_service += [self.turn(self.ceiling_lights, "off"),
self.turn(self.lamps, "off"),
self.turn(self.leds, "off")]
elif scene_name in ['Lights on in hot sunshine',
'Lights on when bright outdoor',
'Lights on when dark outdoor']:
scene_service += [{"parallel":[
{"if": self.continueIf(self.ceiling_light_control_when[scene_name], "on"), "then": self.turn(self.ceiling_lights, "on")},
{"if": self.continueIf(self.lamp_control_when[scene_name] , "on"), "then": self.turn(self.lamps, "on")},
{"if": self.continueIf(self.led_control_when[scene_name] , "on"), "then": self.turn(self.leds, "on")},
{"if": self.continueIf(self.curtain_control_when[scene_name] , "on"), "then": self.turn(self.curtains, "on")}
]}]
else:
raise TypeError( "\n" +\
"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" + \
"Scene " + scene_name + " is not supported." + "\n" + \
"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n")
if parallel_enable == True:
scene_service = [{"parallel":scene_service}]
# Convert to a single service/entry instead of a list
return self.convertToSingleService(scene_service, alias=scene_name)
复制代码
代码的复杂度可以自行扩展。这个self.turn的函数然后又可以继续扩展,比如我在上面的代码里面让它支持调整电视的亮度,开关房间的窗帘,检测如果吸顶灯离线了,用墙壁开关代替之类的。
6. 最后展示一段一个厕所的房间蓝图实例化的全部配置代码:
class EnSuiteToilet(RoomBase):
def get_room_config(self):
super().get_room_config()
#配置需要的自动化,默认不配置任何自动化,每个房间需要把下面的参数设为真来配置自动化。
self.room_name = 'En-suite Toilet'
self.room_short_name = 'ET'
self.cfg_occupancy = True
self.cfg_group_auto = True
self.cfg_temp_control = True
self.cfg_scene = True
self.cfg_motion_light = True
self.cfg_remote_light = True
self.cfg_led_only_scene = True
def get_entity_declarations(self):
#配置房间的需要重命名的设备和传感器
super().get_entity_declarations()
self.add_mac_device("0x158d00053fdaba", self.room_name, 'Door', "Aqara Door & Window Sensor")
self.add_mac_device('0x158d000572839f', self.room_name, 'Motion Sensor', 'Aqara Motion and Illuminance Sensor')
self.add_mac_device('0x158d000522d90c', self.room_name, 'Wall Switch', 'Aqara D1 Wall Switch (With Neutral, Single Rocker)')
def get_motion_sensor_entities(self):
#厕所自己的特色部分,确定只用一个人体传感器
super().get_motion_sensor_entities()
self.all_motion_sensors = [
"binary_sensor.en_suite_toilet_motion_sensor_motion"
]
def getDashboardSettings(self):
#给lovelace配置这个房间的参数,主要是名字和logo
super().getDashboardSettings()
self.dashboard_root = 'en-suite-room'
self.dashboard_view_name = 'en-suite-toilet'
self.room_icon = 'mdi:shower-head'
复制代码
然后仅仅不到100行代码的它生成了1000多行的用yaml写的这个厕所package配置文件 auto_gen_en_suite_toilet.yaml
此项目的源码和生成的代码在这:
当房间蓝图实例化的自动化和实体文件被重新生成后,只需要reload即可生效。但lovelace文件现在还得手动复制进去,应该今年会弄成reload就行。
p.s. 我想大家很多和我一样既不是学软件的,也没在做软件,但我们论坛也有很多软件写的好的大佬,请见谅如果我的代码不够精简和周全。
评分
查看全部评分