diff --git a/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-313.pyc new file mode 100644 index 0000000..f6b71dc Binary files /dev/null and b/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-313.pyc differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke_heimann.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke_heimann.cpython-313.pyc new file mode 100644 index 0000000..ad50a5f Binary files /dev/null and b/custom_zha_quirks/__pycache__/ts0601_smoke_heimann.cpython-313.pyc differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke_heimann2.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke_heimann2.cpython-313.pyc new file mode 100644 index 0000000..c281141 Binary files /dev/null and b/custom_zha_quirks/__pycache__/ts0601_smoke_heimann2.cpython-313.pyc differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-313.pyc new file mode 100644 index 0000000..96ae9bc Binary files /dev/null and b/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-313.pyc differ diff --git a/custom_zha_quirks/ts0601_power_din_tuya.py b/custom_zha_quirks/ts0601_power_din_tuya.py new file mode 100644 index 0000000..eb01e47 --- /dev/null +++ b/custom_zha_quirks/ts0601_power_din_tuya.py @@ -0,0 +1,471 @@ +"""Tuya Din Power Meter.""" +import logging +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time +from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement +from zigpy.zcl.clusters.smartenergy import Metering + +from zhaquirks import Bus, LocalDataCluster +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.tuya import TuyaManufClusterAttributes, TuyaOnOff, TuyaSwitch + +TUYA_TOTAL_ENERGY_ATTR = 0x0211 #energy +TUYA_CURRENT_ATTR = 0x0212 #current +TUYA_POWER_ATTR = 0x0213 #power +TUYA_VOLTAGE_ATTR = 0x0214 #voltaje +TUYA_DIN_SWITCH_ATTR = 0x0101 #switch + +SWITCH_EVENT = "switch_event" + +"""Hiking Power Meter Attributes""" +HIKING_DIN_SWITCH_ATTR = 0x0110 +HIKING_TOTAL_ENERGY_DELIVERED_ATTR = 0x0201 +HIKING_TOTAL_ENERGY_RECEIVED_ATTR = 0x0266 +HIKING_VOLTAGE_CURRENT_ATTR = 0x0006 +HIKING_POWER_ATTR = 0x0267 +HIKING_FREQUENCY_ATTR = 0x0269 +HIKING_POWER_FACTOR_ATTR = 0x026F +HIKING_TOTAL_REACTIVE_ATTR = 0x026D +HIKING_REACTIVE_POWER_ATTR = 0x026E + +"""MatSee Power Meter Attributes""" +MATSEE_DPID_POWER_TOTAL_ID_ATTR = 0x0273 +MATSEE_DPID_POWER_ID_A_ATTR = 0x0265 +MATSEE_DPID_POWER_ID_B_ATTR = 0x0269 +MATSEE_DPID_POWER_DIRECTION_ID_A_ATTR = 0x0266 +MATSEE_DPID_POWER_DIRECTION_ID_B_ATTR = 0x0268 +MATSEE_DPID_FORWARD_ENERGY_TOTAL_A_ATTR = 0x026A +MATSEE_DPID_REVERSE_ENERGY_TOTAL_A_ATTR = 0x026B +MATSEE_DPID_FORWARD_ENERGY_TOTAL_B_ATTR = 0x026C +MATSEE_DPID_REVERSE_ENERGY_TOTAL_B_ATTR = 0x026D +MATSEE_DPID_POWER_FACTOR_A_ATTR = 0x026E +MATSEE_DPID_POWER_FACTOR_B_ATTR = 0x0279 +MATSEE_DPID_POWER_FREQ_ATTR = 0x026F +MATSEE_DPID_VOLTAGE_A_ATTR = 0x0270 +MATSEE_DPID_CURRENT_A_ATTR = 0x0271 +MATSEE_DPID_CURRENT_B_ATTR = 0x0272 +MATSEE_DPID_UPDATE_RATE_ATTR = 0x0281 +MATSEE_DPID_VOLTAGE_A_COEF_ATTR = 0x0274 +MATSEE_DPID_CURRENT_A_COEF_ATTR = 0x0275 +MATSEE_DPID_POWER_A_COEF_ATTR = 0x0276 +MATSEE_DPID_ENERGY_A_COEF_ATTR = 0x0277 +MATSEE_DPID_ENERGY_A_COEF_REV_ATTR = 0x027F +MATSEE_DPID_FREQ_COEF_ATTR = 0x027A +MATSEE_DPID_CURRENT_B_COEF_ATTR = 0x027B +MATSEE_DPID_POWER_B_COEF_ATTR = 0x027C +MATSEE_DPID_ENERGY_B_COEF_ATTR = 0x027D +MATSEE_DPID_ENERGY_B_COEF_REV_ATTR = 0x0280 + +_LOGGER = logging.getLogger(__name__) + +class TuyaManufClusterDinPower(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of the Tuya Power Meter device.""" + + attributes = { + TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True), + TUYA_CURRENT_ATTR: ("current", t.int16s, True), + TUYA_POWER_ATTR: ("power", t.uint16_t, True), + TUYA_VOLTAGE_ATTR: ("voltage", t.uint16_t, True), + TUYA_DIN_SWITCH_ATTR: ("switch", t.uint8_t, True), + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == TUYA_TOTAL_ENERGY_ATTR: + self.endpoint.smartenergy_metering.energy_deliver_reported(value / 100) + elif attrid == TUYA_CURRENT_ATTR: + self.endpoint.electrical_measurement.current_reported(value) + elif attrid == TUYA_POWER_ATTR: + self.endpoint.electrical_measurement.power_reported(value / 10) + elif attrid == TUYA_VOLTAGE_ATTR: + self.endpoint.electrical_measurement.voltage_reported(value / 10) + elif attrid == TUYA_DIN_SWITCH_ATTR: + self.endpoint.device.switch_bus.listener_event( + SWITCH_EVENT, self.endpoint.endpoint_id, value + ) + + +class TuyaPowerMeasurement(LocalDataCluster, ElectricalMeasurement): + """Custom class for power, voltage and current measurement.""" + + cluster_id = ElectricalMeasurement.cluster_id + + POWER_ID = 0x050B + VOLTAGE_ID = 0x0505 + CURRENT_ID = 0x0508 + REACTIVE_POWER_ID = 0x050E + AC_FREQUENCY_ID = 0x0300 + TOTAL_REACTIVE_POWER_ID = 0x0305 + POWER_FACTOR_ID = 0x0510 + + AC_VOLTAGE_MULTIPLIER = 0x0600 + AC_VOLTAGE_DIVISOR = 0x0601 + AC_CURRENT_MULTIPLIER = 0x0602 + AC_CURRENT_DIVISOR = 0x0603 + AC_FREQUENCY_MULTIPLIER = 0x0400 + AC_FREQUENCY_DIVISOR = 0x0401 + + _CONSTANT_ATTRIBUTES = { + AC_VOLTAGE_MULTIPLIER: 1, + AC_VOLTAGE_DIVISOR: 10, + AC_CURRENT_MULTIPLIER: 1, + AC_CURRENT_DIVISOR: 1000, + AC_FREQUENCY_MULTIPLIER: 1, + AC_FREQUENCY_DIVISOR: 100, + } + + def voltage_reported(self, value): + """Voltage reported.""" + self._update_attribute(self.VOLTAGE_ID, value) + + def power_reported(self, value): + """Power reported.""" + self._update_attribute(self.POWER_ID, value) + + def power_factor_reported(self, value): + """Power Factor reported.""" + self._update_attribute(self.POWER_FACTOR_ID, value) + + def reactive_power_reported(self, value): + """Reactive Power reported.""" + self._update_attribute(self.REACTIVE_POWER_ID, value) + + def current_reported(self, value): + """Ampers reported.""" + self._update_attribute(self.CURRENT_ID, value) + + def frequency_reported(self, value): + """AC Frequency reported.""" + self._update_attribute(self.AC_FREQUENCY_ID, value) + + def reactive_energy_reported(self, value): + """Summation Reactive Energy reported.""" + self._update_attribute(self.TOTAL_REACTIVE_POWER_ID, value) + + +class TuyaElectricalMeasurement(LocalDataCluster, Metering): + """Custom class for total energy measurement.""" + + cluster_id = Metering.cluster_id + CURRENT_DELIVERED_ID = 0x0000 + CURRENT_RECEIVED_ID = 0x0001 + #CURRENT_TIER1_DELIVERED_ID = 0x0100 + POWER_WATT = 0x0000 + + """Setting unit of measurement.""" + _CONSTANT_ATTRIBUTES = {0x0300: POWER_WATT} + + def energy_deliver_reported(self, value): + """Summation Energy Deliver reported.""" + self._update_attribute(self.CURRENT_DELIVERED_ID, value) + + def energy_receive_reported(self, value): + """Summation Energy Receive reported.""" + self._update_attribute(self.CURRENT_RECEIVED_ID, value) + + #def energy_tier1_deliver_reported(self, value): + # """Summation Energy Tier1 Receive reported.""" + # self._update_attribute(self.CURRENT_TIER1_DELIVERED_ID, value) + +class HikingManufClusterDinPower(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of the Hiking Power Meter device.""" + + attributes = { + HIKING_DIN_SWITCH_ATTR: ("switch", t.uint8_t, True), + HIKING_TOTAL_ENERGY_DELIVERED_ATTR: ("energy_delivered", t.uint32_t, True), + HIKING_TOTAL_ENERGY_RECEIVED_ATTR: ("energy_received", t.uint16_t, True), + HIKING_VOLTAGE_CURRENT_ATTR: ("voltage_current", t.uint32_t, True), + HIKING_POWER_ATTR: ("power", t.uint16_t, True), + HIKING_FREQUENCY_ATTR: ("frequency", t.uint16_t, True), + HIKING_TOTAL_REACTIVE_ATTR: ("total_reactive_energy", t.int32s, True), + HIKING_REACTIVE_POWER_ATTR: ("reactive_power", t.int16s, True), + HIKING_POWER_FACTOR_ATTR: ("power_factor", t.uint16_t, True), + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == HIKING_DIN_SWITCH_ATTR: + self.endpoint.device.switch_bus.listener_event(SWITCH_EVENT, 16, value) + elif attrid == HIKING_TOTAL_ENERGY_DELIVERED_ATTR: + self.endpoint.smartenergy_metering.energy_deliver_reported(value / 100) + elif attrid == HIKING_TOTAL_ENERGY_RECEIVED_ATTR: + self.endpoint.smartenergy_metering.energy_receive_reported(value / 100) + elif attrid == HIKING_VOLTAGE_CURRENT_ATTR: + self.endpoint.electrical_measurement.current_reported(value >> 16) + self.endpoint.electrical_measurement.voltage_reported( + (value & 0x0000FFFF) / 10 + ) + elif attrid == HIKING_POWER_ATTR: + self.endpoint.electrical_measurement.power_reported(value) + elif attrid == HIKING_FREQUENCY_ATTR: + self.endpoint.electrical_measurement.frequency_reported(value) + elif attrid == HIKING_TOTAL_REACTIVE_ATTR: + self.endpoint.electrical_measurement.reactive_energy_reported(value) + elif attrid == HIKING_REACTIVE_POWER_ATTR: + self.endpoint.electrical_measurement.reactive_power_reported(value) + elif attrid == HIKING_POWER_FACTOR_ATTR: + self.endpoint.electrical_measurement.power_factor_reported(value / 10) + +class MatSeeManufClusterDinPower(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of the Hiking Power Meter device.""" + _LOGGER.debug("PRUEBA MatSeeManufClusterDinPower") + + attributes = { + HIKING_DIN_SWITCH_ATTR: ("switch", t.uint8_t, True), + MATSEE_DPID_REVERSE_ENERGY_TOTAL_A_ATTR: ("energy_delivered", t.uint32_t, True), + MATSEE_DPID_FORWARD_ENERGY_TOTAL_A_ATTR: ("energy_received", t.uint32_t, True), + MATSEE_DPID_REVERSE_ENERGY_TOTAL_B_ATTR: ("energy_delivered_b", t.uint32_t, True), + MATSEE_DPID_FORWARD_ENERGY_TOTAL_B_ATTR: ("energy_received_b", t.uint32_t, True), + MATSEE_DPID_VOLTAGE_A_ATTR: ("voltage_current", t.uint32_t, True), + MATSEE_DPID_POWER_ID_A_ATTR: ("power", t.uint32_t, True), + MATSEE_DPID_POWER_ID_B_ATTR: ("power_b", t.uint32_t, True), + MATSEE_DPID_POWER_FREQ_ATTR: ("frequency", t.uint32_t, True), + MATSEE_DPID_POWER_TOTAL_ID_ATTR: ("total_reactive_energy", t.int32s, True), + MATSEE_DPID_POWER_A_COEF_ATTR: ("reactive_power", t.uint32_t, True), + MATSEE_DPID_POWER_B_COEF_ATTR: ("reactive_power_b", t.uint32_t, True), + MATSEE_DPID_POWER_FACTOR_A_ATTR: ("power_factor", t.uint32_t, True), + MATSEE_DPID_POWER_FACTOR_B_ATTR: ("power_factor_b", t.uint32_t, True), + MATSEE_DPID_CURRENT_A_ATTR: ("current", t.uint32_t, True), + MATSEE_DPID_CURRENT_B_ATTR: ("current_b", t.uint32_t, True), + MATSEE_DPID_POWER_DIRECTION_ID_A_ATTR: ("power_direction", t.uint8_t, True), + MATSEE_DPID_POWER_DIRECTION_ID_B_ATTR: ("power_direction_b", t.uint8_t, True), + MATSEE_DPID_UPDATE_RATE_ATTR: ("update_rate", t.uint32_t, True), + MATSEE_DPID_VOLTAGE_A_COEF_ATTR: ("voltage_coef", t.uint32_t, True), + MATSEE_DPID_CURRENT_A_COEF_ATTR: ("current_coef", t.uint32_t, True), + MATSEE_DPID_CURRENT_B_COEF_ATTR: ("current_coef_b", t.uint32_t, True), + MATSEE_DPID_ENERGY_A_COEF_ATTR: ("energy_coef", t.uint32_t, True), + MATSEE_DPID_ENERGY_B_COEF_ATTR: ("energy_coef_b", t.uint32_t, True), + MATSEE_DPID_ENERGY_A_COEF_REV_ATTR: ("energy_coef_rev", t.uint32_t, True), + MATSEE_DPID_ENERGY_B_COEF_REV_ATTR: ("energy_coef_rev_b", t.uint32_t, True), + MATSEE_DPID_FREQ_COEF_ATTR: ("frequency_coef", t.uint32_t, True), + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == HIKING_DIN_SWITCH_ATTR: + self.endpoint.device.switch_bus.listener_event(SWITCH_EVENT, 16, value) + elif attrid == MATSEE_DPID_REVERSE_ENERGY_TOTAL_A_ATTR: + self.endpoint.smartenergy_metering.energy_deliver_reported(value / 100) + elif attrid == MATSEE_DPID_FORWARD_ENERGY_TOTAL_A_ATTR: + self.endpoint.smartenergy_metering.energy_receive_reported(value / 100) + #elif attrid == MATSEE_DPID_REVERSE_ENERGY_TOTAL_B_ATTR: + # self.endpoint.smartenergy_metering.energy_tier1_deliver_reported(value / 100) + elif attrid == MATSEE_DPID_CURRENT_A_ATTR: + self.endpoint.electrical_measurement.current_reported(value) + #elif attrid == MATSEE_DPID_CURRENT_B_ATTR: + # self.endpoint.electrical_measurement.current_reported((value & 0x0000FFFF)/1000) + elif attrid == MATSEE_DPID_VOLTAGE_A_ATTR: + #self.endpoint.electrical_measurement.current_reported(value >> 16) + #self.endpoint.electrical_measurement.current_reported((value & 0x0000FFFF) / 10) + self.endpoint.electrical_measurement.voltage_reported( + value + ) + elif attrid == MATSEE_DPID_POWER_ID_A_ATTR: + self.endpoint.electrical_measurement.power_reported(value) + elif attrid == MATSEE_DPID_POWER_FREQ_ATTR: + self.endpoint.electrical_measurement.frequency_reported(value) + elif attrid == MATSEE_DPID_POWER_TOTAL_ID_ATTR: + self.endpoint.electrical_measurement.reactive_energy_reported(value) + elif attrid == MATSEE_DPID_POWER_A_COEF_ATTR: + self.endpoint.electrical_measurement.reactive_power_reported(value) + elif attrid == MATSEE_DPID_POWER_FACTOR_A_ATTR: + self.endpoint.electrical_measurement.power_factor_reported(value / 100) + + +class TuyaPowerMeter(TuyaSwitch): + """Tuya power meter device.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.switch_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # "node_descriptor": "", + # device_version=1 + # input_clusters=[0x0000, 0x0004, 0x0005, 0xef00] + # output_clusters=[0x000a, 0x0019] + MODELS_INFO: [ + ("_TZE200_byzdayie", "TS0601"), + ("_TZE200_ewxhg6o9", "TS0601"), + ("_TZE204_81yrt3lo", "TS0601"), + ], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterDinPower, + TuyaPowerMeasurement, + TuyaElectricalMeasurement, + TuyaOnOff, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + } + } + + +class HikingPowerMeter(TuyaSwitch): + """Hiking Power Meter Device - DDS238-2.""" + + signature = { + # "node_descriptor": "", + # device_version=1 + # input_clusters=[0x0000, 0x0004, 0x0005, 0xef00] + # output_clusters=[0x000a, 0x0019] + MODELS_INFO: [("_TZE200_bkkmqmyo", "TS0601")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + HikingManufClusterDinPower, + TuyaElectricalMeasurement, + TuyaPowerMeasurement, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 16: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + TuyaOnOff, + ], + OUTPUT_CLUSTERS: [], + }, + } + } + +class TuyaPowerMeter_GPP(TuyaSwitch): + """Tuya power meter device.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.switch_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # "node_descriptor": "", + # device_version=1 + # input_clusters=[0x0000, 0x0004, 0x0005, 0xef00] + # output_clusters=[0x000a, 0x0019] + MODELS_INFO: [ + ("_TZE200_lsanae15", "TS0601"), + ("_TZE204_81yrt3lo", "TS0601"), + ], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 242: { + # > 16) + self.endpoint.electrical_measurement.voltage_reported( + (value & 0x0000FFFF) / 10 + ) + elif attrid == HIKING_POWER_ATTR: + self.endpoint.electrical_measurement.power_reported(value) + elif attrid == HIKING_FREQUENCY_ATTR: + self.endpoint.electrical_measurement.frequency_reported(value) + elif attrid == HIKING_TOTAL_REACTIVE_ATTR: + self.endpoint.electrical_measurement.reactive_energy_reported(value) + elif attrid == HIKING_REACTIVE_POWER_ATTR: + self.endpoint.electrical_measurement.reactive_power_reported(value) + elif attrid == HIKING_POWER_FACTOR_ATTR: + self.endpoint.electrical_measurement.power_factor_reported(value / 10) + + +class TuyaPowerMeter(TuyaSwitch): + """Tuya power meter device.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.switch_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # "node_descriptor": "", + # device_version=1 + # input_clusters=[0x0000, 0x0004, 0x0005, 0xef00] + # output_clusters=[0x000a, 0x0019] + MODELS_INFO: [ + ("_TZE200_byzdayie", "TS0601"), + ("_TZE200_ewxhg6o9", "TS0601"), + ], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterDinPower, + TuyaPowerMeasurement, + TuyaElectricalMeasurement, + TuyaOnOff, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + } + } + + +class HikingPowerMeter(TuyaSwitch): + """Hiking Power Meter Device - DDS238-2.""" + + signature = { + # "node_descriptor": "", + # device_version=1 + # input_clusters=[0x0000, 0x0004, 0x0005, 0xef00] + # output_clusters=[0x000a, 0x0019] + MODELS_INFO: [ + ("_TZE200_bkkmqmyo", "TS0601"), + ("_TZE204_81yrt3lo", "TS0601"), + ], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + HikingManufClusterDinPower, + TuyaElectricalMeasurement, + TuyaPowerMeasurement, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 16: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + TuyaOnOff, + ], + OUTPUT_CLUSTERS: [], + }, + } + } \ No newline at end of file diff --git a/custom_zha_quirks/ts0601_smoke_tuya.py b/custom_zha_quirks/ts0601_smoke_tuya.py new file mode 100644 index 0000000..526c982 --- /dev/null +++ b/custom_zha_quirks/ts0601_smoke_tuya.py @@ -0,0 +1,120 @@ +"""Smoke Sensor.""" +import logging + +import zigpy.profiles.zha +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks import Bus +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, + ZONE_STATUS, + ZONE_TYPE, +) +from zhaquirks.tuya import TuyaManufCluster, TuyaManufClusterAttributes + +_LOGGER = logging.getLogger(__name__) + +TUYA_SMOKE_DETECTED_ATTR = 0x0401 # [0]/[1] [Detected]/[Clear]! + + +class TuyaSmokeDetectorCluster(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of the TS0601 smoke detector.""" + + attributes = { + TUYA_SMOKE_DETECTED_ATTR: ("smoke_detected", t.uint8_t, True), + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == TUYA_SMOKE_DETECTED_ATTR: + if value == 0: + self.endpoint.device.ias_bus.listener_event( + "update_zone_status", IasZone.ZoneStatus.Alarm_1 + ) + else: + self.endpoint.device.ias_bus.listener_event("update_zone_status", 0) + else: + _LOGGER.warning( + "[0x%04x:%s:0x%04x] unhandled attribute: 0x%04x", + self.endpoint.device.nwk, + self.endpoint.endpoint_id, + self.cluster_id, + attrid, + ) + + +class TuyaIasZone(CustomCluster, IasZone): + """IAS Zone.""" + + _CONSTANT_ATTRIBUTES = {ZONE_TYPE: IasZone.ZoneType.Fire_Sensor} + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.ias_bus.add_listener(self) + + def update_zone_status(self, value): + """Update IAS status.""" + super()._update_attribute(ZONE_STATUS, value) + + +class TuyaSmokeDetector0601(CustomDevice): + """TS0601 Smoke detector quirk.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.ias_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [ + ("_TZE200_aycxwiau", "TS0601"), + ("_TZE200_ntcy3xu1", "TS0601"), + ("_TZE200_vzekyi4c", "TS0601"), + ("_TZE200_uebojraa", "TS0601"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaIasZone, + TuyaSmokeDetectorCluster, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + }, + } \ No newline at end of file diff --git a/esphome/esp-athombulbflasher.yaml b/esphome/esp-athombulbflasher.yaml new file mode 100644 index 0000000..c756adb --- /dev/null +++ b/esphome/esp-athombulbflasher.yaml @@ -0,0 +1,244 @@ +############################################ +############################################# +# ATHOM 15W RGBWW Bulb Flasher +# +# # V1.0 2025-02-15 Initial Version +# +# Device https://www.athom.tech/blank-1/15w-color-bulb +# +# DESCRIPTION +# Starts up and flashes a RGBWW lightbulb a white colour +# On and off time (duty cycle) is configurable +# Colour Temp (in K) is configurable +# There is no fading between transitions +# +############################################# +############################################# + + +############################################# +# USER VARIABLE SUBSTITUTIONS +# Give the device a useful name & description here +# and change values accordingly. +############################################# +substitutions: + devicename: esp-athombulbflasher + friendly_name: "Flasher for Athom 15W Bulb" + description_comment: "Athom RGBWW light bulb that will flash on boot and allow colour temp & duty cycle adjustment" + + # Adjust the color temperature here (200 mired is approx 5000k + white_temp: "3000K" + + # Adjust the ON/OFF times for flashing. + # 600ms on, 400ms off => 60% ON, 40% OFF duty cycle + on_time_ms: "500ms" + off_time_ms: "500ms" + + +############################################# +# OTHER VARIABLE SUBSTITUTIONS +# Give the device a useful name & description here +# and change values accordingly. +############################################# + + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + api_key: !secret esp-athombulbflasher_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-athombulbflasher_ota_pass # unfortunately you can't use substitutions inside secrets names + wifi_ssid: !secret wifi_ssid + wifi_password: !secret wifi_password + fallback_ap_password: !secret fallback_ap_password + + # Add these if we are giving it a static ip, or remove them in the Wifi section + #static_ip_address: !secret esp-athombulbflasher_static_ip + #static_ip_gateway: !secret esp-athombulbflasher_gateway + #static_ip_subnet: !secret esp-athombulbflasher_subnet + + #mqtt_server: !secret mqtt_server + #mqtt_username: !secret mqtt_username + #mqtt_password: !secret mqtt_password + #mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like + + # Add these if we are using the internal web server (this is pretty processor intensive) + #web_server_username: !secret web_server_username + #web_server_password: !secret web_server_password + + #update_interval: 60s # update time for for general sensors etc + + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} # appears on the esphome page in HA + on_boot: + priority: 800 + then: + - light.turn_on: + id: my_light + effect: "Flashing White" + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +esp8266: + board: esp01_1m + +############################################# +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: INFO # INFO Level suggested, or DEBUG for testing + #baud_rate: 0 # set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) + #esp8266_store_log_strings_in_flash: false + #tx_buffer_size: 64 + +############################################# +# Enable the Home Assistant API +# https://esphome.io/components/api.html +############################################# +api: + encryption: + key: ${api_key} + +############################################# +# Enable Over the Air Update Capability +# https://esphome.io/components/ota.html?highlight=ota +############################################# +ota: + - platform: esphome + password: ${ota_pass} + +############################################# +# Safe Mode +# Safe mode will detect boot loops +# https://esphome.io/components/safe_mode +############################################# +safe_mode: + +############################################# +# Wifi Settings +# https://esphome.io/components/wifi.html +# +# Power Save mode (can reduce wifi reliability) +# NONE (least power saving, Default for ESP8266) +# LIGHT (Default for ESP32) +# HIGH (most power saving) +############################################# +wifi: + ssid: ${wifi_ssid} + password: ${wifi_password} + #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode + #manual_ip: # optional static IP address + #static_ip: ${static_ip_address} + #gateway: ${static_ip_gateway} + #subnet: ${static_ip_subnet} + ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode + ssid: ${devicename} AP + password: ${fallback_ap_password} + ap_timeout: 30min # Time until it brings up fallback AP. default is 1min + +captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails + + +############################################# +# MQTT Monitoring +# https://esphome.io/components/mqtt.html?highlight=mqtt +# MUST also have api enabled if you enable MQTT +############################################# +#mqtt: + #broker: ${mqtt_server} + #topic_prefix: ${mqtt_topic}/${devicename} + #username: ${mqtt_username} + #password: ${mqtt_password} + ##discovery: True # enable entity discovery (true is default) + ##discover_ip: True # enable device discovery (true is default) + + +############################################# +# Web Portal for display and monitoring +# Turning this off is probably a good idea to save resources. +# https://esphome.io/components/web_server.html +############################################# +#web_server: +# port: 80 +# auth: +# username: ${web_server_username} # probably a good idea to secure it +# password: ${web_server_password} + + + +# --------------------------------------------------------- +# OUTPUTS: 5 x PWM channels for the Athom 15W RGBWW bulb +# --------------------------------------------------------- +output: + - platform: esp8266_pwm + id: output_red + pin: GPIO4 + frequency: 1000 Hz + + - platform: esp8266_pwm + id: output_green + pin: GPIO5 + frequency: 1000 Hz + + - platform: esp8266_pwm + id: output_blue + pin: GPIO12 + frequency: 1000 Hz + + # Typically one channel is cold white... + - platform: esp8266_pwm + id: output_cold_white + pin: GPIO14 + frequency: 1000 Hz + + # ...and one channel is warm white (inverted on GPIO13). + - platform: esp8266_pwm + id: output_warm_white + pin: GPIO13 + frequency: 1000 Hz + inverted: true + +# --------------------------------------------------------- +# LIGHT COMPONENT: RGBWW + a strobe effect for flashing +# --------------------------------------------------------- +light: + - platform: rgbww + name: ${friendly_name} + id: my_light + red: output_red + green: output_green + blue: output_blue + cold_white: output_cold_white + warm_white: output_warm_white + + # Typical white temperature mapping for RGBWW + cold_white_color_temperature: 6500 K + warm_white_color_temperature: 2700 K + + # Ensure it always powers on (and with our desired effect). + restore_mode: ALWAYS_ON + + # You can omit this or set it to zero for no fade on effect changes. + default_transition_length: 0s + + # The custom strobe effect for flashing white + effects: + - strobe: + name: "Flashing White" + colors: + # Note the Kelvin value is replaced at compile time: + - state: True + brightness: 100% + color_temperature: "${white_temp}" + duration: ${on_time_ms} + - state: False + brightness: 0% + color_temperature: "${white_temp}" + duration: ${off_time_ms} + diff --git a/esphome/esp-leafbat.yaml b/esphome/esp-leafbat.yaml index 6c755a4..e61fcbf 100644 --- a/esphome/esp-leafbat.yaml +++ b/esphome/esp-leafbat.yaml @@ -1,6 +1,6 @@ ############################################# ############################################# -# BYD ATTO3 12V Battery Monitor +# Nissan Leaf 12V Battery Monitor # Monitoring the status of a vehicle 12V battery with # an esp8266 (D1 Mini). It will obviously only # transmit when the vehicle is within wifi range. diff --git a/esphome/esp-masterbathtowelrail.yaml b/esphome/esp-masterbathtowelrail.yaml new file mode 100644 index 0000000..53de751 --- /dev/null +++ b/esphome/esp-masterbathtowelrail.yaml @@ -0,0 +1,629 @@ +############################################# +############################################# +# MASTER BATHROOM HEATED TOWEL RAIL +# Controlled by a Sonoff Basic +# +# V1.0 2025-02-14 Initial Version +# +# INSTRUCTIONS +# - It allows a heated towel rail device to work in a standalone operation +# - On startup, it will turn on for 2 hours then go into timer mode (this allows you to just turn it on to get some heat immediately) +# - The timer has a morning and evening time (but no weekday/weekend setting) +# - Default values are 5am-7am and 9pm-Midnight (as this suits our use case) +# - It uses SNTP for time setting (but obviously only if wifi & networking are working) +# - It will default to an internal timer if no wifi. To reset internal timer, reboot the device at 12pm (noon) +# - If on a network and there is a MQTT server, you can set the 4 on/off times via MQTT (See below commands) +# - You can set 4 modes ON/OFF/TIMER/STARTUP via MQTT +# - Any new timer times set via MQTT will be remembered though a reboot +# - On a reboot, the device will always turn on for the Startup Duration (STARTUP mode, default 2 hours) +# - TIMER mode will always be switched on after startup mode is complete +# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 20 seconds (with ~2 secs in between to allow it to boot) +# +# MQTT Commands +# Values will be set in place on the update_interval time, not immediately +# Use 00:00 in 24hr format for time setting. Note there is no weekday/weekend setting +# mqtt_timer_topic/morning-on/06:00 : Time towel rail will go on +# mqtt_timer_topic/morning-off/08:00 : Time towel rail will go off +# mqtt_timer_topic/evening-on/09:00 : Time towel rail will go on +# mqtt_timer_topic/evening-off/00:00 : Time towel rail will go off +# mqtt_timer_topic/operation/ON : Towel rail permanently on +# mqtt_timer_topic/operation/OFF : Towel rail permanently off +# mqtt_timer_topic/operation/TIMER : Towel rail will obey timer settings +# mqtt_timer_topic/operation/STARTUP : Turn on for 2 hours then TIMER (also on startup) +# +############################################# +############################################# + +############################################# +# VARIABLE SUBSTITUTIONS +# Give the device a useful name & description here +# and change values accordingly. +############################################# +substitutions: + + mqtt_timer_topic: "viewroad-commands/masterbath-towelrail" # Topics you will use to change stuff + startup_duration: "120" # Minutes to stay ON in STARTUP mode before reverting to TIMER + timezone: "Pacific/Auckland" # For setting clock with snmp + + devicename: "esp-masterbathtowelrail" + friendly_name: "Master Bathroom Towelrail" + description_comment: "Sonoff Basic controlling ON/OFF/Timer for the Heated Towel Rail in the Master Bathroom" + + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + api_key: !secret esp-masterbathtowelrail_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-masterbathtowelrail_ota_pass # unfortunately you can't use substitutions inside secrets names + wifi_ssid: !secret wifi_ssid + wifi_password: !secret wifi_password + fallback_ap_password: !secret fallback_ap_password + + # Add these if we are giving it a static ip, or remove them in the Wifi section + #static_ip_address: !secret esp-occupancyoffice_static_ip + #static_ip_gateway: !secret esp-occupancyoffice_gateway + #static_ip_subnet: !secret esp-occupancyoffice_subnet + + mqtt_server: !secret mqtt_server + mqtt_username: !secret mqtt_username + mqtt_password: !secret mqtt_password + mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like + + # Add these if we are using the internal web server (this is pretty processor intensive) + #web_server_username: !secret web_server_username + #web_server_password: !secret web_server_password + + update_interval: 60s # update time for for general sensors etc + + +############################################# +# ESPHome +# https://esphome.io/components/esphome.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} #a ppears on the esphome page in HA + on_boot: + priority: 900 # high priority to run after globals are initialized + then: + - lambda: |- + // 1) Figure out the current time in "seconds from midnight" + // using SNTP if available, otherwise fallback_time * 60. + bool have_sntp = id(sntp_time).now().is_valid(); + int current_time_s = 0; + + if (have_sntp) { + auto now = id(sntp_time).now(); + current_time_s = now.hour * 3600 + now.minute * 60 + now.second; + } else { + // fallback_time is in minutes; convert to seconds + current_time_s = id(fallback_time) * 60; + } + + // 2) Compare with the last boot time + int diff = current_time_s - id(last_boot_time_s); + + // If within 20 seconds, increment boot_count; otherwise reset to 1 + if (diff >= 0 && diff <= 20) { + id(boot_count)++; + } else { + id(boot_count) = 1; + } + + // Update stored last boot time + id(last_boot_time_s) = current_time_s; + + // 3) If we've booted 4+ times in 20s => force ON mode + if (id(boot_count) >= 4) { + id(operation_mode) = 1; // ON + ESP_LOGI("power_cycle", "Detected 4 power cycles in 20s => Forcing ON mode"); + } else { + // Otherwise do your normal startup logic: + id(operation_mode) = 3; // on_boot -> sets operation_mode = 3 (STARTUP) + id(startup_timer) = 0; // and reset startup_timer = 0 (for time sync if no sntp) + ESP_LOGI("power_cycle", "Boot count=%d => STARTUP mode", id(boot_count)); + } + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +esp8266: + board: esp01_1m # The original sonoff basic + +############################################# +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: INFO #INFO Level suggested, or DEBUG for testing + #baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) + #esp8266_store_log_strings_in_flash: false + #tx_buffer_size: 64 + +############################################# +# Enable the Home Assistant API +# https://esphome.io/components/api.html +############################################# +api: + encryption: + key: ${api_key} + +############################################# +# Enable Over the Air Update Capability +# https://esphome.io/components/ota.html?highlight=ota +############################################# +ota: + - platform: esphome + password: ${ota_pass} + +############################################# +# Safe Mode +# Safe mode will detect boot loops +# https://esphome.io/components/safe_mode +############################################# +safe_mode: + +############################################# +# Wifi Settings +# https://esphome.io/components/wifi.html +# +# Power Save mode (can reduce wifi reliability) +# NONE (least power saving, Default for ESP8266) +# LIGHT (Default for ESP32) +# HIGH (most power saving) +############################################# +wifi: + ssid: ${wifi_ssid} + password: ${wifi_password} + #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode + #manual_ip: # optional static IP address + #static_ip: ${static_ip_address} + #gateway: ${static_ip_gateway} + #subnet: ${static_ip_subnet} + ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode + ssid: ${devicename} AP + password: ${fallback_ap_password} + ap_timeout: 30min # Time until it brings up fallback AP. default is 1min + +captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails + +############################################# +# Real time clock time source for ESPHome +# If it's invalid, we fall back to an internal clock +# https://esphome.io/components/time/index.html +# https://esphome.io/components/time/sntp +############################################# +time: + - platform: sntp + id: sntp_time + +############################################# +# MQTT Monitoring +# https://esphome.io/components/mqtt.html?highlight=mqtt +# MUST also have api enabled if you enable MQTT +############################################# +mqtt: + broker: ${mqtt_server} + topic_prefix: ${mqtt_topic}/${devicename} + username: ${mqtt_username} + password: ${mqtt_password} + #discovery: True # enable entity discovery (true is default) + #discover_ip: True # enable device discovery (true is default) + +############################################# +# Global Variables for use in automations etc +# https://esphome.io/guides/automations.html?highlight=globals#global-variables +############################################# +globals: + + # Tracks the time (in seconds from midnight) at the previous boot + - id: last_boot_time_s + type: int + restore_value: true + initial_value: "0" + + # Counts how many consecutive boots have occurred within 10 seconds + - id: boot_count + type: int + restore_value: true + initial_value: "0" + + # Morning On time (minutes from midnight), + # default 05:00 => 300 + - id: morning_on + type: int + restore_value: true + initial_value: "300" + + # Morning Off time (minutes from midnight), + # default 07:00 => 420 + - id: morning_off + type: int + restore_value: true + initial_value: "420" + + # Evening On time (minutes from midnight), + # default 21:00 => 1260 + - id: evening_on + type: int + restore_value: true + initial_value: "1260" + + # Evening Off time (minutes from midnight), + # default 00:00 => 0 => treat as midnight + - id: evening_off + type: int + restore_value: true + initial_value: "0" + + #################################################### + # operation_mode: + # 0 = OFF + # 1 = ON + # 2 = TIMER + # 3 = STARTUP + #################################################### + - id: operation_mode + type: int + restore_value: false + initial_value: "3" + + #################################################### + # fallback_time is used if SNTP is invalid. + # We assume user powers on the device at 12:00 noon + # => 12 * 60 = 720 minutes from midnight. + # Not restored, so it resets each boot. + #################################################### + - id: fallback_time + type: int + restore_value: false + initial_value: "720" + + #################################################### + # startup_timer: counts minutes in STARTUP mode + # After 'startup_duration' minutes, revert to TIMER. + # Not restored, so each boot starts fresh at 0. + #################################################### + - id: startup_timer + type: int + restore_value: false + initial_value: "0" + +############################################# +# Text Sensors +# https://esphome.io/components/text_sensor/index.html +############################################# +text_sensor: + +############################ +# MQTT Subscriptions +############################ + + #################################################### + # Subscribe to the Morning On time, format "HH:MM" + # We check x.size() == 5 and x[2] == ':', + # then parse x.substr(0,2) and x.substr(3,2) + # std::string uses 'substr', not 'substring'. + #################################################### + - platform: mqtt_subscribe + name: "Morning On Time" + id: morning_on_topic + topic: "${mqtt_timer_topic}/morning-on" + internal: True + on_value: + then: + - lambda: |- + // Expect "HH:MM" => total length = 5, with ':' + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); // "HH" + int minute = atoi(x.substr(3, 2).c_str()); // "MM" + id(morning_on) = hour * 60 + minute; + ESP_LOGI("timer","Received new Morning On: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Morning On format: %s", x.c_str()); + } + + #################################################### + # Morning Off time => "HH:MM" + #################################################### + - platform: mqtt_subscribe + name: "Morning Off Time" + id: morning_off_topic + topic: "${mqtt_timer_topic}/morning-off" + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); + int minute = atoi(x.substr(3, 2).c_str()); + id(morning_off) = hour * 60 + minute; + ESP_LOGI("timer","Received new Morning Off: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Morning Off format: %s", x.c_str()); + } + + #################################################### + # Evening On time => "HH:MM" + #################################################### + - platform: mqtt_subscribe + name: "Evening On Time" + id: evening_on_topic + topic: "${mqtt_timer_topic}/evening-on" + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); + int minute = atoi(x.substr(3, 2).c_str()); + id(evening_on) = hour * 60 + minute; + ESP_LOGI("timer","Received new Evening On: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Evening On format: %s", x.c_str()); + } + + #################################################### + # Evening Off time => "HH:MM" + #################################################### + - platform: mqtt_subscribe + name: "Evening Off Time" + id: evening_off_topic + topic: "${mqtt_timer_topic}/evening-off" + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); + int minute = atoi(x.substr(3, 2).c_str()); + id(evening_off) = hour * 60 + minute; + ESP_LOGI("timer","Received new Evening Off: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Evening Off format: %s", x.c_str()); + } + + #################################################### + # Subscribe to operation mode: + # OFF, ON, TIMER, STARTUP + # We do case-insensitive compare using strcasecmp + # (Requires typically included in ESPHome) + #################################################### + - platform: mqtt_subscribe + name: "Timer Operation Mode" + id: timer_operation_mode_topic + topic: "${mqtt_timer_topic}/operation" + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + /* + * In standard C++ (ESPHome), no 'equalsIgnoreCase()'. + * We use 'strcasecmp' for case-insensitive compare. + * Returns 0 if they match ignoring case. + */ + if (strcasecmp(x.c_str(), "TIMER") == 0) { + id(operation_mode) = 2; + ESP_LOGI("timer","Operation mode set to TIMER"); + } else if (strcasecmp(x.c_str(), "ON") == 0) { + id(operation_mode) = 1; + ESP_LOGI("timer","Operation mode set to ON"); + } else if (strcasecmp(x.c_str(), "OFF") == 0) { + id(operation_mode) = 0; + ESP_LOGI("timer","Operation mode set to OFF"); + } else if (strcasecmp(x.c_str(), "STARTUP") == 0) { + id(operation_mode) = 3; + id(startup_timer) = 0; + ESP_LOGI("timer","Operation mode set to STARTUP"); + } else { + ESP_LOGW("timer","Invalid operation mode: %s", x.c_str()); + } + + ###################################################### + # Expose the current operation mode (OFF, ON, TIMER, STARTUP) + ###################################################### + - platform: template + name: "Operation Mode State" + lambda: |- + // 0=OFF, 1=ON, 2=TIMER, 3=STARTUP + switch (id(operation_mode)) { + case 0: return {"OFF"}; + case 1: return {"ON"}; + case 2: return {"TIMER"}; + case 3: return {"STARTUP"}; + default: return {"UNKNOWN"}; + } + update_interval: ${update_interval} + + ###################################################### + # Expose the "Morning On" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Morning On Time State" + lambda: |- + int hour = id(morning_on) / 60; + int minute = id(morning_on) % 60; + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Expose the "Morning Off" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Morning Off Time State" + lambda: |- + int hour = id(morning_off) / 60; + int minute = id(morning_off) % 60; + // Increase buffer size to 8 just to be safe + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Expose the "Evening On" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Evening On Time State" + lambda: |- + int hour = id(evening_on) / 60; + int minute = id(evening_on) % 60; + // Increase buffer size to 8 just to be safe + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Expose the "Evening Off" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Evening Off Time State" + lambda: |- + int hour = id(evening_off) / 60; + int minute = id(evening_off) % 60; + // Increase buffer size to 8 just to be safe + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # ESPHome Info + ###################################################### + - platform: version + name: ${friendly_name} Version + - platform: wifi_info + ip_address: + name: ${friendly_name} IP Address + +############################################# +# General Sensors +# https://esphome.io/components/sensor/index.html +############################################# +sensor: + - platform: uptime # Uptime for this device + name: ${friendly_name} Uptime + update_interval: ${update_interval} + - platform: wifi_signal # Wifi Strength + name: ${friendly_name} Wifi Signal + update_interval: ${update_interval} + +#################################################### +# Relay Switch (Sonoff Basic Relay on GPIO12) +#################################################### +switch: + - platform: gpio + name: "Towel Rail Power" + pin: GPIO12 + id: relay + restore_mode: RESTORE_DEFAULT_OFF + +#################################################### +# Check every minute to decide relay state +#################################################### +interval: + - interval: ${update_interval} + then: + - lambda: |- + // operation_mode: + // 0 = OFF + // 1 = ON + // 2 = TIMER + // 3 = STARTUP + int mode = id(operation_mode); + + ////////////////////////////////////////////////// + // STARTUP MODE: Relay ON for 'startup_duration' + // minutes, then automatically revert to TIMER. + ////////////////////////////////////////////////// + if (mode == 3) { + id(startup_timer)++; + // Compare with the substitution startup_duration + if (id(startup_timer) < (int) ${startup_duration}) { + // Still within the STARTUP period => turn relay on + id(relay).turn_on(); + } else { + // After 'startup_duration' minutes => switch to TIMER + id(operation_mode) = 2; + } + // Skip the rest of the logic + return; + } + + ////////////////////////////////////////////////// + // OFF MODE => always off + ////////////////////////////////////////////////// + if (mode == 0) { + id(relay).turn_off(); + return; + } + + ////////////////////////////////////////////////// + // ON MODE => always on + ////////////////////////////////////////////////// + if (mode == 1) { + id(relay).turn_on(); + return; + } + + ////////////////////////////////////////////////// + // TIMER MODE => follow morning/evening schedule + // using SNTP if valid, else fallback_time + ////////////////////////////////////////////////// + if (mode == 2) { + auto now = id(sntp_time).now(); + bool have_sntp = now.is_valid(); + + int current_mins; + if (!have_sntp) { + // SNTP not available => fallback clock + current_mins = id(fallback_time); + // increment the fallback clock by 1 minute + id(fallback_time) += 1; + // wrap around at 1440 => next day + if (id(fallback_time) >= 1440) { + id(fallback_time) = 0; + } + } else { + // Use real time from SNTP + current_mins = now.hour * 60 + now.minute; + } + + bool should_on = false; + + // If evening_off == 0 => treat as midnight => 1440 + int evening_off_local = id(evening_off); + if (evening_off_local == 0) { + evening_off_local = 1440; + } + + // Check morning window + // Example: morning_on=360 => 06:00, morning_off=480 => 08:00 + // If current_mins in [360..480), should_on = true + if (id(morning_on) < id(morning_off)) { + if (current_mins >= id(morning_on) && current_mins < id(morning_off)) { + should_on = true; + } + } + + // Check evening window + // Example: evening_on=540 => 09:00, evening_off=1440 => midnight + if (id(evening_on) < evening_off_local) { + if (current_mins >= id(evening_on) && current_mins < evening_off_local) { + should_on = true; + } + } + + // Final relay state based on schedule + if (should_on) { + id(relay).turn_on(); + } else { + id(relay).turn_off(); + } + } \ No newline at end of file diff --git a/esphome/trash/esp-entmulti.yaml b/esphome/trash/esp-entmulti.yaml deleted file mode 100644 index 3329c93..0000000 --- a/esphome/trash/esp-entmulti.yaml +++ /dev/null @@ -1,488 +0,0 @@ -############################################# -############################################# -# -# -############################################# -############################################# - -############################################# -# Variable Substitutions -# Give the device a useful name & description here -# and change values accordingly. -############################################# -substitutions: - devicename: "esp-entmulti" - friendly_name: "Outside Entrance Multisensor" - description_comment: "D1 Mini ESP32 outside entranceway with, mmWave presence, PIR and more" - - #if NOT using a secrets file, just replace these with the passwords etc (in quotes) - api_key: !secret esp-entmulti_api_key #unfortunately you can't use substitutions in secrets names - ota_pass: !secret esp-entmulti_ota_pass #unfortunately you can't use substitutions in secrets names - wifi_ssid: !secret wifi_ssid - wifi_password: !secret wifi_password - fallback_ap_password: !secret fallback_ap_password - #Add these if we are giving it a static ip, or remove them in the Wifi section - #static_ip_address: !secret esp-entmulti_static_ip - #static_ip_gateway: !secret esp-entmulti_gateway - #static_ip_subnet: !secret esp-entmulti_subnet - - mqtt_server: !secret mqtt_server - mqtt_username: !secret mqtt_username - mqtt_password: !secret mqtt_password - mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like - - #web_server_username: !secret web_server_username - #web_server_password: !secret web_server_password - - update_time: 30s #update time for for general temp sensors etc - - -############################################# -# ESPHome -# https://esphome.io/components/esphome.html -############################################# -esphome: - name: ${devicename} - friendly_name: ${friendly_name} - comment: ${description_comment} #appears on the esphome page in HA - min_version: 2024.6.0 - - -############################################# -# ESP Platform and Framework -# https://esphome.io/components/esp32.html -############################################# -esp32: - board: esp32dev - framework: - #type: arduino - type: esp-idf #Suggested Use ESP-IDF Framework, or Plug Out the UART Cable Might Cause ESP32 Hang. - version: recommended #recommended, latest or dev - -############################################# -# ESPHome external or custom components to use -# https://esphome.io/components/external_components.html -# https://github.com/ssieb/esphome_components/tree/master/components/serial -############################################# -#external_components: -# - source: -# type: git -# url: https://github.com/ssieb/custom_components #Thanks for @ssieb components. -# components: [ serial ] #text_sensor that reads lines for a uart. Also, a sensor that reads single binary values from the uart. - -############################################# -# ESPHome Logging Enable -# https://esphome.io/components/logger.html -############################################# -logger: - level: INFO #INFO Level suggested, or DEBUG for testing - #baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) - #esp8266_store_log_strings_in_flash: false - #tx_buffer_size: 64 - -############################################# -# Enable the Home Assistant API -# https://esphome.io/components/api.html -############################################# -api: - encryption: - key: ${api_key} -# on_client_connected: -# - esp32_ble_tracker.start_scan: -# continuous: true -# on_client_disconnected: -# - esp32_ble_tracker.stop_scan: - -############################################# -# Enable Over the Air Update Capability -# https://esphome.io/components/ota.html?highlight=ota -############################################# -ota: - - platform: esphome - password: ${ota_pass} - -############################################# -# Safe Mode -# Safe mode will detect boot loops -# https://esphome.io/components/safe_mode -############################################# -safe_mode: - -############################################# -# Wifi Settings -# https://esphome.io/components/wifi.html -# -# Power Save mode (can reduce wifi reliability) -# NONE (least power saving, Default for ESP8266) -# LIGHT (Default for ESP32) -# HIGH (most power saving) -############################################# -wifi: - ssid: ${wifi_ssid} - password: ${wifi_password} - #power_save_mode: LIGHT #https://esphome.io/components/wifi.html#wifi-power-save-mode - #manual_ip: #optional static IP address - #static_ip: ${static_ip_address} - #gateway: ${static_ip_gateway} - #subnet: ${static_ip_subnet} - ap: #Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: $devicename fallback AP - password: !secret fallback_ap_password - ap_timeout: 5min #Time until it brings up fallback AP. default is 1min - -############################################# -# Web Portal for display and monitoring -# Turning this off is probably a good idea to save resources. -# https://esphome.io/components/web_server.html -############################################# -#web_server: -# port: 80 -# auth: -# username: ${web_server_username} #probably a good idea to secure it -# password: ${web_server_password} - -############################################# -# MQTT Monitoring -# https://esphome.io/components/mqtt.html?highlight=mqtt -# MUST also have api enabled if you enable MQTT -############################################# -mqtt: - broker: ${mqtt_server} - topic_prefix: ${mqtt_topic}/${devicename} - username: ${mqtt_username} - password: ${mqtt_password} - -############################################# -# i2c bus -# https://esphome.io/components/i2c.html -# 10, 50, 100, 200, 800 are possible settings -# for frequency, 50kHz is default -############################################# -#i2c: -# sda: GPIO19 -# scl: GPIO21 -# scan: True #look for devices on boot up and report - #frequency: 100kHz - -############################################# -# UART Serial -# hardware on EPS32, but software, and can be glitchy on ESP8266 -# https://esphome.io/components/uart.html -############################################# -#uart: -# id: ld2410_uart -# rx_pin: GPIO16 #For ESP32, you can use any pin, Recommend Use UART_2, Don't use UART_0, It might Cause Boot Fail or System Hang -# tx_pin: GPIO17 #For ESP32, you can use any pin, Recommend Use UART_2, Don't use UART_0, It might Cause Boot Fail or System Hang -# baud_rate: 256000 # default for LD2410 is 25600, 8, 0, NONE -# data_bits: 8 -# stop_bits: 1 -# parity: NONE - -############################################# -# Bluetooth -# https://esphome.io/components/bluetooth_proxy.html -# https://esphome.io/components/esp32_ble_tracker.html -# Remember that this takes a LOT of processing. On the -# ESP32, enable the IDF framework, and disable the -# Web server component. Changing to the IDF framework -# needs to be via cable not OTA to change the -# partition setup. -############################################# -#bluetooth_proxy: -# active: true -# cache_services: true -# -#esp32_ble_tracker: -# scan_parameters: -# active: true -# continuous: false - -############################################# -# Global Variables for use in automations etc -# https://esphome.io/guides/automations.html?highlight=globals#global-variables -############################################# - - -############################################# -# General esp status LED -# https://esphome.io/components/status_led.html -############################################# -status_led: - pin: - number: GPIO2 #ESP32 Onboard LED - ignore_strapping_warning: True #https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins - inverted: false - -############################################# -# Interval Automations -# https://esphome.io/guides/automations.html -############################################# - -############################################# -# LD2410 Sensors -# https://esphome.io/components/sensor/ld2410.html -# https://www.hlktech.net/index.php?id=988 -############################################# -#ld2410: -# uart_id: ld2410_uart - -############################################# -# Number Sensors (custom component) -# refer https://github.com/ssieb/esphome_components/tree/master/components/serial -############################################# -#number: -# - platform: ld2410 -# timeout: -# name: Timeout -# light_threshold: -# name: Light Threshold -# max_move_distance_gate: -# name: Max Move Distance Gate -# max_still_distance_gate: -# name: Max Still Distance Gate -# g0: -# move_threshold: -# name: g0 move threshold -# still_threshold: -# name: g0 still threshold -# g1: -# move_threshold: -# name: g1 move threshold -# still_threshold: -# name: g1 still threshold -# g2: -# move_threshold: -# name: g2 move threshold -# still_threshold: -# name: g2 still threshold -# g3: -# move_threshold: -# name: g3 move threshold -# still_threshold: -# name: g3 still threshold -# g4: -# move_threshold: -# name: g4 move threshold -# still_threshold: -# name: g4 still threshold -# g5: -# move_threshold: -# name: g5 move threshold -# still_threshold: -# name: g5 still threshold -# g6: -# move_threshold: -# name: g6 move threshold -# still_threshold: -# name: g6 still threshold -# g7: -# move_threshold: -# name: g7 move threshold -# still_threshold: -# name: g7 still threshold -# g8: -# move_threshold: -# name: g8 move threshold -# still_threshold: -# name: g8 still threshold - -#The ld2410 select allows you to control your LD2410 Sensor. -#distance_resolution (Optional): Control the gates distance resolution. Can be 0.75m or 0.2m. Defaults to 0.75m. All options from Select. -#baud_rate (Optional): Control the serial port baud rate. Defaults to 256000. Once changed, all sensors will stop working until a fresh install with an updated UART Component configuration. All options from Select. -#light_function (Optional): If set, will affect the OUT pin value, based on light threshold. Can be off, low or above. Defaults to off. All options from Select. -#out_pin_level (Optional): Control OUT pin away value. Can be low or high. Defaults to low. All options from Select. -#ld2410_id (Optional, ID): Manually specify the ID for the LD2410 Sensor component if you are using multiple components. -#select: -# - platform: ld2410 -# distance_resolution: -# name: ${friendly_name} LD2140 Distance Resolution -# baud_rate: -# name: ${friendly_name} LD2140 Baud Rate -# light_function: -# name: ${friendly_name} LD2140 Light Function -# out_pin_level: -# name: ${friendly_name} LD2140 Out Pin Level - -############################################# -# General Sensors -# https://esphome.io/components/sensor/index.html -############################################# -sensor: -# - platform: bme280_i2c -# address: 0x76 -# update_interval: ${update_time} -# temperature: -# name: ${friendly_name} BME280 Temp -# accuracy_decimals: 1 -# oversampling: 2x -# pressure: -# name: ${friendly_name} BME280 Pressure -# oversampling: 2x -# humidity: -# name: ${friendly_name} BME280 Humidity -# accuracy_decimals: 1 -# oversampling: 2x - - ################################ - # WIFI SIGNAL - # Quality of Wifi in dBm - # https://esphome.io/components/sensor/wifi_signal.html - ################################ - - platform: wifi_signal - name: ${friendly_name} WiFi Signal - update_interval: 20s - #retain: true #retain useful if sleeping - - - platform: uptime - name: ${friendly_name} Uptime - update_interval: 20s - - -#The ld2410 sensor values -# - platform: ld2410 -# light: -# name: Light -# moving_distance: -# name : Moving Distance -# still_distance: -# name: Still Distance -# moving_energy: -# name: Move Energy -# still_energy: -# name: Still Energy -# detection_distance: -# name: Detection Distance -# g0: -# move_energy: -# name: g0 move energy -# still_energy: -# name: g0 still energy -# g1: -# move_energy: -# name: g1 move energy -# still_energy: -# name: g1 still energy -# g2: -# move_energy: -# name: g2 move energy -# still_energy: -# name: g2 still energy -# g3: -# move_energy: -# name: g3 move energy -# still_energy: -# name: g3 still energy -# g4: -# move_energy: -# name: g4 move energy -# still_energy: -# name: g4 still energy -# g5: -# move_energy: -# name: g5 move energy -# still_energy: -# name: g5 still energy -# g6: -# move_energy: -# name: g6 move energy -# still_energy: -# name: g6 still energy -# g7: -# move_energy: -# name: g7 move energy -# still_energy: -# name: g7 still energy -# g8: -# move_energy: -# name: g8 move energy -# still_energy: -# name: g8 still energy - -# The ld2410 switch allows you to control your LD2410 Sensor. -#Bluetooth switch is only useful of you have a B or C model -#switch: -# - platform: ld2410 -# engineering_mode: -# name: ${friendly_name} LD2140 Engineering Mode - #bluetooth: - #name: ${friendly_name} LD2140 Control Bluetooth - - -#The ld2410 button allows resetting -#button: -# - platform: ld2410 -# factory_reset: -# name: ${friendly_name} LD2140 Factory reset" -# restart: -# name: ${friendly_name} LD2140 Restart -## query_params: -# name: Query Parameters - -############################################# -# Text Sensors -# refer https://esphome.io/components/text_sensor/index.html -############################################# -#The ld2410 text sensor allows you to get information about your LD2410 Sensor. -#Bluetooth sensor is only useful of you have a B or C model -#text_sensor: -# - platform: ld2410 -# version: -# name: ${friendly_name} LD2140 Firmware Version - #mac_address: - #name: ${friendly_name} LD2140 BT MAC Address - - -############################################# -# Binary Sensors -# https://esphome.io/components/binary_sensor/index.html -############################################# -binary_sensor: - -# - platform: ld2410 -# has_target: -# name: ${friendly_name} Presence -# has_moving_target: -# name: ${friendly_name} Moving Target -# has_still_target: -# name: ${friendly_name} Still Target -# out_pin_presence_status: -# name: ${friendly_name} LD2140 Out Pin Presence Status - - #Standard PIR Sensor - - platform: gpio - pin: - number: GPIO13 - mode: - input: True - pullup: True - inverted: True - filters: - - delayed_on: 200ms - name: ${friendly_name} PIR Sensor - device_class: motion - - #RF Input from Vibration Sensor (Green Bin) - - platform: gpio - pin: - number: GPIO04 - mode: - input: true - pullup: true - inverted: True - filters: - - delayed_on: 20ms - name: ${friendly_name} Green Bin motion - device_class: vibration - - #RF Input from Vibration Sensor (Red Bin) - - platform: gpio - pin: - number: GPIO15 - mode: - input: true - pullup: true - inverted: True - filters: - - delayed_on: 20ms - name: ${friendly_name} Red Bin motion - device_class: vibration \ No newline at end of file diff --git a/packages/piano_practice.yaml b/packages/piano_practice.yaml.old similarity index 100% rename from packages/piano_practice.yaml rename to packages/piano_practice.yaml.old