From fda2e0cc242bfb45e1fa4f8b4b84e07f75691be6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Nov 2019 21:19:01 +0300 Subject: [PATCH] esphome --- appdaemon/settings/apps/climate/climate.py | 41 ++- appdaemon/settings/apps/climate/climate.yaml | 1 + appdaemon/settings/apps/climate/irgen.py | 326 ++++++++++++++++++ appdaemon/settings/apps/light/salt_lamp.py | 17 +- appdaemon/settings/apps/light/salt_lamp.yaml | 3 +- docker-compose.yml | 16 + esphome/settings/senseair.yaml | 38 ++ .../settings/configuration/remote/xiaomi.yaml | 3 +- hass/settings/secrets.yaml.sample | 10 +- hass/settings/ui-lovelace.yaml | 2 + nginx/settings/site-confs/default.conf.save | 30 -- 11 files changed, 445 insertions(+), 42 deletions(-) create mode 100644 appdaemon/settings/apps/climate/irgen.py create mode 100644 esphome/settings/senseair.yaml delete mode 100644 nginx/settings/site-confs/default.conf.save diff --git a/appdaemon/settings/apps/climate/climate.py b/appdaemon/settings/apps/climate/climate.py index 441b622..83ecb75 100644 --- a/appdaemon/settings/apps/climate/climate.py +++ b/appdaemon/settings/apps/climate/climate.py @@ -1,6 +1,7 @@ import appdaemon.plugins.hass.hassapi as hass import struct import binascii +import codecs class Climate(hass.Hass): ir_topic = "home/remote/rm2/code/set" @@ -38,7 +39,8 @@ class Climate(hass.Hass): return remote = Remote() code = remote.set_mode("NONE", "NONE", 18, "OFF") - self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8")) + self.log("command: {}".format(code)) + self.call_service("remote/send_command", entity_id=self.args['remote'], command = code) self.split_state = "off" if (self.timer != None): self.cancel_timer(self.timer) @@ -52,15 +54,17 @@ class Climate(hass.Hass): self.turn_on("switch.plug_158d00010dd98d") if (self.timer != None): self.cancel_timer(self.timer) - self.timer = self.run_in(self.run_in_split_mode, 10, mode=mode, temp=temp, state="ON") + self.timer = self.run_in(self.run_in_split_mode, 1, mode=mode, temp=temp, state="ON") def run_in_split_mode(self, args): code = "" self.log('mode: {}'.format(args['mode'])) remote = Remote() code = remote.set_mode(args['mode'], self.fan_speed, args['temp'], args['state']) - code = code.decode("utf-8") - self.call_service("mqtt/publish", topic = self.ir_topic, payload = code) + self.log("command: {}".format(code)) + # code = code.decode("utf-8") + self.call_service("remote/send_command", entity_id=self.args['remote'], command = code) + #self.call_service("mqtt/publish", topic = self.ir_topic, payload = code) def do_action(self): if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']): @@ -227,8 +231,33 @@ class Remote(object): 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 + + pronto = self.lirc2pronto(self.codes) + # print(pronto) + # self.codes = binascii.hexlify(self.lirc2broadlink(self.codes)) + # base64code = codecs.encode(codecs.decode(self.codes, 'hex'), 'base64').decode() + # print(base64code) + + return pronto + + def lirc2pronto(self, pulses, ff=0x6d): + frequency=1/(float(ff)*0.241246) + prontoByte = [] + prontoByte.append(int(0)) + prontoByte.append(ff) + prontoByte.append(int(0)) + prontoByte.append(int(33)) + for i in range(0,len(pulses)): + prontoByte.append(int(round(float(pulses[i])*frequency))) + prontoByte[3] = int(len(prontoByte)/2 - 2) + + prontoCode = [] + for i in range(0,len(prontoByte)): + beB = struct.pack('>I', prontoByte[i]) + c = str(binascii.hexlify(beB)[4:8])[2:6] + prontoCode.append(c) + codeStr = "pronto: {}".format(' '.join(prontoCode)) + return codeStr def lirc2broadlink(self, pulses): array = bytearray() diff --git a/appdaemon/settings/apps/climate/climate.yaml b/appdaemon/settings/apps/climate/climate.yaml index 74d5b3f..1d5059a 100644 --- a/appdaemon/settings/apps/climate/climate.yaml +++ b/appdaemon/settings/apps/climate/climate.yaml @@ -10,5 +10,6 @@ mainroom_climate: ha_panel: alarm_control_panel.ha_alarm door_window: group.ac_door_window color_entity: light.gateway_light_f0b429aa1463 + remote: remote.xiaomi_miio_192_168_1_52 global_dependencies: - globals \ No newline at end of file diff --git a/appdaemon/settings/apps/climate/irgen.py b/appdaemon/settings/apps/climate/irgen.py new file mode 100644 index 0000000..8f4d4a4 --- /dev/null +++ b/appdaemon/settings/apps/climate/irgen.py @@ -0,0 +1,326 @@ + +from base64 import b64encode, b64decode +import binascii +from itertools import islice + +gen_raw_nec_protocols_standard = ['nec1', + 'nec2', + 'necx1', + 'necx2'] +gen_raw_nec_protocols_suffixes = ['', + '-y1', + '-y2', + '-y3', + '-f16'] + +gen_raw_nec_protocols = list(x + y for x in gen_raw_nec_protocols_standard + for y in gen_raw_nec_protocols_suffixes) + +gen_raw_rc5_protocols = ['rc5'] + +gen_raw_rc6_protocols = ['rc6'] + +gen_raw_protocols = [*gen_raw_nec_protocols, *gen_raw_rc5_protocols, *gen_raw_rc6_protocols] + +def uX_to_bin(v, x): + if(v < 0): + v += (1 << x) + return bin(v)[2:].rjust(x, '0') + +def gen_raw_rc5(protocol, device, subdevice, function, toggle=0): + logical_bit = 889.0 + + def encode_bit(s): + if s == '1': + yield logical_bit * -1 + yield logical_bit * 1 + else: + yield logical_bit * 1 + yield logical_bit * -1 + + def encode_uX(x, l): + for s in uX_to_bin(x, l): + yield from encode_bit(s) + + yield from encode_bit('1') # start + if function < 64: + yield from encode_bit('1') # field (function 0-63) + else: + yield from encode_bit('0') # field (function 64-127) + yield from encode_bit(str(toggle)) # toggle + + # address + yield from encode_uX(device, 5) + + # command + yield from encode_uX(function % 64, 6) + + # trailing silence + yield logical_bit * -100 + +def gen_raw_rc6(protocol, device, subdevice, function, toggle=0, mode=0): + logical_bit = 444.0 + + def encode_bit(s): + if s == '1': + yield logical_bit * 1 + yield logical_bit * -1 + else: + yield logical_bit * -1 + yield logical_bit * 1 + + def encode_uX(x, l): + for s in uX_to_bin(x, l): + yield from encode_bit(s) + + #LS + yield logical_bit * 6 + yield logical_bit * -2 + + #SB + yield from encode_bit('1') + + #Mode + yield from encode_uX(mode, 3) + + #TB + if toggle: + yield logical_bit * 2 + yield logical_bit * -2 + else: + yield logical_bit * -2 + yield logical_bit * 2 + + #Control + yield from encode_uX(device, 8) + + #Information + yield from encode_uX(function, 8) + + #Signal Free + yield logical_bit * -6 + + +def gen_raw_nec(protocol, device, subdevice, function): + + logical_bit = 562.5 + + protocol_base, protocol_suffix = (protocol.split('-') + [None])[:2] + + def encode(value): + b = uX_to_bin(value, 8) + for s in reversed(b): + yield logical_bit # burst + if s == '1': + yield logical_bit * -3 # one is encoded by 3 length + else: + yield logical_bit * -1 # zero is encoded by 1 lengths + + if protocol_base in ('nec1', 'necx1'): + yield logical_bit * 16 # leading burst + else: + yield logical_bit * 8 # leading burst + + yield logical_bit * -8 # space before data + + yield from encode(device) + if subdevice >= 0: + yield from encode(subdevice) + else: + yield from encode(~device) + + yield from encode(function & 0xFF) + if protocol_suffix == 'y1': # Yamaha special version 1 + yield from encode(function ^ 0x7F) + elif protocol_suffix == 'y2': # Yamaha special version 2 + yield from encode(function ^ 0xFE) + elif protocol_suffix == 'y3': # Yamaha special version 3 + yield from encode(function ^ 0x7E) + elif protocol_suffix == 'f16': # 16 bit function + yield from encode((function >> 8) & 0xFF) + else: # Standard invert + yield from encode(function ^ 0xFF) + + yield logical_bit # Trailing burst + yield logical_bit * -3 # Trailing zero to separate + + +def gen_raw_general(protocol, device, subdevice, function, **kwargs): + if protocol.lower() in gen_raw_nec_protocols: + yield from gen_raw_nec(protocol.lower(), + int(device), + int(subdevice), + int(function)) + + if protocol.lower() in gen_raw_rc5_protocols: + yield from gen_raw_rc5(protocol.lower(), + int(device), + int(subdevice), + int(function)) + + if protocol.lower() in gen_raw_rc6_protocols: + yield from gen_raw_rc6(protocol.lower(), + int(device), + int(subdevice), + int(function)) + + +def gen_simplified_from_raw(x): + """ + Simplify raw string. + + Combine successive same sign value, drop zeros, drop leading negative + """ + value = 0 + for i in x: + if i == 0: + continue + elif value == 0: + if i > 0: + value = i + else: + pass # leading negative + elif (value > 0) == (i > 0): + value += i + else: + yield value + value = i + if value != 0: + yield value + +def gen_paired_from_raw(x): + """ + Create pairs of on, off + """ + + sign = 1 + for i in x: + if (i < 0) ^ (sign < 0): + yield 0.0 + yield i + else: + yield i + sign = -sign + if sign < 0: + yield 0.0 + +def gen_raw_from_broadlink(data): + v = iter(data) + code = next(v) + repeat = next(v) + + assert code == 0x26 # IR + + length = int.from_bytes(islice(v, 2), byteorder='little') + + def decode_one(x): + return round(x * 8192 / 269) + + def decode_iter(x): + sign = 1 + while True: + d = next(x) + if d == 0: + d = int.from_bytes(islice(x, 2), byteorder='big') + + yield sign * decode_one(d) + sign = sign * -1 + + yield from decode_iter(islice(v, length)) + + +def gen_raw_from_broadlink_base64(data): + yield from gen_raw_from_broadlink(b64decode(data)) + + +def gen_broadlink_from_raw(data, repeat=0): + yield from b'\x26' # IR + yield from repeat.to_bytes(1, byteorder='big') # Repeat + + def encode_one(x): + # v = abs(int(i / 32.84)) + v = abs(int(x * 269 / 8192)) + if v > 255: + yield from b'\x00' + yield from v.to_bytes(2, byteorder='big') + else: + yield from v.to_bytes(1, byteorder='big') + + def encode_list(x): + for i in gen_simplified_from_raw(x): + yield from encode_one(i) + + c = bytearray(encode_list(data)) + count = len(c) + yield from count.to_bytes(2, byteorder='little') + yield from c + yield from b'\x0d' + yield from b'\x05' + + # calculate total length for padding + count += 6 # header+len+trailer + count += 4 # rm.send_data() 4 byte header (not seen here) + yield from bytearray(16 - (count % 16)) + + +def gen_broadlink_base64_from_raw(data, repeat=0): + return b64encode(bytes(gen_broadlink_from_raw(data, repeat))) + + +def gen_raw_from_pronto(data): + clock = 0.241246 # Pronto clock base: 1000000 / (32768 * 506 / 4) + + v = iter(data) + zero = next(v) + assert zero == 0 + base = next(v) + freq = 1.0 / (base * clock) + + seq1_len = next(v) + seq2_len = next(v) + + for _ in range(seq1_len): + yield +round(next(v) / freq, 1) + yield -round(next(v) / freq, 1) + + for _ in range(seq2_len): + yield +round(next(v) / freq, 1) + yield -round(next(v) / freq, 1) + + +def gen_pronto_from_raw_int(seq1, seq2, base=None, freq=None): + clock = 0.241246 # Pronto clock base: 1000000 / (32768 * 506 / 4) + + if freq is None: + if base is None: + freq = 0.040 + else: + freq = 1.0 / (base * clock) + + if base is None: + base = int(1 / (freq * clock)) + + yield 0 + yield base + + def fixup(x): + return list(gen_paired_from_raw(gen_simplified_from_raw(x))) + #return list(gen_paired_from_raw((x))) + + simple1 = fixup(seq1) + simple2 = fixup(seq2) + + yield int(len(simple1)/2) # sequence 1 + yield int(len(simple2)/2) # sequence 2 + + for x in simple1: + yield int(abs(x) * freq) + + for x in simple2: + yield int(abs(x) * freq) + + +def gen_pronto_from_raw(seq1, seq2, base=None, freq=None): + data = gen_pronto_from_raw_int(seq1, seq2, base, freq) + for value in data: + yield "{0:0{1}x}".format(value,4) \ No newline at end of file diff --git a/appdaemon/settings/apps/light/salt_lamp.py b/appdaemon/settings/apps/light/salt_lamp.py index e5fcd6b..e88938a 100644 --- a/appdaemon/settings/apps/light/salt_lamp.py +++ b/appdaemon/settings/apps/light/salt_lamp.py @@ -1,12 +1,12 @@ import appdaemon.plugins.hass.hassapi as hass class Noolite(hass.Hass): - brightness = 25 + brightness = 40 force = False def initialize(self): self.run_at_sunrise(self.sunrise_cb) - self.run_at_sunset(self.sunset_cb) + # self.run_at_sunset(self.sunset_cb) self.listen_state(self.light_change, "group.mainroom_light") self.listen_event(self.away_mode, "away_mode") @@ -15,7 +15,7 @@ class Noolite(hass.Hass): def light_change(self, entity, attribute, old, new, kwargs): sun_down = self.sun_down() - if new == "off" and (sun_down or self.force): + if new == "off" and (sun_down or self.force) and self.somebody_home(): self.noolite_on(self.brightness) else: self.noolite_off() @@ -29,7 +29,18 @@ class Noolite(hass.Hass): self.turn_on(self.args['entity_id'], brightness=self.brightness) def sunset_cb(self, kwargs): + if not self.somebody_home(): + return self.noolite_on(self.brightness) + def somebody_home(self): + if 'ha_panel' not in self.args: + return True + ha_panel_state = self.get_state(self.args['ha_panel']) + if ha_panel_state == 'armed_away': + return False + return True + + def sunrise_cb(self, kwargs): self.noolite_off() diff --git a/appdaemon/settings/apps/light/salt_lamp.yaml b/appdaemon/settings/apps/light/salt_lamp.yaml index 3dfd30b..973715c 100644 --- a/appdaemon/settings/apps/light/salt_lamp.yaml +++ b/appdaemon/settings/apps/light/salt_lamp.yaml @@ -1,4 +1,5 @@ salt_lamp: module: salt_lamp class: Noolite - entity_id: light.salt_lamp \ No newline at end of file + entity_id: light.salt_lamp + ha_panel: alarm_control_panel.ha_alarm diff --git a/docker-compose.yml b/docker-compose.yml index 3c9556a..cdda877 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -187,6 +187,22 @@ services: options: max-size: 50m + esphome: + image: esphome/esphome + container_name: esphome + # ports: + # - "6052:6052" + volumes: + - ./esphome/settings:/config + - ./hass/settings/secrets.yaml:/config/secrets.yaml:ro + network_mode: "host" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # networks: # traefik: # external: diff --git a/esphome/settings/senseair.yaml b/esphome/settings/senseair.yaml new file mode 100644 index 0000000..8250d8d --- /dev/null +++ b/esphome/settings/senseair.yaml @@ -0,0 +1,38 @@ +logger: + baud_rate: 0 + +esphome: + name: senseair + platform: ESP8266 + board: d1_mini + +wifi: + ssid: !secret wifi_24_name + password: !secret wifi_24_pass + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Senseair Fallback Hotspot" + password: "FVFA2EoK8phQ" + +captive_portal: + +# Enable Home Assistant API +api: + password: !secret esphome_pass + +ota: + password: !secret esphome_pass + + +uart: + rx_pin: D7 + tx_pin: D8 + baud_rate: 9600 + +sensor: + - platform: senseair + co2: + name: "SenseAir CO2 Value" + update_interval: 60s + diff --git a/hass/settings/configuration/remote/xiaomi.yaml b/hass/settings/configuration/remote/xiaomi.yaml index 2ca9338..3e5a42a 100644 --- a/hass/settings/configuration/remote/xiaomi.yaml +++ b/hass/settings/configuration/remote/xiaomi.yaml @@ -1,3 +1,4 @@ - platform: xiaomi_miio + name: xiaomi_remote host: !secret xiaomi_remote_host - token: !secret xiaomi_remote_token \ No newline at end of file + token: !secret xiaomi_remote_token diff --git a/hass/settings/secrets.yaml.sample b/hass/settings/secrets.yaml.sample index 34a5a66..ae26d11 100644 --- a/hass/settings/secrets.yaml.sample +++ b/hass/settings/secrets.yaml.sample @@ -17,6 +17,13 @@ yandex_tts_key: xxxxxxxxx ################################### mysql_url: xxxxxxxxx +################################### +# ESPHOME SETTINGS # +################################### +wifi_24_name: xxxxxxxxx +wifi_24_pass: xxxxxxxxx +esphome_pass: xxxxxxxxx + ################################### # APPLETV SETTINGS # ################################### @@ -141,4 +148,5 @@ mosru_paycode: xxxxxxxxx amazon_mail: xxxxxxxxx amazon_pass: xxxxxxxxx alexa_client_id: xxxxxxxxx -alexa_client_secret: xxxxxxxxx \ No newline at end of file +alexa_client_secret: xxxxxxxxx + diff --git a/hass/settings/ui-lovelace.yaml b/hass/settings/ui-lovelace.yaml index 89b8128..3bc35d3 100644 --- a/hass/settings/ui-lovelace.yaml +++ b/hass/settings/ui-lovelace.yaml @@ -23,6 +23,7 @@ views: - sensor.temperature_158d0001149b3c - sensor.humidity_158d0001149b3c - sensor.xiaomi_airpurifier_air_quality_pm25 + - sensor.senseair_co2_value - light.yeelight_color1_34ce008fe328 - light.yeelight_color1_286c071065c7 - light.yeelight_mono1_34ce0087ff33 @@ -321,6 +322,7 @@ views: refresh_interval: 60 entities: - sensor.xiaomi_airpurifier_air_quality_pm25 + - sensor.senseair_co2_value - title: "Настройки" icon: mdi:settings cards: diff --git a/nginx/settings/site-confs/default.conf.save b/nginx/settings/site-confs/default.conf.save deleted file mode 100644 index ba2f7c4..0000000 --- a/nginx/settings/site-confs/default.conf.save +++ /dev/null @@ -1,30 +0,0 @@ -server { - listen 80 default_server; - #listen 443 default ssl; - - server_name _; - - #ssl_certificate /config/keys/cert.crt; - #ssl_certificate_key /config/keys/cert.key; - - client_max_body_size 0; - - #letsencrypt challenge response -CdVsNcvBapg7d3o0ZaixdMbMt5FqG7T7Z19CJsotuGI.hTi2mqIkE64NQibEfAxiC4ih0s6_pk4Ne2hCgHXFntI location ~/.well-known/acme-challenge/(.*)$ { - return 200 '$1'; - } - - location / { - real_ip_header X-Forwarded-For; - set_real_ip_from 0.0.0.0/0; - - proxy_pass http://hass; - proxy_set_header Host $host; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Host $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } -}