updating configuration
This commit is contained in:
@@ -1,337 +0,0 @@
|
|||||||
import appdaemon.plugins.hass.hassapi as hass
|
|
||||||
import struct
|
|
||||||
import binascii
|
|
||||||
# for PID
|
|
||||||
import time
|
|
||||||
|
|
||||||
class Climate(hass.Hass):
|
|
||||||
ir_topic = "home/remote/rm2/code/set"
|
|
||||||
target_temp = 24.0 #default
|
|
||||||
pid = PID(P = 0.2, I=0.0, D=0.0)
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
if "target_temp" not in self.args or "temp_sensor" not in self.args or "ha_panel" not in self.args or "door_window" not in self.args:
|
|
||||||
self.error("Please provide target_temp, temp_sensor, ha_panel, door_window in config!")
|
|
||||||
return
|
|
||||||
self.listen_state(self.target_temp_changed, self.args['target_temp'])
|
|
||||||
self.listen_state(self.temp_sensor_changed, self.args['temp_sensor'])
|
|
||||||
self.listen_state(self.constraint_changed, self.args['constraint'])
|
|
||||||
self.listen_state(self.ha_panel_changed, self.args['ha_panel'])
|
|
||||||
self.listen_state(self.door_window_changed, self.args['door_window'])
|
|
||||||
self.target_temp_changed("", "", "", "", "")
|
|
||||||
|
|
||||||
def door_window_changed(self, entity, attribute, old, new, kwargs):
|
|
||||||
self.do_action()
|
|
||||||
|
|
||||||
def ha_panel_changed(self, entity, attribute, old, new, kwargs):
|
|
||||||
self.do_action()
|
|
||||||
|
|
||||||
def constraint_changed(self, entity, attribute, old, new, kwargs):
|
|
||||||
self.do_action()
|
|
||||||
|
|
||||||
def target_temp_changed(self, entity, attribute, old, new, kwargs):
|
|
||||||
self.target_temp = float(self.get_state(self.args['target_temp']))
|
|
||||||
self.pid.SetPoint = self.target_temp
|
|
||||||
self.log('Target temp changed to: ' + str(self.target_temp))
|
|
||||||
self.do_action()
|
|
||||||
|
|
||||||
def temp_sensor_changed(self, entity, attribute, old, new, kwargs):
|
|
||||||
self.log('New room temp: ' + str(new))
|
|
||||||
self.do_action()
|
|
||||||
|
|
||||||
def split_off(self):
|
|
||||||
remote = Remote()
|
|
||||||
code = remote.set_mode("NONE", "NONE", self.target_temp, "OFF")
|
|
||||||
self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8"))
|
|
||||||
|
|
||||||
def do_action(self):
|
|
||||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
|
||||||
self.log("Temperature control is disabled.")
|
|
||||||
self.split_off()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Проверить alarm_panel
|
|
||||||
ha_panel_state = self.get_state(self.args['ha_panel'])
|
|
||||||
if (ha_panel_state != 'disarmed'):
|
|
||||||
self.log("Nobody home. Turning off split.")
|
|
||||||
self.split_off()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Проверить датчики дверей и окон
|
|
||||||
door_window_state = self.get_state(self.args['door_window'])
|
|
||||||
if (door_window_state != 'off'):
|
|
||||||
self.log("Balcony door is opened. Turning off split.")
|
|
||||||
self.split_off()
|
|
||||||
return
|
|
||||||
|
|
||||||
room_temp = float(self.get_state(self.args['temp_sensor']))
|
|
||||||
self.pid.update(room_temp)
|
|
||||||
output = self.pid.output
|
|
||||||
self.log("PID: " + str(output) + ".")
|
|
||||||
|
|
||||||
# Определяем режим
|
|
||||||
mode = "COOLING"
|
|
||||||
room_temp = float(self.get_state(self.args['temp_sensor']))
|
|
||||||
if (room_temp >= self.target_temp):
|
|
||||||
mode = "COOLING"
|
|
||||||
else:
|
|
||||||
mode = "HEATING"
|
|
||||||
|
|
||||||
temp_diff = abs(room_temp - self.target_temp)
|
|
||||||
if (output < 0):
|
|
||||||
self.log("Temperature differs on {} degree, turn on split for {}".format(temp_diff, mode))
|
|
||||||
remote = Remote()
|
|
||||||
code = remote.set_mode(mode, "2", int(self.target_temp), "ON")
|
|
||||||
self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8"))
|
|
||||||
else:
|
|
||||||
self.log("Temperature is ok, turn off split.")
|
|
||||||
self.split_off()
|
|
||||||
|
|
||||||
#
|
|
||||||
# App to control climate device (split system)
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
#
|
|
||||||
# notify = notification platform to send notifications to
|
|
||||||
#
|
|
||||||
# None
|
|
||||||
#
|
|
||||||
# Release Notes
|
|
||||||
#
|
|
||||||
# Version 1.0:
|
|
||||||
# Initial Version
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
Library that genrates LG air conditioner remote codes
|
|
||||||
"""
|
|
||||||
FIRST_BYTE = 136 # b10001000
|
|
||||||
|
|
||||||
STATE = {
|
|
||||||
"ON": 0,
|
|
||||||
"OFF": 24, # b11000
|
|
||||||
"CHANGE_MODE": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
MODE = {
|
|
||||||
"HEATING": 4, # b100
|
|
||||||
"AUTO": 3, # b011
|
|
||||||
"DEHUIDIFICATION": 1, # b001
|
|
||||||
"COOLING": 0,
|
|
||||||
"NONE": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
TEMPERATURE_OFFSET = 15
|
|
||||||
|
|
||||||
FAN = {
|
|
||||||
"1": 1,
|
|
||||||
"2": 0,
|
|
||||||
"3": 2,
|
|
||||||
"4": 4, # b100
|
|
||||||
"NONE": 5, # b101
|
|
||||||
}
|
|
||||||
|
|
||||||
FIRST_HIGH = 8271
|
|
||||||
FIRST_LOW = 4298
|
|
||||||
ZERO_AND_ONE_HIGH = 439
|
|
||||||
ZERO_LOW = 647
|
|
||||||
ONE_LOW = 1709
|
|
||||||
|
|
||||||
BUFFER_SIZE = 59
|
|
||||||
|
|
||||||
def test_bit(num, offset):
|
|
||||||
"""
|
|
||||||
Test the num(int) if at the given offset bit is 1
|
|
||||||
"""
|
|
||||||
mask = 1 << offset
|
|
||||||
return num & mask
|
|
||||||
|
|
||||||
def set_bit(num, offset):
|
|
||||||
"""
|
|
||||||
Set bit to at the given offset to 1
|
|
||||||
"""
|
|
||||||
mask = 1 << offset
|
|
||||||
return num | mask
|
|
||||||
|
|
||||||
class Remote(object):
|
|
||||||
"""
|
|
||||||
Library that genrates LG air conditioner remote codes
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.codes = [0] * BUFFER_SIZE
|
|
||||||
self.crc = 0
|
|
||||||
|
|
||||||
def set_mode(self, mode, fan, temperature, state):
|
|
||||||
"""
|
|
||||||
Generate code and put it in the buffer
|
|
||||||
"""
|
|
||||||
self.codes[0] = FIRST_HIGH
|
|
||||||
self.codes[1] = FIRST_LOW
|
|
||||||
self.crc = 0
|
|
||||||
|
|
||||||
self.fill_buffer(0, 8, FIRST_BYTE)
|
|
||||||
self.fill_buffer(8, 5, STATE[state])
|
|
||||||
|
|
||||||
if state == 'OFF':
|
|
||||||
self.fill_buffer(13, 3, MODE['NONE'])
|
|
||||||
else:
|
|
||||||
self.fill_buffer(13, 3, MODE[mode])
|
|
||||||
|
|
||||||
if state == 'OFF':
|
|
||||||
self.fill_buffer(16, 4, 0)
|
|
||||||
else:
|
|
||||||
self.fill_buffer(16, 4, temperature - TEMPERATURE_OFFSET)
|
|
||||||
|
|
||||||
self.fill_buffer(20, 1, 0) # jet
|
|
||||||
|
|
||||||
if state == 'OFF':
|
|
||||||
self.fill_buffer(21, 3, FAN['NONE'])
|
|
||||||
else:
|
|
||||||
self.fill_buffer(21, 3, FAN[fan])
|
|
||||||
|
|
||||||
self.fill_buffer(24, 4, self.crc)
|
|
||||||
self.codes[BUFFER_SIZE - 1] = ZERO_AND_ONE_HIGH
|
|
||||||
self.codes = binascii.hexlify(self.lirc2broadlink(self.codes))
|
|
||||||
return self.codes
|
|
||||||
|
|
||||||
def lirc2broadlink(self, pulses):
|
|
||||||
array = bytearray()
|
|
||||||
|
|
||||||
for pulse in pulses:
|
|
||||||
pulse = int(pulse * 269 / 8192) # 32.84ms units
|
|
||||||
|
|
||||||
if pulse < 256:
|
|
||||||
array += bytearray(struct.pack('>B', pulse)) # big endian (1-byte)
|
|
||||||
else:
|
|
||||||
array += bytearray([0x00]) # indicate next number is 2-bytes
|
|
||||||
array += bytearray(struct.pack('>H', pulse)) # big endian (2-bytes)
|
|
||||||
|
|
||||||
packet = bytearray([0x26, 0x00]) # 0x26 = IR, 0x00 = no repeats
|
|
||||||
packet += bytearray(struct.pack('<H', len(array))) # little endian byte count
|
|
||||||
packet += array
|
|
||||||
packet += bytearray([0x0d, 0x05]) # IR terminator
|
|
||||||
|
|
||||||
# Add 0s to make ultimate packet size a multiple of 16 for 128-bit AES encryption.
|
|
||||||
remainder = (len(packet) + 4) % 16 # rm.send_data() adds 4-byte header (02 00 00 00)
|
|
||||||
if remainder:
|
|
||||||
packet += bytearray(16 - remainder)
|
|
||||||
|
|
||||||
return packet
|
|
||||||
|
|
||||||
def fill_buffer(self, pos, bits, value):
|
|
||||||
"""
|
|
||||||
Fill buffer
|
|
||||||
"""
|
|
||||||
i = bits
|
|
||||||
while i > 0:
|
|
||||||
index = 2 + 2 * (pos + bits-i)
|
|
||||||
self.codes[index] = ZERO_AND_ONE_HIGH
|
|
||||||
|
|
||||||
if test_bit(value, i - 1) != 0:
|
|
||||||
self.codes[index + 1] = ONE_LOW
|
|
||||||
else:
|
|
||||||
self.codes[index + 1] = ZERO_LOW
|
|
||||||
|
|
||||||
if test_bit(value, i - 1) != 0:
|
|
||||||
bitset = 0
|
|
||||||
bitset = set_bit(bitset, (128 + i - pos - bits - 1) % 4)
|
|
||||||
self.crc = self.crc + bitset
|
|
||||||
|
|
||||||
i -= 1
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------- PID CONTROLLER --------------------------------------------
|
|
||||||
class PID:
|
|
||||||
"""PID Controller
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, P=0.2, I=0.0, D=0.0):
|
|
||||||
|
|
||||||
self.Kp = P
|
|
||||||
self.Ki = I
|
|
||||||
self.Kd = D
|
|
||||||
|
|
||||||
self.sample_time = 0.00
|
|
||||||
self.current_time = time.time()
|
|
||||||
self.last_time = self.current_time
|
|
||||||
|
|
||||||
self.clear()
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Clears PID computations and coefficients"""
|
|
||||||
self.SetPoint = 0.0
|
|
||||||
|
|
||||||
self.PTerm = 0.0
|
|
||||||
self.ITerm = 0.0
|
|
||||||
self.DTerm = 0.0
|
|
||||||
self.last_error = 0.0
|
|
||||||
|
|
||||||
# Windup Guard
|
|
||||||
self.int_error = 0.0
|
|
||||||
self.windup_guard = 20.0
|
|
||||||
|
|
||||||
self.output = 0.0
|
|
||||||
|
|
||||||
def update(self, feedback_value):
|
|
||||||
"""Calculates PID value for given reference feedback
|
|
||||||
.. math::
|
|
||||||
u(t) = K_p e(t) + K_i \int_{0}^{t} e(t)dt + K_d {de}/{dt}
|
|
||||||
.. figure:: images/pid_1.png
|
|
||||||
:align: center
|
|
||||||
Test PID with Kp=1.2, Ki=1, Kd=0.001 (test_pid.py)
|
|
||||||
"""
|
|
||||||
error = self.SetPoint - feedback_value
|
|
||||||
|
|
||||||
self.current_time = time.time()
|
|
||||||
delta_time = self.current_time - self.last_time
|
|
||||||
delta_error = error - self.last_error
|
|
||||||
|
|
||||||
if (delta_time >= self.sample_time):
|
|
||||||
self.PTerm = self.Kp * error
|
|
||||||
self.ITerm += error * delta_time
|
|
||||||
|
|
||||||
if (self.ITerm < -self.windup_guard):
|
|
||||||
self.ITerm = -self.windup_guard
|
|
||||||
elif (self.ITerm > self.windup_guard):
|
|
||||||
self.ITerm = self.windup_guard
|
|
||||||
|
|
||||||
self.DTerm = 0.0
|
|
||||||
if delta_time > 0:
|
|
||||||
self.DTerm = delta_error / delta_time
|
|
||||||
|
|
||||||
# Remember last time and last error for next calculation
|
|
||||||
self.last_time = self.current_time
|
|
||||||
self.last_error = error
|
|
||||||
|
|
||||||
self.output = self.PTerm + (self.Ki * self.ITerm) + (self.Kd * self.DTerm)
|
|
||||||
|
|
||||||
def setKp(self, proportional_gain):
|
|
||||||
"""Determines how aggressively the PID reacts to the current error with setting Proportional Gain"""
|
|
||||||
self.Kp = proportional_gain
|
|
||||||
|
|
||||||
def setKi(self, integral_gain):
|
|
||||||
"""Determines how aggressively the PID reacts to the current error with setting Integral Gain"""
|
|
||||||
self.Ki = integral_gain
|
|
||||||
|
|
||||||
def setKd(self, derivative_gain):
|
|
||||||
"""Determines how aggressively the PID reacts to the current error with setting Derivative Gain"""
|
|
||||||
self.Kd = derivative_gain
|
|
||||||
|
|
||||||
def setWindup(self, windup):
|
|
||||||
"""Integral windup, also known as integrator windup or reset windup,
|
|
||||||
refers to the situation in a PID feedback controller where
|
|
||||||
a large change in setpoint occurs (say a positive change)
|
|
||||||
and the integral terms accumulates a significant error
|
|
||||||
during the rise (windup), thus overshooting and continuing
|
|
||||||
to increase as this accumulated error is unwound
|
|
||||||
(offset by errors in the other direction).
|
|
||||||
The specific problem is the excess overshooting.
|
|
||||||
"""
|
|
||||||
self.windup_guard = windup
|
|
||||||
|
|
||||||
def setSampleTime(self, sample_time):
|
|
||||||
"""PID that should be updated at a regular interval.
|
|
||||||
Based on a pre-determined sampe time, the PID decides if it should compute or return immediately.
|
|
||||||
"""
|
|
||||||
self.sample_time = sample_time
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Water leak detector
|
||||||
|
# close water valve and send a report
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# sensor = water leak sensor group
|
||||||
|
# valve = water valve - app will close it, when leak is detected
|
||||||
|
# notify = notify name to send notification
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class ToiletPresence(hass.Hass):
|
||||||
|
state = ""
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
if 'sensor' not in self.args:
|
||||||
|
self.error("Please provide sensor in config!")
|
||||||
|
return
|
||||||
|
self.listen_state(self.distance_changed, self.args['sensor'])
|
||||||
|
|
||||||
|
def distance_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
distance = int(new)
|
||||||
|
|
||||||
|
if distance > 1000 or distance < 850:
|
||||||
|
if self.state == "" or self.state == "off":
|
||||||
|
self.set_value("on")
|
||||||
|
elif distance < 1000 and distance > 850:
|
||||||
|
if self.state == "" or self.state == "on":
|
||||||
|
self.set_value("off")
|
||||||
|
|
||||||
|
def set_value(self, state):
|
||||||
|
self.log('Toilet presense: {}'.format(state))
|
||||||
|
self.state = state
|
||||||
|
entity_id = "sensor.toilet_presence"
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
attributes['description'] = 'Created and updated from appdaemon ({})'.format(__name__)
|
||||||
|
|
||||||
|
self.set_state(entity_id, state = state, attributes = attributes)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#################################
|
||||||
|
# WATER LEAK #
|
||||||
|
#################################
|
||||||
|
toilet_presence:
|
||||||
|
module: toilet_presence
|
||||||
|
class: ToiletPresence
|
||||||
|
# constraint: input_boolean.water_leak_control
|
||||||
|
sensor: sensor.toilet_distance
|
||||||
|
# valve: switch.water_valve
|
||||||
|
# notify: telegram
|
||||||
|
# gw_mac: !secret aqara1_mac
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -17,7 +17,8 @@ import datetime
|
|||||||
class AirFresher(hass.Hass):
|
class AirFresher(hass.Hass):
|
||||||
timers = []
|
timers = []
|
||||||
fresh_times = 0
|
fresh_times = 0
|
||||||
fresh_every_mins = 15
|
fresh_times_max = 5
|
||||||
|
fresh_every_mins = 1
|
||||||
last_fresh_date = None
|
last_fresh_date = None
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
@@ -41,8 +42,8 @@ class AirFresher(hass.Hass):
|
|||||||
self.log('Start freshing timer.')
|
self.log('Start freshing timer.')
|
||||||
self.last_fresh_date = self.datetime()
|
self.last_fresh_date = self.datetime()
|
||||||
self.fresh_times = 0
|
self.fresh_times = 0
|
||||||
newtime = self.datetime()+datetime.timedelta(seconds=5)
|
newtime = self.datetime()+datetime.timedelta(seconds=15)
|
||||||
self.timers.append(self.run_every(self.timer_tick, newtime, 4*60))
|
self.timers.append(self.run_every(self.timer_tick, newtime, self.fresh_every_mins*60))
|
||||||
|
|
||||||
def cancel_timers(self) -> None:
|
def cancel_timers(self) -> None:
|
||||||
for timer in self.timers:
|
for timer in self.timers:
|
||||||
@@ -50,15 +51,19 @@ class AirFresher(hass.Hass):
|
|||||||
timers = []
|
timers = []
|
||||||
|
|
||||||
def timer_tick(self, args) -> None:
|
def timer_tick(self, args) -> None:
|
||||||
self.fresh()
|
toilet_presence = self.get_state("sensor.toilet_presence")
|
||||||
|
self.log('Timer ticked, presence is: {}'.format(toilet_presence))
|
||||||
|
if (toilet_presence == "on"):
|
||||||
|
self.fresh()
|
||||||
|
|
||||||
def fresh(self):
|
def fresh(self):
|
||||||
self.log("current timer: {}".format(self.last_fresh_date))
|
self.log("current timer: {}".format(self.last_fresh_date))
|
||||||
|
|
||||||
self.fresh_times = self.fresh_times + 1
|
self.fresh_times = self.fresh_times + 1
|
||||||
if (self.fresh_times > 5):
|
if (self.fresh_times > self.fresh_times_max):
|
||||||
self.log('Fresh max times is reached. Canceling timer.')
|
self.log('Fresh max times is reached. Canceling timer.')
|
||||||
self.cancel_timers()
|
self.cancel_timers()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log('freshing in {}'.format(self.fresh_times))
|
self.log('freshing in {}'.format(self.fresh_times))
|
||||||
self.call_service("mqtt/publish", topic = "home/airfresher/airfresher/fresh/set", payload = "1")
|
self.call_service("mqtt/publish", topic = "home/airfresher/airfresher/fresh/set", payload = "1")
|
||||||
|
|||||||
@@ -64,11 +64,12 @@ homekit: !include configuration/homekit/homekit.yaml
|
|||||||
#power meter
|
#power meter
|
||||||
modbus: !include configuration/modbus.yaml
|
modbus: !include configuration/modbus.yaml
|
||||||
|
|
||||||
mysensors: !include configuration/mysensors.yaml
|
#mysensors: !include configuration/mysensors.yaml
|
||||||
|
|
||||||
utility_meter: !include_dir_merge_named configuration/utility_meter
|
utility_meter: !include_dir_merge_named configuration/utility_meter
|
||||||
|
|
||||||
speedtestdotnet: !include configuration/speedtest.yaml
|
speedtestdotnet: !include configuration/speedtest.yaml
|
||||||
|
yandex_smart_home: !include configuration/alice.yaml
|
||||||
|
|
||||||
# fan:
|
# fan:
|
||||||
# - platform: xiaomi_miio
|
# - platform: xiaomi_miio
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
filter:
|
||||||
|
include_domains:
|
||||||
|
- alarm_control_panel
|
||||||
|
- switch
|
||||||
|
- light
|
||||||
|
- vacuum
|
||||||
|
- cover
|
||||||
|
exclude_entities:
|
||||||
|
- switch.plug_158d00020f3a29
|
||||||
|
- switch.plug_158d00026ea017
|
||||||
|
- switch.xiaomi_miio_switch
|
||||||
|
# include_entities:
|
||||||
|
# - media_player.tv
|
||||||
|
# entity_config:
|
||||||
|
# switch.kitchen:
|
||||||
|
# name: CUSTOM_NAME_FOR_YANDEX_SMART_HOME
|
||||||
|
# light.living_room:
|
||||||
|
# room: LIVING_ROOM
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
gateways:
|
#gateways:
|
||||||
- device: !secret mysensors_gateway_ip
|
# - device: !secret mysensors_gateway_ip
|
||||||
tcp_port: 5003
|
# tcp_port: 5003
|
||||||
version: '2.0'
|
#version: '2.0'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
airfresher_reset:
|
||||||
|
sequence:
|
||||||
|
- service: mqtt.publish
|
||||||
|
data:
|
||||||
|
topic: 'home/airfresher/airfresher/remain/set'
|
||||||
|
payload: 1500
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- platform: mqtt
|
||||||
|
state_topic: 'home/bath/bath/distance'
|
||||||
|
name: 'toilet_distance'
|
||||||
|
unit_of_measurement: 'cm'
|
||||||
+1
Submodule hass/settings/custom_components/yandex_smart_home added at a323a790d1
@@ -52,6 +52,7 @@ views:
|
|||||||
- light.bath
|
- light.bath
|
||||||
- light.philips_led
|
- light.philips_led
|
||||||
- sensor.airfresher_remain
|
- sensor.airfresher_remain
|
||||||
|
- sensor.toilet_presence
|
||||||
- type: entities
|
- type: entities
|
||||||
title: "Прихожая"
|
title: "Прихожая"
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
|
|||||||
Reference in New Issue
Block a user