├── src ├── webh │ └── .gitkeep ├── version.h ├── websrc │ ├── 3rdparty │ │ ├── fonts │ │ │ └── glyphicons-halflings-regular.woff │ │ └── css │ │ │ ├── sidebar.css │ │ │ └── footable.bootstrap-3.1.6.min.css │ └── index.html ├── Ntp.h ├── ems_utils.h ├── ds18.h ├── pid.h ├── irtuart.h ├── Timezone.h ├── TimeLib.h ├── pid.cpp ├── my_config.h ├── Ntp.cpp ├── TimeLib.cpp ├── test_data.h ├── ds18.cpp ├── custom.js ├── Timezone.cpp ├── ems_utils.cpp ├── TelnetSpy.h └── custom.htm ├── _config.yml ├── doc ├── home assistant │ ├── ha.png │ ├── ha_notify.jpg │ ├── script.yaml │ ├── binary_sensor.yaml │ ├── customize.yaml │ ├── notify.yaml │ ├── switch.yaml │ ├── automation.yaml │ ├── climate.yaml │ ├── ui-lovelace.yaml │ └── sensor.yaml ├── web │ ├── ems_dashboard.PNG │ └── system_status.PNG ├── schematics │ ├── circuit.png │ ├── breadboard.png │ ├── wemos_kees.png │ ├── IRT-V09_schema.png │ ├── breadboard_example.png │ └── Schematic_EMS-ESP-supercap.png ├── telnet │ ├── telnet_menu.jpg │ ├── telnet_stats.PNG │ └── telnet_verbose.PNG ├── ems gateway │ ├── ems-kit-2.jpg │ ├── on-boiler.jpg │ ├── ems-board-white.jpg │ └── logo-proddy-fw.jpg └── ems references │ └── wiki_ ems_ telegrams.pdf ├── .gitpod.Dockerfile ├── .gitpod.yml ├── tools ├── webfilesbuilder │ ├── gulp.meta.js │ ├── package.json │ ├── gulp.js │ └── gulpfile.js └── wsemulator │ ├── run.sh │ ├── package.json │ ├── run.ps1 │ ├── package-lock.json │ └── wserver.js ├── .github ├── contribute.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── questions---troubleshooting.md │ └── bug_report.md └── stale.yml ├── .gitignore ├── scripts ├── clean_fw.py ├── analyze_stackdmp.py ├── main_script.py ├── stackdmp.txt ├── pre_script.py ├── build.sh ├── decoder.py └── decoder_linux.py ├── .clang-format ├── .travis.yml ├── platformio.ini ├── README.md └── LICENSE /src/webh/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /src/version.h: -------------------------------------------------------------------------------- 1 | #define APP_VERSION "1.9.10.20110420" 2 | -------------------------------------------------------------------------------- /doc/home assistant/ha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/home assistant/ha.png -------------------------------------------------------------------------------- /doc/web/ems_dashboard.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/web/ems_dashboard.PNG -------------------------------------------------------------------------------- /doc/web/system_status.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/web/system_status.PNG -------------------------------------------------------------------------------- /doc/schematics/circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/schematics/circuit.png -------------------------------------------------------------------------------- /doc/telnet/telnet_menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/telnet/telnet_menu.jpg -------------------------------------------------------------------------------- /doc/telnet/telnet_stats.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/telnet/telnet_stats.PNG -------------------------------------------------------------------------------- /doc/ems gateway/ems-kit-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/ems gateway/ems-kit-2.jpg -------------------------------------------------------------------------------- /doc/ems gateway/on-boiler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/ems gateway/on-boiler.jpg -------------------------------------------------------------------------------- /doc/schematics/breadboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/schematics/breadboard.png -------------------------------------------------------------------------------- /doc/schematics/wemos_kees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/schematics/wemos_kees.png -------------------------------------------------------------------------------- /doc/telnet/telnet_verbose.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/telnet/telnet_verbose.PNG -------------------------------------------------------------------------------- /doc/home assistant/ha_notify.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/home assistant/ha_notify.jpg -------------------------------------------------------------------------------- /doc/ems gateway/ems-board-white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/ems gateway/ems-board-white.jpg -------------------------------------------------------------------------------- /doc/ems gateway/logo-proddy-fw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/ems gateway/logo-proddy-fw.jpg -------------------------------------------------------------------------------- /doc/schematics/IRT-V09_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/schematics/IRT-V09_schema.png -------------------------------------------------------------------------------- /doc/schematics/breadboard_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/schematics/breadboard_example.png -------------------------------------------------------------------------------- /doc/ems references/wiki_ ems_ telegrams.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/ems references/wiki_ ems_ telegrams.pdf -------------------------------------------------------------------------------- /doc/schematics/Schematic_EMS-ESP-supercap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/doc/schematics/Schematic_EMS-ESP-supercap.png -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | USER gitpod 4 | 5 | RUN pip3 install -U platformio && brew install uncrustify 6 | -------------------------------------------------------------------------------- /src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Victor-Mo/IRT-ESP/HEAD/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | tasks: 5 | - init: cd ./tools/webfilesbuilder && npm ci && node_modules/gulp/bin/gulp.js && cd ../.. 6 | command: pio run 7 | 8 | -------------------------------------------------------------------------------- /doc/home assistant/script.yaml: -------------------------------------------------------------------------------- 1 | # ems-esp 2 | shower_coldshot: 3 | sequence: 4 | - service: mqtt.publish 5 | data_template: 6 | topic: 'home/ems-esp/generic_cmd' 7 | payload: '{cmd:"coldshot"}' 8 | -------------------------------------------------------------------------------- /tools/webfilesbuilder/gulp.meta.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function () { 4 | return { 5 | packages: ['gulp-concat', 'gulp-htmlmin', 'gulp-flatmap', 'gulp-gzip', 'gulp-uglify', 'fs', 'path', 'pump'], 6 | deployFiles: ['gulpfile.js'], 7 | take: 'last-line' 8 | }; 9 | }; -------------------------------------------------------------------------------- /tools/wsemulator/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node $PWD/../webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd $PWD/../webfilesbuilder 4 | 5 | open -na Google\ Chrome --args --disable-web-security --remote-debugging-port=9222 --user-data-dir="/tmp/chrome_dev" $PWD/../../src/websrc/temp/index.html 6 | 7 | node wserver.js 8 | -------------------------------------------------------------------------------- /doc/home assistant/binary_sensor.yaml: -------------------------------------------------------------------------------- 1 | - platform: mqtt 2 | name: 'Tap Water' 3 | state_topic: 'home/ems-esp/tapwater_active' 4 | payload_on: "1" 5 | payload_off: "0" 6 | 7 | - platform: mqtt 8 | name: 'Heating' 9 | state_topic: 'home/ems-esp/heating_active' 10 | payload_on: "1" 11 | payload_off: "0" 12 | -------------------------------------------------------------------------------- /.github/contribute.md: -------------------------------------------------------------------------------- 1 | Do you want to do a pull request? 2 | 3 | Excellent! Thanks for contributing! 4 | 5 | Please do keep in mind these basic rules: 6 | 7 | ## Pull request ## 8 | * Do the pull request against the **`dev` branch** 9 | * **Only touch relevant files** (beware if your editor has auto-formatting feature enabled) 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # platformio 5 | .pio 6 | lib/readme.txt 7 | 8 | # web stuff compiled 9 | src/websrc/temp 10 | src/webh/*.gz.h 11 | 12 | # NPM directories 13 | node_modules 14 | 15 | # OS specific 16 | .DS_Store 17 | 18 | # project specfic 19 | scripts/stackdmp.txt 20 | firmware 21 | 22 | # firmware 23 | *.bin 24 | -------------------------------------------------------------------------------- /doc/home assistant/customize.yaml: -------------------------------------------------------------------------------- 1 | sensor.boiler_boottime: 2 | friendly_name: Controller last restart 3 | icon: mdi:clock-start 4 | 5 | sensor.showertime_time: 6 | friendly_name: 'Last shower at' 7 | icon: mdi:timelapse 8 | 9 | sensor.boiler_updated: 10 | friendly_name: 'Data last received' 11 | icon: mdi:clock-start 12 | 13 | -------------------------------------------------------------------------------- /tools/wsemulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsemulator", 3 | "version": "0.0.1", 4 | "description": "Emulate websocket communication ", 5 | "main": "wserver.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "proddy", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "ws": "^4.1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /doc/home assistant/notify.yaml: -------------------------------------------------------------------------------- 1 | - name: pushover 2 | platform: pushover 3 | api_key: !secret pushover_api_key 4 | user_key: !secret pushover_user_key 5 | 6 | - name: general_notify 7 | platform: group 8 | services: 9 | - service: ios_pauls_iphone 10 | - service: pushover 11 | 12 | - name: admin_notify 13 | platform: group 14 | services: 15 | - service: ios_pauls_iphone 16 | - service: pushover 17 | -------------------------------------------------------------------------------- /scripts/clean_fw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from subprocess import call 3 | import os 4 | Import("env") 5 | 6 | def clean(source, target, env): 7 | print("\n** Starting clean...") 8 | call(["pio", "run", "-t", "erase"]) 9 | call(["esptool.py", "-p COM6", "write_flash 0x00000", os.getcwd()+"../firmware/*.bin"]) 10 | print("\n** Finished clean.") 11 | 12 | # built in targets: (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota) 13 | env.AddPreAction("buildprog", clean) 14 | 15 | -------------------------------------------------------------------------------- /tools/webfilesbuilder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webfilesbuilder", 3 | "version": "1.0.0", 4 | "description": "Combine all js and css files into one and gzip them", 5 | "main": "gulpfile.js", 6 | "author": "proddy", 7 | "private": true, 8 | "license": "GPL-3.0", 9 | "devDependencies": { 10 | "gulp": "^4.0.0", 11 | "gulp-htmlmin": "^4.0.0", 12 | "gulp-gzip": "^1.4.2", 13 | "gulp-concat": "^2.6.1", 14 | "gulp-flatmap": "^1.0.2", 15 | "gulp-uglify": "^3.0.2", 16 | "pump": "^3.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tools/webfilesbuilder/gulp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-path-concat */ 2 | 3 | 'use strict'; 4 | 5 | var path = require('path'); 6 | process.env.NODE_PATH = (process.env.NODE_PATH || '').split(path.delimiter) 7 | .filter((p) => p).concat(__dirname + '/node_modules').join(path.delimiter); 8 | require('module')._initPaths(); // eslint-disable-line no-underscore-dangle 9 | 10 | require('gulp'); 11 | require('gulp-concat'); 12 | require('gulp/bin/gulp.js'); 13 | require('fs'); 14 | require('gulp-gzip'); 15 | require('gulp-flatmap'); 16 | require('path'); 17 | require('gulp-htmlmin'); 18 | require('gulp-uglify'); 19 | require('pump'); -------------------------------------------------------------------------------- /doc/home assistant/switch.yaml: -------------------------------------------------------------------------------- 1 | # EMS-ESP 2 | - platform: mqtt 3 | name: "Shower Timer" 4 | state_topic: "home/ems-esp/shower_data" 5 | value_template: "{{ value_json.timer }}" 6 | command_topic: "home/ems-esp/shower_data" 7 | payload_on: '{"timer":"1"}' 8 | payload_off: '{"timer":"0"}' 9 | state_on: "1" 10 | state_off: "0" 11 | 12 | - platform: mqtt 13 | name: "Long Shower Alert" 14 | state_topic: "home/ems-esp/shower_data" 15 | value_template: "{{ value_json.alert }}" 16 | command_topic: "home/ems-esp/shower_data" 17 | payload_on: '{"alert":"1"}' 18 | payload_off: '{"alert":"0"}' 19 | state_on: "1" 20 | state_off: "0" 21 | -------------------------------------------------------------------------------- /tools/wsemulator/run.ps1: -------------------------------------------------------------------------------- 1 | $ScriptDir = Split-Path $script:MyInvocation.MyCommand.Path 2 | 3 | # build web 4 | $webfilesbuilder = $ScriptDir + "\..\webfilesbuilder" 5 | node $webfilesbuilder\node_modules\gulp\bin\gulp.js --cwd $webfilesbuilder 6 | 7 | # run chrome 8 | $pathToChrome = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' 9 | $tempFolder = '--user-data-dir=c:\temp' 10 | $startmode = '--remote-debugging-port=9222 --disable-web-security --disable-gpu' 11 | $startPage = $ScriptDir + "\..\..\src\websrc\temp\index.html" 12 | Start-Process -FilePath $pathToChrome -ArgumentList $tempFolder, $startmode, $startPage 13 | 14 | # run ws fake server 15 | node wserver.js 16 | -------------------------------------------------------------------------------- /scripts/analyze_stackdmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from subprocess import call 3 | import os 4 | 5 | # example stackdmp.txt would contain text like below copied & pasted from a 'crash dump' command: 6 | 7 | # >>>stack>>> 8 | # 3fffff20: 3fff32f0 00000003 3fff3028 402101b2 9 | # 3fffff30: 3fffdad0 3fff3280 0000000d 402148aa 10 | # 3fffff40: 3fffdad0 3fff3280 3fff326c 3fff32f0 11 | # 3fffff50: 0000000d 3fff326c 3fff3028 402103bd 12 | # 3fffff60: 0000000d 3fff34cc 40211de4 3fff34cc 13 | # 3fffff70: 3fff3028 3fff14c4 3fff301c 3fff34cc 14 | # 3fffff80: 3fffdad0 3fff14c4 3fff3028 40210493 15 | # 3fffff90: 3fffdad0 00000000 3fff14c4 4020a738 16 | # 3fffffa0: 3fffdad0 00000000 3fff349c 40211e90 17 | # 3fffffb0: feefeffe feefeffe 3ffe8558 40100b01 18 | # << 11 | #include 12 | 13 | #include "TimeLib.h" // customized version of the Time library 14 | #include "Timezone.h" 15 | 16 | #define NTP_PACKET_SIZE 48 // NTP time is in the first 48 bytes of the message 17 | #define NTP_INTERVAL_DEFAULT 720 // every 12 hours 18 | #define NTP_TIMEZONE_DEFAULT 2 // CE 19 | #define NTP_TIMEZONE_MAX 11 20 | 21 | class NtpClient { 22 | public: 23 | void ICACHE_FLASH_ATTR Ntp(const char * server, time_t syncMins, uint8_t tz_index); 24 | ICACHE_FLASH_ATTR virtual ~NtpClient(); 25 | 26 | static char * TimeServerName; 27 | static IPAddress timeServer; 28 | static time_t syncInterval; 29 | static Timezone * tz; 30 | static TimeChangeRule * tcr; 31 | 32 | static AsyncUDP udpListener; 33 | 34 | static byte NTPpacket[NTP_PACKET_SIZE]; 35 | 36 | static ICACHE_FLASH_ATTR time_t getNtpTime(); 37 | }; 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /tools/wsemulator/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsemulator", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 10 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 11 | }, 12 | "safe-buffer": { 13 | "version": "5.1.2", 14 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 15 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 16 | }, 17 | "ws": { 18 | "version": "4.1.0", 19 | "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", 20 | "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", 21 | "requires": { 22 | "async-limiter": "~1.0.0", 23 | "safe-buffer": "~5.1.0" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /doc/home assistant/automation.yaml: -------------------------------------------------------------------------------- 1 | - id: ems-esp_offline 2 | alias: See if ems-esp goes offline 3 | initial_state: true 4 | trigger: 5 | platform: state 6 | entity_id: sensor.ems_esp_status 7 | to: offline 8 | action: 9 | - service: notify.admin_notify 10 | data_template: 11 | title: EMS-ESP 12 | message: 'gone offline' 13 | 14 | - id: emsesp_restart 15 | alias: See if ems-esp restarts 16 | initial_state: true 17 | trigger: 18 | platform: mqtt 19 | topic: home/ems-esp/start 20 | payload: start 21 | action: 22 | - service: notify.admin_notify 23 | data_template: 24 | title: ems-esp has booted 25 | message: EMS-ESP 26 | - service: mqtt.publish 27 | data_template: 28 | topic: home/ems-esp/start 29 | payload: '{{ now().strftime("%H:%M:%S %-d/%b/%Y") }}' 30 | 31 | - id: boiler_shower 32 | alias: Alert shower time 33 | initial_state: true 34 | trigger: 35 | platform: state 36 | entity_id: sensor.last_shower_duration 37 | action: 38 | - service: notify.admin_notify 39 | data_template: 40 | title: Shower finished at {{states.sensor.time.state}} 41 | message: "{{ states.sensor.last_shower_duration.state }}" 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Before creating a new feature request please check that you have searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* 11 | 12 | *Completing this template will help developers and contributors evaluating the feature. If the information provided is not enough the issue will likely be closed.* 13 | 14 | *You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the request then you can delete them.* 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | *A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]* 18 | 19 | **Describe the solution you'd like** 20 | *A clear and concise description of what you want to happen.* 21 | 22 | **Describe alternatives you've considered** 23 | *A clear and concise description of any alternative solutions or features you've considered.* 24 | 25 | **Additional context** 26 | *Add any other context or screenshots about the feature request here.* 27 | -------------------------------------------------------------------------------- /doc/home assistant/climate.yaml: -------------------------------------------------------------------------------- 1 | - platform: mqtt 2 | name: Thermostat 3 | modes: 4 | - "auto" 5 | - "heat" 6 | - "off" 7 | mode_command_topic: "home/ems-esp/thermostat_cmd_mode1" 8 | temperature_command_topic: "home/ems-esp/thermostat_cmd_temp1" 9 | 10 | mode_state_topic: "home/ems-esp/thermostat_data" 11 | current_temperature_topic: "home/ems-esp/thermostat_data" 12 | temperature_state_topic: "home/ems-esp/thermostat_data" 13 | 14 | mode_state_template: "{{ value_json.hc1.mode }}" 15 | current_temperature_template: "{{ value_json.hc1.currtemp }}" 16 | temperature_state_template: "{{ value_json.hc1.seltemp }}" 17 | 18 | temp_step: 0.5 19 | 20 | - platform: mqtt 21 | name: boiler 22 | modes: 23 | - "auto" 24 | - "off" 25 | min_temp: 40 26 | max_temp: 60 27 | temp_step: 1 28 | 29 | current_temperature_topic: "home/ems-esp/boiler_data" 30 | temperature_state_topic: "home/ems-esp/boiler_data" 31 | mode_state_topic: "home/ems-esp/boiler_data" 32 | 33 | current_temperature_template: "{{ value_json.wWCurTmp }}" 34 | temperature_state_template: "{{ value_json.wWSelTemp }}" 35 | mode_state_template: "{% if value_json.wWActivated == 'off' %} off {% else %} auto {% endif %}" 36 | 37 | temperature_command_topic: "home/ems-esp/boiler_cmd_wwtemp" 38 | mode_command_topic: "home/ems-esp/boiler_cmd_wwactivated" 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions---troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Questions & Troubleshooting 3 | about: Anything not a bug or feature request 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Before creating a new issue please check that you have:* 11 | 12 | * *searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* 13 | * *searched the [wiki help pages](https://github.com/proddy/EMS-ESP/wiki/Troubleshooting)* 14 | 15 | 16 | *Completing this template will help developers and contributors help you. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.* 17 | 18 | *You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.* 19 | 20 | **Question** 21 | *A clear and concise description of what the problem/doubt is.* 22 | 23 | **Screenshots** 24 | *If applicable, add screenshots to help explain your problem.* 25 | 26 | **Device information** 27 | *Copy-paste here the information as it is outputted by the device. You can get this information by from the telnet session with the logging set to Verbose mode.* 28 | 29 | **Additional context** 30 | *Add any other context about the problem here.* 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Before creating a new issue please check that you have:* 11 | 12 | * *searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* 13 | * *searched the [wiki help pages](https://github.com/proddy/EMS-ESP/wiki/Troubleshooting)* 14 | 15 | *Completing this template will help developers and contributors to address the issue. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.* 16 | 17 | *You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.* 18 | 19 | **Bug description** 20 | *A clear and concise description of what the bug is.* 21 | 22 | **Steps to reproduce** 23 | *Steps to reproduce the behavior.* 24 | 25 | **Expected behavior** 26 | *A clear and concise description of what you expected to happen.* 27 | 28 | **Screenshots** 29 | *If applicable, add screenshots to help explain your problem.* 30 | 31 | **Device information** 32 | *Copy-paste here the information as it is outputted by the device. You can get this information by from the telnet session using the `system` command and `info`.* 33 | 34 | **Additional context** 35 | *Add any other context about the problem here.* 36 | -------------------------------------------------------------------------------- /src/ems_utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Generic utils 3 | * 4 | * Paul Derbyshire - https://github.com/proddy/EMS-ESP 5 | * 6 | */ 7 | 8 | 9 | #pragma once 10 | 11 | #include "MyESP.h" 12 | #include "irt.h" 13 | 14 | #ifdef EMS_UTILS_INCLUDE_DEBUG_MACRO 15 | #define myDebug(...) myESP.myDebug(__VA_ARGS__) 16 | #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) 17 | #endif 18 | 19 | char * _float_to_char(char * a, float f, uint8_t precision = 2); 20 | char * _bool_to_char(char * s, uint8_t value); 21 | char * _short_to_char(char * s, int16_t value, uint8_t decimals = 1); 22 | char * _ushort_to_char(char * s, uint16_t value, uint8_t decimals = 1); 23 | void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals = 1); 24 | void _renderUShortValue(const char * prefix, const char * postfix, uint16_t value, uint8_t decimals = 1); 25 | char * _int_to_char(char * s, uint8_t value, uint8_t div = 1); 26 | void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 1); 27 | void _renderLongValue(const char * prefix, const char * postfix, uint32_t value); 28 | void _renderBoolValue(const char * prefix, uint8_t value); 29 | char * _hextoa(uint8_t value, char * buffer); 30 | char * _smallitoa(uint8_t value, char * buffer); 31 | char * _smallitoa3(uint16_t value, char * buffer); 32 | //uint8_t _readIntNumber(); 33 | //float _readFloatNumber(); 34 | //uint16_t _readHexNumber(); 35 | //char * _readWord(); 36 | size_t _parse_cmd_line(char *cmd_line, char **argv, size_t max_argv); 37 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an Issue or Pull Request becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale Issue or Pull Request is closed. 5 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 6 | daysUntilClose: 7 7 | 8 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 9 | exemptLabels: 10 | - enhancement 11 | - bug 12 | - staged for release 13 | 14 | # Set to true to ignore issues in a project (defaults to false) 15 | exemptProjects: false 16 | 17 | # Set to true to ignore issues in a milestone (defaults to false) 18 | exemptMilestones: false 19 | 20 | # Label to use when marking as stale 21 | staleLabel: stale 22 | 23 | # Comment to post when marking as stale. Set to `false` to disable 24 | markComment: > 25 | This issue has been automatically marked as stale because it has not had 26 | recent activity. It will be closed in 7 days if no further activity occurs. 27 | Thank you for your contributions. 28 | 29 | # Comment to post when removing the stale label. 30 | # unmarkComment: > 31 | # Your comment here. 32 | 33 | # Comment to post when closing a stale Issue or Pull Request. 34 | closeComment: > 35 | This issue will be auto-closed because there hasn't been any activity for two months. Feel free to open a new one if you still experience this problem. 36 | 37 | # Limit the number of actions per hour, from 1-30. Default is 30 38 | limitPerRun: 30 39 | 40 | # Limit to only `issues` or `pulls` 41 | only: issues 42 | -------------------------------------------------------------------------------- /src/ds18.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Dallas support for external temperature sensors 3 | * Copyright (C) 2017-2018 by Xose Pérez 4 | * 5 | * Paul Derbyshire - https://github.com/proddy/EMS-ESP 6 | * 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | #define DS18_CHIP_DS18S20 0x10 15 | #define DS18_CHIP_DS1822 0x22 16 | #define DS18_CHIP_DS18B20 0x28 17 | #define DS18_CHIP_DS1825 0x3B 18 | 19 | #define DS18_DATA_SIZE 9 20 | #define DS18_DISCONNECTED -127 21 | #define DS18_CRC_ERROR -126 22 | 23 | #define GPIO_NONE 0x99 24 | #define DS18_READ_INTERVAL 2000 // Force sensor read & cache every 2 seconds 25 | 26 | #define DS18_CMD_START_CONVERSION 0x44 27 | #define DS18_CMD_READ_SCRATCHPAD 0xBE 28 | 29 | typedef struct { 30 | uint8_t address[8]; 31 | uint8_t data[DS18_DATA_SIZE]; 32 | } ds_device_t; 33 | 34 | class DS18 { 35 | public: 36 | DS18(); 37 | ~DS18(); 38 | 39 | void setup(uint8_t gpio, bool parasite); 40 | uint8_t scan(); 41 | void loop(); 42 | char * getDeviceType(char * s, unsigned char index); 43 | char * getDeviceID(char * buffer, unsigned char index); 44 | float getValue(unsigned char index); 45 | int16_t getRawValue(unsigned char index); // raw values, needs / 16 46 | 47 | protected: 48 | bool validateID(unsigned char id); 49 | unsigned char chip(unsigned char index); 50 | uint8_t loadDevices(); 51 | 52 | OneWire * _wire; 53 | uint8_t _count; // # devices 54 | uint8_t _gpio; // the sensor pin 55 | uint8_t _parasite; // parasite mode 56 | }; 57 | -------------------------------------------------------------------------------- /scripts/stackdmp.txt: -------------------------------------------------------------------------------- 1 | >>>stack>>> 2 | 3ffffdc0: 3fff472c 00000001 3ffec811 40224020 3 | 3ffffdd0: 3fff472c 00000019 3fff8acd 0000001b 4 | 3ffffde0: 3fff4a5c 0000072a 0000072a 402113c8 5 | 3ffffdf0: 3ffec4b5 3fff44d8 3fff8ab4 402117f0 6 | 3ffffe00: 3fff8ab4 3fff44d8 3fff472c 4022d20a 7 | 3ffffe10: 00000000 40209737 3fff472c 4022d1f8 8 | 3ffffe20: 3ffec4b5 3fff44d8 3fff8ab4 00000003 9 | 3ffffe30: 00000002 3fff4884 3fff44d8 40246fb8 10 | 3ffffe40: 3fff8ca0 ff000000 00004000 4020fb00 11 | 3ffffe50: 3fff8ca0 ff000000 3fff44d8 4024f9d5 12 | 3ffffe60: 4020d0d0 3fff4884 3fff44d8 00000003 13 | 3ffffe70: 00000002 3fff4884 3fff44d8 4020aa5c 14 | 3ffffe80: 45455b20 4d4f5250 4545205d 4d4f5250 15 | 3ffffe90: 63655320 20726f74 6c6f6f70 7a697320 16 | 3ffffea0: 73692065 202c3420 20646e61 75206e69 17 | 3ffffeb0: 61206573 203a6572 39313031 31303120 18 | 3ffffec0: 30312038 31203731 20363130 00000000 19 | 3ffffed0: 6f626552 6120746f 72657466 63757320 20 | 3ffffee0: 73736563 206c7566 2041544f 61647075 21 | 3ffffef0: 36006574 00000000 00000000 00000000 22 | 3fffff00: 00000000 00000007 3ffe8304 40227977 23 | 3fffff10: 0000000d 00000001 3fff44d8 3fff47b0 24 | 3fffff20: 3fff8e64 00000001 3fff44d8 4020c9e1 25 | 3fffff30: 0000006d 3fff4740 0000000d 4021813e 26 | 3fffff40: 0000006d 3fff4740 3fff472c 3fff47b0 27 | 3fffff50: 0000000d 3fff472c 3fff44d8 4020cfbd 28 | 3fffff60: 0000000d 3fff4a1c 3ffe97b8 3fff49c0 29 | 3fffff70: 3fff44d8 3fff2adc 3fff44c8 3fff49c0 30 | 3fffff80: 3fffdad0 3fff2adc 3fff44d8 4020d090 31 | 3fffff90: 3fffdad0 00000000 3fff2adc 40205cbc 32 | 3fffffa0: 3fffdad0 00000000 3fff4990 4020f088 33 | 3fffffb0: feefeffe feefeffe 3ffe97b8 401006f1 34 | << of a template declaration 26 | BinPackArguments: false 27 | BinPackParameters: false 28 | BreakBeforeBinaryOperators: NonAssignment 29 | BreakConstructorInitializersBeforeComma: true # Always break constructor initializers before commas and align the commas with the colon. 30 | ExperimentalAutoDetectBinPacking: false 31 | KeepEmptyLinesAtTheStartOfBlocks: false 32 | MaxEmptyLinesToKeep: 4 33 | PenaltyBreakBeforeFirstCallParameter: 200 34 | PenaltyExcessCharacter: 10 35 | PointerAlignment: Middle 36 | SpaceAfterCStyleCast: false 37 | SpaceBeforeAssignmentOperators: true 38 | SpacesInCStyleCastParentheses: false 39 | SpacesInParentheses: false 40 | DerivePointerAlignment: false 41 | SortIncludes: false -------------------------------------------------------------------------------- /scripts/pre_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from subprocess import call 3 | import os 4 | import re 5 | Import("env") 6 | 7 | def build_web(): 8 | print("** Building web...") 9 | env.Execute( 10 | "node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --silent --cwd ./tools/webfilesbuilder") 11 | 12 | def code_check(source, target, env): 13 | print("** Starting cppcheck...") 14 | call(["cppcheck", os.getcwd()+"/.", "--force", "--enable=all"]) 15 | print("\n** Finished cppcheck...\n") 16 | print("\n** Starting cpplint...") 17 | call(["cpplint", "--extensions=ino,cpp,h", "--filter=-legal/copyright,-build/include,-whitespace", 18 | "--linelength=120", "--recursive", "src", "lib/myESP"]) 19 | print("\n** Finished cpplint...") 20 | 21 | 22 | # build web files 23 | build_web() 24 | 25 | # extract application details 26 | bag = {} 27 | exprs = [ 28 | (re.compile(r'^#define APP_VERSION\s+"(\S+)"'), 'app_version'), 29 | (re.compile(r'^#define APP_NAME\s+"(\S+)"'), 'app_name'), 30 | (re.compile(r'^#define APP_HOSTNAME\s+"(\S+)"'), 'app_hostname') 31 | ] 32 | with open('./src/version.h', 'r') as f: 33 | for l in f.readlines(): 34 | for expr, var in exprs: 35 | m = expr.match(l) 36 | if m and len(m.groups()) > 0: 37 | bag[var] = m.group(1) 38 | 39 | with open('./src/ems-esp.cpp', 'r') as f: 40 | for l in f.readlines(): 41 | for expr, var in exprs: 42 | m = expr.match(l) 43 | if m and len(m.groups()) > 0: 44 | bag[var] = m.group(1) 45 | 46 | app_version = bag.get('app_version') 47 | app_name = bag.get('app_name') 48 | app_hostname = bag.get('app_hostname') 49 | board = env['BOARD'] 50 | branch = env['PIOENV'] 51 | 52 | # build filename, replacing . with _ for the version 53 | env.Replace(PROGNAME=app_name + "-" + 54 | app_version.replace(".", "_") + "-" + board) 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: bionic 3 | language: python 4 | python: 5 | - "3.8" 6 | 7 | cache: 8 | directories: 9 | - ${HOME}/.pio 10 | 11 | env: 12 | global: 13 | # - BUILDER_TOTAL_THREADS=4 14 | - BUILDER_TOTAL_THREADS=1 15 | - OWNER=${TRAVIS_REPO_SLUG%/*} 16 | - DEV=${OWNER/Victor-Mo/dev} 17 | - BRANCH=${TRAVIS_BRANCH/dev/} 18 | - TAG=${DEV}${BRANCH:+_}${BRANCH} 19 | 20 | install: 21 | - env | grep TRAVIS 22 | - set -e 23 | - pip install -U platformio 24 | - pio platform update -p 25 | - set +e 26 | 27 | branches: 28 | except: 29 | - /^travis-.*-build$/ 30 | 31 | script: 32 | - ./scripts/build.sh 33 | # - ./scripts/build.sh -p 34 | 35 | stages: 36 | - name: Release 37 | # if: type IN (cron, api) 38 | 39 | jobs: 40 | include: 41 | - stage: Release 42 | # env: BUILDER_THREAD=0 43 | # - env: BUILDER_THREAD=1 44 | # - env: BUILDER_THREAD=2 45 | # - env: BUILDER_THREAD=3 46 | 47 | before_deploy: 48 | - export FIRMWARE_VERSION=$(grep -E '^#define APP_VERSION' ./src/version.h | awk '{print $3}' | sed 's/"//g') 49 | - git tag -f travis-${TAG}-build 50 | - git remote add gh 51 | https://${OWNER}:${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git 52 | - git push gh :travis-${TAG}-build || true 53 | - git push -f gh travis-${TAG}-build 54 | - git remote remove gh 55 | 56 | deploy: 57 | provider: releases 58 | edge: 59 | # source: wenkokke/dpl 60 | branch: master 61 | token: ${GITHUB_TOKEN} 62 | file_glob: true 63 | # file: "firmware/*.bin" 64 | file: "*.bin" 65 | name: latest development build 66 | release_notes: 67 | Version $FIRMWARE_VERSION. 68 | Automatic firmware builds of the current IRT-ESP branch built on $(date +'%F %T %Z') from commit $TRAVIS_COMMIT. 69 | Warning, this is a development build and not fully tested. Use at your own risk. 70 | cleanup: false 71 | prerelease: true 72 | overwrite: true 73 | target_commitish: $TRAVIS_COMMIT 74 | on: 75 | tags: false 76 | branch: master 77 | 78 | notifications: 79 | email: 80 | on_success: change 81 | on_failure: change 82 | 83 | 84 | -------------------------------------------------------------------------------- /doc/home assistant/ui-lovelace.yaml: -------------------------------------------------------------------------------- 1 | title: Home 2 | views: 3 | 4 | - title: Heating 5 | cards: 6 | - type: entities 7 | title: Boiler 8 | show_header_toggle: false 9 | entities: 10 | - sensor.boiler_boottime 11 | - sensor.boiler_updated 12 | - type: divider 13 | - sensor.warm_water_selected_temperature 14 | - sensor.warm_water_current_temperature 15 | - sensor.warm_water_activated 16 | - sensor.warm_water_3_way_valve 17 | - sensor.warm_water_tapwater_flow_rate 18 | - type: divider 19 | - sensor.boiler_temperature 20 | - sensor.return_temperature 21 | - sensor.current_flow_temperature 22 | - sensor.pump_modulation 23 | - sensor.ignition 24 | - sensor.gas 25 | - sensor.fan 26 | - sensor.boiler_pump 27 | - sensor.system_pressure 28 | - sensor.burner_max_power 29 | - sensor.burner_current_power 30 | 31 | - type: vertical-stack 32 | cards: 33 | - type: entities 34 | title: Shower Monitor 35 | show_header_toggle: false 36 | entities: 37 | - switch.shower_timer 38 | - switch.long_shower_alert 39 | - type: divider 40 | - sensor.last_shower_duration 41 | - sensor.showertime_time 42 | - type: entity-button 43 | icon: mdi:shower-head 44 | name: send a cold shot of shower water 45 | entity: script.shower_coldshot 46 | tap_action: 47 | action: call-service 48 | service: script.turn_on 49 | service_data: 50 | entity_id: script.shower_coldshot 51 | 52 | - type: vertical-stack 53 | cards: 54 | - type: history-graph 55 | entities: 56 | - sensor.current_room_temperature 57 | - sensor.dark_sky_temperature 58 | - type: thermostat 59 | entity: climate.thermostat 60 | - type: thermostat 61 | name: WarmWater 62 | entity: climate.boiler 63 | -------------------------------------------------------------------------------- /src/pid.h: -------------------------------------------------------------------------------- 1 | /*This file has been prepared for Doxygen automatic documentation generation.*/ 2 | /*! \file ********************************************************************* 3 | * 4 | * \brief Header file for pid.c. 5 | * 6 | * - File: pid.h 7 | * - Compiler: IAR EWAAVR 4.11A 8 | * - Supported devices: All AVR devices can be used. 9 | * - AppNote: AVR221 - Discrete PID controller 10 | * 11 | * \author Atmel Corporation: http://www.atmel.com \n 12 | * Support email: avr@atmel.com 13 | * 14 | * $Name$ 15 | * $Revision: 456 $ 16 | * $RCSfile$ 17 | * $Date: 2006-02-16 12:46:13 +0100 (to, 16 feb 2006) $ 18 | *****************************************************************************/ 19 | 20 | #ifndef PID_H 21 | #define PID_H 22 | 23 | #include "stdint.h" 24 | 25 | #define SCALING_FACTOR 128 26 | 27 | /*! \brief PID Status 28 | * 29 | * Setpoints and data used by the PID control algorithm 30 | */ 31 | typedef struct PID_DATA{ 32 | //! Last process value, used to find derivative of process value. 33 | int16_t lastProcessValue; 34 | //! Summation of errors, used for integrate calculations 35 | int32_t sumError; 36 | //! The Proportional tuning constant, multiplied with SCALING_FACTOR 37 | int16_t P_Factor; 38 | //! The Integral tuning constant, multiplied with SCALING_FACTOR 39 | int16_t I_Factor; 40 | //! The Derivative tuning constant, multiplied with SCALING_FACTOR 41 | int16_t D_Factor; 42 | //! Maximum allowed error, avoid overflow 43 | int16_t maxError; 44 | //! Maximum allowed sumerror, avoid overflow 45 | int32_t maxSumError; 46 | } pidData_t; 47 | 48 | /*! \brief Maximum values 49 | * 50 | * Needed to avoid sign/overflow problems 51 | */ 52 | // Maximum value of variables 53 | #define MAX_INT INT16_MAX 54 | #define MAX_LONG INT32_MAX 55 | #define MAX_I_TERM (MAX_LONG / 2) 56 | 57 | // Boolean values 58 | #define FALSE 0 59 | #define TRUE 1 60 | 61 | void pid_Init(int16_t p_factor, int16_t i_factor, int16_t d_factor, struct PID_DATA *pid); 62 | int16_t pid_Controller(int16_t setPoint, int16_t processValue, struct PID_DATA *pid_st); 63 | void pid_Reset_Integrator(pidData_t *pid_st); 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /src/irtuart.h: -------------------------------------------------------------------------------- 1 | /* 2 | * irtuart.h 3 | * 4 | * Header file for irtuart.cpp 5 | * 6 | * Paul Derbyshire - https://github.com/proddy/EMS-ESP 7 | */ 8 | #pragma once 9 | 10 | #include 11 | 12 | #define IRTUART_UART 0 // UART 0 13 | #define IRTUART_CONFIG_PASSIF (0x1C | (1 << UCRXI) ) // 8N1 (8 bits, no stop bits, 1 parity), invert Rxd 14 | #define IRTUART_CONFIG_ACTIVE (0x1C | (1 << UCRXI) | (1 << UCTXI)) // 8N1 (8 bits, no stop bits, 1 parity), invert Rxd, Invert TX 15 | 16 | 17 | #define IRTUART_BAUD 4800 // uart baud rate for the IRT circuit 18 | 19 | 20 | #define IRT_MAXBUFFERS 5 // buffers for circular filling to avoid collisions 21 | #define IRT_MAXBUFFERSIZE (IRT_MAX_TELEGRAM_LENGTH + 2) // max size of the rx buffer. IRT packets are max 32 bytes, plus extra 2 for BRKs 22 | #define IRT_MAXTXBUFFERSIZE (IRT_MAXBUFFERSIZE / 2) // max size of tx buffer 23 | 24 | //#define IRTUART_BIT_TIME 208 // bit time @4800 baud 25 | 26 | #define IRTUART_TX_BRK_WAIT 2070 // the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag) 27 | //#define IRTUART_TX_WAIT_BYTE IRTUART_BIT_TIME * 10 // Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) 28 | //#define IRTUART_TX_WAIT_BRK IRTUART_BIT_TIME * 11 // Time to send a BRK Signal (11 Bit) 29 | //#define IRTUART_TX_WAIT_GAP IRTUART_BIT_TIME * 7 // Gap between to Bytes 30 | //#define IRTUART_TX_LAG 8 31 | 32 | #define IRTUART_TX_MSG_TIMEOUT_MS 4000 // if a message is still waiting after 4s abort 33 | 34 | 35 | #define IRTUART_recvTaskPrio 1 36 | #define IRTUART_recvTaskQueueLen 64 37 | 38 | typedef struct { 39 | uint8_t length; 40 | uint8_t buffer[IRT_MAXBUFFERSIZE]; 41 | } _IRTRxBuf; 42 | 43 | typedef struct { 44 | uint8_t valid; 45 | uint8_t state; 46 | uint8_t address; 47 | unsigned long start_time; 48 | 49 | uint8_t tx_bytes; 50 | uint8_t rx_bytes; 51 | 52 | uint8_t pos; 53 | uint8_t len; 54 | uint8_t buffer[IRT_MAXTXBUFFERSIZE]; 55 | 56 | } _IRTTxBuf; 57 | 58 | void ICACHE_FLASH_ATTR irtuart_init(); 59 | void ICACHE_FLASH_ATTR irtuart_setup(); 60 | void ICACHE_FLASH_ATTR irtuart_stop(); 61 | void ICACHE_FLASH_ATTR irtuart_start(); 62 | 63 | uint16_t ICACHE_FLASH_ATTR irtuart_check_tx(uint8_t reset_if_done); 64 | uint16_t ICACHE_FLASH_ATTR irtuart_send_tx_buffer(uint8_t address, uint8_t *telegram, uint8_t len); 65 | 66 | -------------------------------------------------------------------------------- /src/Timezone.h: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------* 2 | * Arduino Timezone Library * 3 | * Jack Christensen Mar 2012 * 4 | * * 5 | * Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and * 6 | * licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html * 7 | *----------------------------------------------------------------------*/ 8 | 9 | #ifndef TIMEZONE_H_INCLUDED 10 | #define TIMEZONE_H_INCLUDED 11 | #include 12 | #include 13 | 14 | // convenient constants for TimeChangeRules 15 | enum week_t { Last, First, Second, Third, Fourth }; 16 | enum dow_t { Sun = 1, Mon, Tue, Wed, Thu, Fri, Sat }; 17 | enum month_t { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }; 18 | 19 | // structure to describe rules for when daylight/summer time begins, 20 | // or when standard time begins. 21 | struct TimeChangeRule { 22 | char abbrev[6]; // five chars max 23 | uint8_t week; // First, Second, Third, Fourth, or Last week of the month 24 | uint8_t dow; // day of week, 1=Sun, 2=Mon, ... 7=Sat 25 | uint8_t month; // 1=Jan, 2=Feb, ... 12=Dec 26 | uint8_t hour; // 0-23 27 | int offset; // offset from UTC in minutes 28 | }; 29 | 30 | class Timezone { 31 | public: 32 | Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart); 33 | Timezone(TimeChangeRule stdTime); 34 | time_t toLocal(time_t utc); 35 | time_t toLocal(time_t utc, TimeChangeRule ** tcr); 36 | time_t toUTC(time_t local); 37 | bool utcIsDST(time_t utc); 38 | bool locIsDST(time_t local); 39 | void setRules(TimeChangeRule dstStart, TimeChangeRule stdStart); 40 | 41 | private: 42 | void calcTimeChanges(int yr); 43 | void initTimeChanges(); 44 | time_t toTime_t(TimeChangeRule r, int yr); 45 | TimeChangeRule m_dst; // rule for start of dst or summer time for any year 46 | TimeChangeRule m_std; // rule for start of standard time for any year 47 | time_t m_dstUTC; // dst start for given/current year, given in UTC 48 | time_t m_stdUTC; // std time start for given/current year, given in UTC 49 | time_t m_dstLoc; // dst start for given/current year, given in local time 50 | time_t m_stdLoc; // std time start for given/current year, given in local time 51 | }; 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/TimeLib.h: -------------------------------------------------------------------------------- 1 | /* 2 | * customized version of Time library, originally Copyright (c) Michael Margolis 2009-2014 3 | * modified by Paul S https://github.com/PaulStoffregen/Time 4 | */ 5 | 6 | #ifndef _TimeLib_h 7 | #define _TimeLib_h 8 | 9 | #include 10 | 11 | #define SECS_PER_MIN ((time_t)(60UL)) 12 | #define SECS_PER_HOUR ((time_t)(3600UL)) 13 | #define SECS_PER_DAY ((time_t)(SECS_PER_HOUR * 24UL)) 14 | #define tmYearToCalendar(Y) ((Y) + 1970) // full four digit year 15 | 16 | // This ugly hack allows us to define C++ overloaded functions, when included 17 | // from within an extern "C", as newlib's sys/stat.h does. Actually it is 18 | // intended to include "time.h" from the C library (on ARM, but AVR does not 19 | // have that file at all). On Mac and Windows, the compiler will find this 20 | // "Time.h" instead of the C library "time.h", so we may cause other weird 21 | // and unpredictable effects by conflicting with the C library header "time.h", 22 | // but at least this hack lets us define C++ functions as intended. Hopefully 23 | // nothing too terrible will result from overriding the C library header?! 24 | extern "C++" { 25 | typedef enum { timeNotSet, timeNeedsSync, timeSet } timeStatus_t; 26 | 27 | typedef struct { 28 | uint8_t Second; 29 | uint8_t Minute; 30 | uint8_t Hour; 31 | uint8_t Wday; // day of week, sunday is day 1 32 | uint8_t Day; 33 | uint8_t Month; 34 | uint8_t Year; // offset from 1970; 35 | } tmElements_t; 36 | 37 | typedef time_t (*getExternalTime)(); 38 | 39 | time_t now(); // return the current time as seconds since Jan 1 1970 40 | void setTime(time_t t); 41 | timeStatus_t timeStatus(); // indicates if time has been set and recently synchronized 42 | void setSyncProvider(getExternalTime getTimeFunction); // identify the external time provider 43 | void setSyncInterval(time_t interval); // set the number of seconds between re-sync 44 | time_t makeTime(const tmElements_t & tm); // convert time elements into time_t 45 | 46 | uint8_t to_hour(time_t t); // the hour for the given time 47 | uint8_t to_minute(time_t t); // the minute for the given time 48 | uint8_t to_second(time_t t); // the second for the given time 49 | uint8_t to_day(time_t t); // the day for the given time (0-6) 50 | uint8_t to_month(time_t t); // the month for the given time 51 | uint8_t to_weekday(time_t t); // weekday, sunday is day 1 52 | uint16_t to_year(time_t t); // the year for the given time 53 | 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /src/websrc/3rdparty/css/sidebar.css: -------------------------------------------------------------------------------- 1 | html {position: relative;overflow: scroll;overflow-x: hidden;min-height: 100% }::-webkit-scrollbar {width: 0px;background: transparent;}::-webkit-scrollbar-thumb {background: #e8e8e8;}body {background: #f1f3f6;margin-bottom: 60px }p {font-size: 1.1em;font-weight: 300;line-height: 1.7em;color: #999 }a, a:focus, a:hover {color: inherit;text-decoration: none;transition: all .3s }.navbar {padding: 15px 10px;background: #fff;border: none;border-radius: 0;margin-bottom: 40px;box-shadow: 1px 1px 3px rgba(0, 0, 0, .1) }#dismiss, #sidebar {background: #337ab7 }#content.navbar-btn {box-shadow: none;outline: 0;border: none }.line {width: 100%;height: 1px;border-bottom: 1px dashed #ddd;margin: 40px 0 }#sidebar {width: 250px;position: fixed;top: 0;left: -250px;height: 100vh;z-index: 999;color: #fff;transition: all .3s;overflow-y: auto;box-shadow: 3px 3px 3px rgba(0, 0, 0, .2) }@media screen and (min-width:768px) {#sidebar {left: 0 }.footer {margin-left: 250px }#ajaxcontent {margin-left: 250px }#dismiss, .navbar-btn {display: none }}#sidebar.active {left: 0 }#dismiss {width: 35px;height: 35px;line-height: 35px;text-align: center;position: absolute;top: 10px;right: 10px;cursor: pointer;-webkit-transition: all .3s;-o-transition: all .3s;transition: all .3s }#dismiss:hover {background: #fff;color: #337ab7 }.overlay {position: fixed;width: 100vw;height: 100vh;background: rgba(0, 0, 0, .7);z-index: 998;display: none }#sidebar .sidebar-header {padding: 20px;background: #337ab7 }#sidebar ul.components {padding: 20px 0;border-bottom: 1px solid #47748b }#content, ul.CTAs {padding: 20px }#sidebar ul p {color: #fff;padding: 10px }#sidebar ul li a {padding: 10px;font-size: 1.1em;display: block }#sidebar ul li a:hover {color: #337ab7;background: #fff }#sidebar ul li.active>a, a[aria-expanded=true] {color: #fff;background: #2e6da4 }a[data-toggle=collapse] {position: relative }a[aria-expanded=false]::before, a[aria-expanded=true]::before {content: '\e259';display: block;position: absolute;right: 20px;font-family: 'Glyphicons Halflings';font-size: .6em }#sidebar ul ul a, ul.CTAs a {font-size: .9em }a[aria-expanded=true]::before {content: '\e260' }#sidebar ul ul a {padding-left: 30px;background: #2e6da4 }ul.CTAs a {text-align: center;display: block;border-radius: 5px;margin-bottom: 5px }a.download {background: #fff;color: #337ab7 }#sidebar a.article, a.article:hover {background: #2e6da4;color: #fff }#content {width: 100%;min-height: 100vh;transition: all .3s;position: absolute;top: 0;right: 0 }.footer {position: fixed;bottom: 0;width: 100%;height: 45px;background-color: #f1f3f6 }i {margin-right: 1em } -------------------------------------------------------------------------------- /src/pid.cpp: -------------------------------------------------------------------------------- 1 | /*This file has been prepared for Doxygen automatic documentation generation.*/ 2 | /*! \file ********************************************************************* 3 | * 4 | * \brief General PID implementation for AVR. 5 | * 6 | * Discrete PID controller implementation. Set up by giving P/I/D terms 7 | * to Init_PID(), and uses a struct PID_DATA to store internal values. 8 | * 9 | * - File: pid.c 10 | * - Compiler: IAR EWAAVR 4.11A 11 | * - Supported devices: All AVR devices can be used. 12 | * - AppNote: AVR221 - Discrete PID controller 13 | * 14 | * \author Atmel Corporation: http://www.atmel.com \n 15 | * Support email: avr@atmel.com 16 | * 17 | * $Name$ 18 | * $Revision: 456 $ 19 | * $RCSfile$ 20 | * $Date: 2006-02-16 12:46:13 +0100 (to, 16 feb 2006) $ 21 | *****************************************************************************/ 22 | 23 | #include "pid.h" 24 | #include "stdint.h" 25 | 26 | /*! \brief Initialisation of PID controller parameters. 27 | * 28 | * Initialise the variables used by the PID algorithm. 29 | * 30 | * \param p_factor Proportional term. 31 | * \param i_factor Integral term. 32 | * \param d_factor Derivate term. 33 | * \param pid Struct with PID status. 34 | */ 35 | void pid_Init(int16_t p_factor, int16_t i_factor, int16_t d_factor, struct PID_DATA *pid) 36 | // Set up PID controller parameters 37 | { 38 | // Start values for PID controller 39 | pid->sumError = 0; 40 | pid->lastProcessValue = 0; 41 | // Tuning constants for PID loop 42 | pid->P_Factor = p_factor; 43 | pid->I_Factor = i_factor; 44 | pid->D_Factor = d_factor; 45 | // Limits to avoid overflow 46 | pid->maxError = MAX_INT / (pid->P_Factor + 1); 47 | pid->maxSumError = MAX_I_TERM / (pid->I_Factor + 1); 48 | } 49 | 50 | 51 | /*! \brief PID control algorithm. 52 | * 53 | * Calculates output from setpoint, process value and PID status. 54 | * 55 | * \param setPoint Desired value. 56 | * \param processValue Measured value. 57 | * \param pid_st PID status struct. 58 | */ 59 | int16_t pid_Controller(int16_t setPoint, int16_t processValue, struct PID_DATA *pid_st) 60 | { 61 | int16_t error, p_term, d_term; 62 | int32_t i_term, ret, temp; 63 | 64 | error = setPoint - processValue; 65 | 66 | // Calculate Pterm and limit error overflow 67 | if (error > pid_st->maxError){ 68 | p_term = MAX_INT; 69 | } 70 | else if (error < -pid_st->maxError){ 71 | p_term = -MAX_INT; 72 | } 73 | else{ 74 | p_term = pid_st->P_Factor * error; 75 | } 76 | 77 | // Calculate Iterm and limit integral runaway 78 | temp = pid_st->sumError + error; 79 | if(temp > pid_st->maxSumError){ 80 | i_term = MAX_I_TERM; 81 | pid_st->sumError = pid_st->maxSumError; 82 | } 83 | else if(temp < -pid_st->maxSumError){ 84 | i_term = -MAX_I_TERM; 85 | pid_st->sumError = -pid_st->maxSumError; 86 | } 87 | else{ 88 | pid_st->sumError = temp; 89 | i_term = pid_st->I_Factor * pid_st->sumError; 90 | } 91 | 92 | // Calculate Dterm 93 | d_term = pid_st->D_Factor * (pid_st->lastProcessValue - processValue); 94 | 95 | pid_st->lastProcessValue = processValue; 96 | 97 | ret = (p_term + i_term + d_term) / SCALING_FACTOR; 98 | if(ret > MAX_INT){ 99 | ret = MAX_INT; 100 | } 101 | else if(ret < -MAX_INT){ 102 | ret = -MAX_INT; 103 | } 104 | 105 | return((int16_t)ret); 106 | } 107 | 108 | /*! \brief Resets the integrator. 109 | * 110 | * Calling this function will reset the integrator in the PID regulator. 111 | */ 112 | void pid_Reset_Integrator(pidData_t *pid_st) 113 | { 114 | pid_st->sumError = 0; 115 | } 116 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; 2 | ; PlatformIO Project Configuration File for EMS-ESP 3 | ; 4 | 5 | [platformio] 6 | default_envs = release 7 | ;default_envs = debug 8 | 9 | [common] 10 | ; custom build options are: 11 | ; -DMYESP_TIMESTAMP 12 | ; -DTESTS 13 | ; -DCRASH 14 | ; -DFORCE_SERIAL 15 | ; -DMYESP_DEBUG 16 | ; -DSENSOR_MQTT_USEID 17 | ;custom_flags = -DFORCE_SERIAL -DMYESP_DEBUG 18 | custom_flags = 19 | 20 | # Available lwIP variants (macros): 21 | # -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH = v1.4 Higher Bandwidth (default) 22 | # -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY = v2 Lower Memory 23 | # -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH = v2 Higher Bandwidth 24 | # -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH 25 | # -DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY 26 | # Other flags 27 | # -DVTABLES_IN_FLASH 28 | # -DNO_GLOBAL_EEPROM 29 | # -DBEARSSL_SSL_BASIC 30 | #general_flags = -DNO_GLOBAL_EEPROM -DVTABLES_IN_FLASH -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH 31 | general_flags = -DNO_GLOBAL_EEPROM 32 | 33 | 34 | # From https://github.com/esp8266/Arduino/blob/master/tools/sdk/ld 35 | # eagle.flash.4m1m.ld = 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer 36 | # eagle.flash.4m2m.ld = same as above but with 2024 KB SPIFFS 37 | # eagle.flash.4m.ld = same as above but with no SPIFFS storage 38 | board_build.ldscript = eagle.flash.4m.ld 39 | build_flags = ${common.general_flags} -std=c++11 -fno-exceptions 40 | 41 | [env] 42 | framework = arduino 43 | ;platform = espressif8266@2.2.2 ; arduino core 2.5.2 44 | platform = espressif8266 45 | ;platform = https://github.com/platformio/platform-espressif8266#develop 46 | ;platform = https://github.com/platformio/platform-espressif8266#feature/stage 47 | lib_compat_mode = strict 48 | lib_deps = 49 | https://github.com/rlogiacco/CircularBuffer 50 | https://github.com/PaulStoffregen/OneWire 51 | https://github.com/me-no-dev/ESPAsyncWebServer 52 | https://github.com/me-no-dev/ESPAsyncUDP 53 | uuid-common@^1.1.0 54 | uuid-log@^2.1.1 55 | uuid-syslog@^2.0.4 ; https://github.com/nomis/mcu-uuid-syslog 56 | JustWifi@2.0.2 ; https://github.com/xoseperez/justwifi 57 | AsyncMqttClient@0.8.2 ; https://github.com/marvinroger/async-mqtt-client 58 | EEPROM_Rotate@0.9.2 ; https://github.com/xoseperez/eeprom_rotate 59 | ArduinoJson@6.14.1 ; https://github.com/bblanchon/ArduinoJson 60 | ESPAsyncTCP@1.2.2 ; https://github.com/me-no-dev/ESPAsyncTCP 61 | upload_speed = 921600 62 | monitor_speed = 115200 63 | 64 | ; example ports for OSX 65 | ;upload_port = /dev/cu.wchusbserial14403 66 | ;upload_port = /dev/cu.usbserial-1440 67 | 68 | ; comment out this section if using USB and not OTA for firmware uploads 69 | upload_protocol = espota 70 | upload_port = irt-esp.local 71 | 72 | # 73 | # These following targets are used by TravisCI to build the firmware versions on Release 74 | # Do not modify 75 | # 76 | [env:travis] 77 | board = esp12e 78 | build_flags = ${common.build_flags} 79 | extra_scripts = scripts/main_script.py 80 | 81 | [env:esp12e] 82 | board = esp12e 83 | build_flags = ${common.build_flags} 84 | extra_scripts = scripts/main_script.py 85 | 86 | [env:d1_mini] 87 | board = d1_mini 88 | build_flags = ${common.build_flags} 89 | extra_scripts = scripts/main_script.py 90 | 91 | [env:nodemcuv2] 92 | board = nodemcuv2 93 | build_flags = ${common.build_flags} 94 | extra_scripts = scripts/main_script.py 95 | 96 | [env:nodemcu] 97 | board = nodemcu 98 | build_flags = ${common.build_flags} 99 | extra_scripts = scripts/main_script.py 100 | 101 | # 102 | # These two targets below (release and debug) can be modified 103 | # 104 | [env:debug] 105 | board = d1_mini 106 | build_type = debug 107 | build_flags = ${common.build_flags} ${common.custom_flags} 108 | extra_scripts = 109 | pre:scripts/pre_script.py 110 | scripts/main_script.py 111 | 112 | [env:release] 113 | board = d1_mini 114 | build_type = release 115 | build_flags = ${common.build_flags} ${common.custom_flags} 116 | extra_scripts = 117 | pre:scripts/pre_script.py 118 | scripts/main_script.py 119 | -------------------------------------------------------------------------------- /src/my_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * my_config.h 3 | * 4 | * All configurations and customization's go here 5 | * 6 | * Paul Derbyshire - https://github.com/proddy/EMS-ESP 7 | */ 8 | 9 | #pragma once 10 | 11 | //#include "ems.h" 12 | 13 | // TOPICS with *_CMD_* are used for receiving commands from an MQTT Broker 14 | // EMS-ESP will subscribe to these topics 15 | 16 | #define TOPIC_GENERIC_CMD "generic_cmd" // for receiving generic system commands via MQTT 17 | 18 | // MQTT for thermostat 19 | // these topics can be suffixed with a Heating Circuit number, e.g. thermostat_cmd_temp1 and thermostat_data1 20 | #define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values to MQTT 21 | 22 | #define TOPIC_THERMOSTAT_CMD "thermostat_cmd" // for receiving thermostat commands via MQTT 23 | #define TOPIC_THERMOSTAT_CMD_TEMP_HA "thermostat_cmd_temp" // temp changes via MQTT, for HA climate component 24 | #define TOPIC_THERMOSTAT_CMD_MODE_HA "thermostat_cmd_mode" // mode changes via MQTT, for HA climate component 25 | #define TOPIC_THERMOSTAT_CMD_TEMP "temp" // temp changes via MQTT 26 | #define TOPIC_THERMOSTAT_CMD_MODE "mode" // mode changes via MQTT 27 | #define TOPIC_THERMOSTAT_CMD_DAYTEMP "daytemp" // day temp (RC35 specific) 28 | #define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "nighttemp" // night temp (RC35 specific) 29 | #define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "holidayttemp" // holiday temp (RC35 specific) 30 | 31 | #define THERMOSTAT_CURRTEMP "currtemp" // current temperature 32 | #define THERMOSTAT_SELTEMP "seltemp" // selected temperature 33 | #define THERMOSTAT_HC "hc" // which heating circuit number 34 | #define THERMOSTAT_MODE "mode" // mode 35 | #define THERMOSTAT_DAYTEMP "daytemp" // RC35 specific 36 | #define THERMOSTAT_NIGHTTEMP "nighttemp" // RC35 specific 37 | #define THERMOSTAT_HOLIDAYTEMP "holidayttemp" // RC35 specific 38 | #define THERMOSTAT_HEATINGTYPE "heatingtype" // RC35 specific (3=floorheating) 39 | #define THERMOSTAT_CIRCUITCALCTEMP "circuitcalctemp" // RC35 specific 40 | 41 | // mixing module 42 | #define MIXING_HC "hc" // which heating circuit number 43 | #define MIXING_WWC "wwc" // which warm water circuit number 44 | 45 | // MQTT for boiler 46 | #define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT 47 | #define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running 48 | #define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on 49 | 50 | #define TOPIC_BOILER_CMD "boiler_cmd" // for receiving boiler commands via MQTT 51 | #define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // change water on/off 52 | #define TOPIC_BOILER_CMD_WWONETIME "boiler_cmd_wwonetime" // warm warter one time loading 53 | //#define TOPIC_BOILER_CMD_WWCIRCULATION "boiler_cmd_wwcirculation" // start warm warter circulation 54 | #define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // wwtemp changes via MQTT 55 | #define TOPIC_BOILER_CMD_COMFORT "comfort" // ww comfort setting via MQTT 56 | #define TOPIC_BOILER_CMD_FLOWTEMP "flowtemp" // flowtemp value via MQTT 57 | 58 | // MQTT for mixing device 59 | #define TOPIC_MIXING_DATA "mixing_data" // for sending mixing device values to MQTT 60 | 61 | // MQTT for SM10/SM100/SM200 Solar Module 62 | #define TOPIC_SM_DATA "sm_data" // topic name 63 | #define SM_COLLECTORTEMP "collectortemp" // collector temp 64 | #define SM_BOTTOMTEMP "bottomtemp" // bottom temp 65 | #define SM_PUMPMODULATION "pumpmodulation" // pump modulation 66 | #define SM_PUMP "pump" // pump active 67 | #define SM_ENERGYLASTHOUR "energylasthour" // energy last hour 68 | #define SM_ENERGYTODAY "energytoday" // energy today 69 | #define SM_ENERGYTOTAL "energytotal" // energy total 70 | #define SM_PUMPWORKMIN "pumpWorkMin" // Total minutes 71 | 72 | // MQTT for HP (HeatPump) 73 | #define TOPIC_HP_DATA "hp_data" // topic name 74 | #define HP_PUMPMODULATION "pumpmodulation" // pump modulation 75 | #define HP_PUMPSPEED "pumpspeed" // pump speed 76 | 77 | // shower time 78 | #define TOPIC_SHOWER_DATA "shower_data" // for sending shower time results 79 | #define TOPIC_SHOWER_TIMER "timer" // toggle switch for enabling the shower logic 80 | #define TOPIC_SHOWER_ALERT "alert" // toggle switch for enabling the shower alarm logic 81 | #define TOPIC_SHOWER_COLDSHOT "coldshot" // used to trigger a coldshot from an MQTT command 82 | #define TOPIC_SHOWER_DURATION "duration" // duration of the last shower 83 | 84 | // MQTT for External Sensors 85 | #define TOPIC_EXTERNAL_SENSORS "sensors" // topic for sending sensor values to MQTT 86 | #define PAYLOAD_EXTERNAL_SENSOR_NUM "sensor" // which sensor # 87 | #define PAYLOAD_EXTERNAL_SENSOR_ID "id" 88 | #define PAYLOAD_EXTERNAL_SENSOR_TEMP "temp" 89 | -------------------------------------------------------------------------------- /doc/home assistant/sensor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # thermostat HC1 3 | 4 | - platform: mqtt 5 | state_topic: 'home/ems-esp/thermostat_data' 6 | name: 'Current Room Temperature' 7 | unit_of_measurement: '°C' 8 | value_template: "{{ value_json.hc1.currtemp }}" 9 | 10 | - platform: mqtt 11 | state_topic: 'home/ems-esp/thermostat_data' 12 | name: 'Current Set Temperature' 13 | unit_of_measurement: '°C' 14 | value_template: "{{ value_json.hc1.seltemp }}" 15 | 16 | - platform: mqtt 17 | state_topic: 'home/ems-esp/thermostat_data' 18 | name: 'Current Mode' 19 | value_template: "{{ value_json.hc1.mode }}" 20 | 21 | # boiler 22 | 23 | - platform: mqtt 24 | state_topic: 'home/ems-esp/boiler_data' 25 | name: 'Tap Water' 26 | value_template: '{{ value_json.tapwaterActive }}' 27 | 28 | - platform: mqtt 29 | state_topic: 'home/ems-esp/boiler_data' 30 | name: 'Heating' 31 | value_template: '{{ value_json.heatingActive }}' 32 | 33 | - platform: mqtt 34 | state_topic: 'home/ems-esp/boiler_data' 35 | name: 'Warm Water selected temperature' 36 | unit_of_measurement: '°C' 37 | value_template: '{{ value_json.wWSelTemp }}' 38 | 39 | - platform: mqtt 40 | state_topic: 'home/ems-esp/boiler_data' 41 | name: 'Warm Water tapwater flow rate' 42 | unit_of_measurement: 'l/min' 43 | value_template: '{{ value_json.wWCurFlow }}' 44 | 45 | - platform: mqtt 46 | state_topic: 'home/ems-esp/boiler_data' 47 | name: 'Warm Water current temperature' 48 | unit_of_measurement: '°C' 49 | value_template: '{{ value_json.wWCurTmp }}' 50 | 51 | - platform: mqtt 52 | state_topic: 'home/ems-esp/boiler_data' 53 | name: 'Warm Water activated' 54 | value_template: '{{ value_json.wWActivated }}' 55 | 56 | - platform: mqtt 57 | state_topic: 'home/ems-esp/boiler_data' 58 | name: 'Warm Water 3-way valve' 59 | value_template: '{{ value_json.wWHeat }}' 60 | 61 | - platform: mqtt 62 | state_topic: 'home/ems-esp/boiler_data' 63 | name: 'Current flow temperature' 64 | unit_of_measurement: '°C' 65 | value_template: '{{ value_json.curFlowTemp }}' 66 | 67 | - platform: mqtt 68 | state_topic: 'home/ems-esp/boiler_data' 69 | name: 'Return temperature' 70 | unit_of_measurement: '°C' 71 | value_template: '{{ value_json.retTemp }}' 72 | 73 | - platform: mqtt 74 | state_topic: 'home/ems-esp/boiler_data' 75 | name: 'Gas' 76 | value_template: '{{ value_json.burnGas }}' 77 | 78 | - platform: mqtt 79 | state_topic: 'home/ems-esp/boiler_data' 80 | name: 'Boiler pump' 81 | value_template: '{{ value_json.heatPmp }}' 82 | 83 | - platform: mqtt 84 | state_topic: 'home/ems-esp/boiler_data' 85 | name: 'Fan' 86 | value_template: '{{ value_json.fanWork }}' 87 | 88 | - platform: mqtt 89 | state_topic: 'home/ems-esp/boiler_data' 90 | name: 'Ignition' 91 | value_template: '{{ value_json.ignWork }}' 92 | 93 | - platform: mqtt 94 | state_topic: 'home/ems-esp/boiler_data' 95 | name: 'Circulation pump' 96 | value_template: '{{ value_json.wWCirc }}' 97 | 98 | - platform: mqtt 99 | state_topic: 'home/ems-esp/boiler_data' 100 | name: 'Burner max power' 101 | unit_of_measurement: '%' 102 | value_template: '{{ value_json.selBurnPow }}' 103 | 104 | - platform: mqtt 105 | state_topic: 'home/ems-esp/boiler_data' 106 | name: 'Burner max power' 107 | unit_of_measurement: '%' 108 | value_template: '{{ value_json.selBurnPow }}' 109 | 110 | - platform: mqtt 111 | state_topic: 'home/ems-esp/boiler_data' 112 | name: 'Burner current power' 113 | unit_of_measurement: '%' 114 | value_template: '{{ value_json.curBurnPow }}' 115 | 116 | - platform: mqtt 117 | state_topic: 'home/ems-esp/boiler_data' 118 | name: 'System Pressure' 119 | unit_of_measurement: 'bar' 120 | value_template: '{{ value_json.sysPress }}' 121 | 122 | - platform: mqtt 123 | state_topic: 'home/ems-esp/boiler_data' 124 | name: 'Boiler temperature' 125 | unit_of_measurement: '°C' 126 | value_template: '{{ value_json.boilTemp }}' 127 | 128 | - platform: mqtt 129 | state_topic: 'home/ems-esp/boiler_data' 130 | name: 'Pump modulation' 131 | unit_of_measurement: '%' 132 | value_template: '{{ value_json.pumpMod }}' 133 | 134 | # shower time duration 135 | 136 | - platform: mqtt 137 | name: 'Last shower duration' 138 | state_topic: "home/ems-esp/shower_data" 139 | value_template: "{{ value_json.duration | is_defined }}" 140 | 141 | - platform: template 142 | sensors: 143 | showertime_time: 144 | value_template: '{{ as_timestamp(states.sensor.last_shower_duration.last_updated) | int | timestamp_custom("%-I:%M on %a %-d %b") }}' 145 | 146 | boiler_updated: 147 | value_template: '{{ as_timestamp(states.sensor.boiler_temperature.last_updated) | timestamp_custom("%H:%M on %d/%b") }}' 148 | 149 | boiler_boottime: 150 | value_template: '{{ as_timestamp(states.automation.see_if_boiler_restarts.attributes.last_triggered) | timestamp_custom("%H:%M:%S %d/%m/%y") }}' 151 | 152 | # general 153 | 154 | - platform: mqtt 155 | state_topic: 'home/ems-esp/status' 156 | name: 'ems-esp status' 157 | -------------------------------------------------------------------------------- /src/Ntp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Ntp.cpp 3 | */ 4 | 5 | #include "Ntp.h" 6 | #include "MyESP.h" 7 | 8 | char * NtpClient::TimeServerName; 9 | Timezone * NtpClient::tz; 10 | TimeChangeRule * NtpClient::tcr; 11 | time_t NtpClient::syncInterval; 12 | IPAddress NtpClient::timeServer; 13 | AsyncUDP NtpClient::udpListener; 14 | byte NtpClient::NTPpacket[NTP_PACKET_SIZE]; 15 | 16 | // references: 17 | // https://github.com/filipdanic/compact-timezone-list/blob/master/index.js 18 | // https://github.com/sanohin/google-timezones-json/blob/master/timezones.json 19 | // https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json 20 | // https://home.kpn.nl/vanadovv/time/TZworld.html 21 | // https://www.timeanddate.com/time/zones/ 22 | 23 | // Australia Eastern Time Zone (Sydney, Melbourne) 24 | TimeChangeRule aEDT = {"AEDT", First, Sun, Oct, 2, 660}; // UTC + 11 hours 25 | TimeChangeRule aEST = {"AEST", First, Sun, Apr, 3, 600}; // UTC + 10 hours 26 | Timezone ausET(aEDT, aEST); 27 | 28 | // Moscow Standard Time (MSK, does not observe DST) 29 | TimeChangeRule msk = {"MSK", Last, Sun, Mar, 1, 180}; 30 | Timezone MSK(msk); 31 | 32 | // Turkey 33 | TimeChangeRule trt = {"TRT", Last, Sun, Mar, 1, 180}; 34 | Timezone TRT(trt); 35 | 36 | // Central European Time (Frankfurt, Paris) 37 | TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time 38 | TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time 39 | Timezone CE(CEST, CET); 40 | 41 | // United Kingdom (London, Belfast) 42 | TimeChangeRule BST = {"BST", Last, Sun, Mar, 1, 60}; // British Summer Time 43 | TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0}; // Standard Time 44 | Timezone UK(BST, GMT); 45 | 46 | // UTC 47 | TimeChangeRule utcRule = {"UTC", Last, Sun, Mar, 1, 0}; // UTC 48 | Timezone UTC(utcRule); 49 | 50 | // US Eastern Time Zone (New York, Detroit) 51 | TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; // Eastern Daylight Time = UTC - 4 hours 52 | TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; // Eastern Standard Time = UTC - 5 hours 53 | Timezone usET(usEDT, usEST); 54 | 55 | // US Central Time Zone (Chicago, Houston) 56 | TimeChangeRule usCDT = {"CDT", Second, Sun, Mar, 2, -300}; 57 | TimeChangeRule usCST = {"CST", First, Sun, Nov, 2, -360}; 58 | Timezone usCT(usCDT, usCST); 59 | 60 | // US Mountain Time Zone (Denver, Salt Lake City) 61 | TimeChangeRule usMDT = {"MDT", Second, Sun, Mar, 2, -360}; 62 | TimeChangeRule usMST = {"MST", First, Sun, Nov, 2, -420}; 63 | Timezone usMT(usMDT, usMST); 64 | 65 | // Arizona is US Mountain Time Zone but does not use DST 66 | Timezone usAZ(usMST); 67 | 68 | // US Pacific Time Zone (Las Vegas, Los Angeles) 69 | TimeChangeRule usPDT = {"PDT", Second, Sun, Mar, 2, -420}; 70 | TimeChangeRule usPST = {"PST", First, Sun, Nov, 2, -480}; 71 | Timezone usPT(usPDT, usPST); 72 | 73 | // build index of all timezones 74 | Timezone * timezones[] = {&ausET, &MSK, &CE, &UK, &UTC, &usET, &usCT, &usMT, &usAZ, &usPT, &TRT}; 75 | 76 | void ICACHE_FLASH_ATTR NtpClient::Ntp(const char * server, time_t syncMins, uint8_t tz_index) { 77 | TimeServerName = strdup(server); 78 | syncInterval = syncMins * 60; // convert to seconds 79 | 80 | // check for out of bounds 81 | if (tz_index >= NTP_TIMEZONE_MAX) { 82 | tz_index = NTP_TIMEZONE_DEFAULT; 83 | } 84 | tz = timezones[tz_index]; // set timezone 85 | 86 | WiFi.hostByName(TimeServerName, timeServer); 87 | setSyncProvider(getNtpTime); 88 | setSyncInterval(syncInterval); 89 | } 90 | 91 | ICACHE_FLASH_ATTR NtpClient::~NtpClient() { 92 | udpListener.close(); 93 | } 94 | 95 | // send an NTP request to the time server at the given address 96 | time_t ICACHE_FLASH_ATTR NtpClient::getNtpTime() { 97 | memset(NTPpacket, 0, sizeof(NTPpacket)); 98 | NTPpacket[0] = 0b11100011; 99 | NTPpacket[1] = 0; 100 | NTPpacket[2] = 6; 101 | NTPpacket[3] = 0xEC; 102 | NTPpacket[12] = 49; 103 | NTPpacket[13] = 0x4E; 104 | NTPpacket[14] = 49; 105 | NTPpacket[15] = 52; 106 | if (udpListener.connect(timeServer, 123)) { 107 | udpListener.onPacket([](AsyncUDPPacket packet) { 108 | unsigned long highWord = word(packet.data()[40], packet.data()[41]); 109 | unsigned long lowWord = word(packet.data()[42], packet.data()[43]); 110 | time_t UnixUTCtime = (highWord << 16 | lowWord) - 2208988800UL; 111 | time_t adjustedtime = (*tz).toLocal(UnixUTCtime, &tcr); 112 | 113 | myESP.myDebug_P(PSTR("[NTP] Internet time: %02d:%02d:%02d UTC on %d/%d. Local time: %02d:%02d:%02d %s"), 114 | to_hour(UnixUTCtime), 115 | to_minute(UnixUTCtime), 116 | to_second(UnixUTCtime), 117 | to_day(UnixUTCtime), 118 | to_month(UnixUTCtime), 119 | to_hour(adjustedtime), 120 | to_minute(adjustedtime), 121 | to_second(adjustedtime), 122 | tcr->abbrev); 123 | 124 | setTime(adjustedtime); 125 | }); 126 | } 127 | udpListener.write(NTPpacket, sizeof(NTPpacket)); 128 | return 0; 129 | } 130 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ### Functions 5 | 6 | is_git() { 7 | command -v git >/dev/null 2>&1 || return 1 8 | command git rev-parse >/dev/null 2>&1 || return 1 9 | 10 | return 0 11 | } 12 | 13 | stat_bytes() { 14 | filesize=`du -k "$1" | cut -f1;` 15 | echo 'size:' $filesize 'bytes' 16 | } 17 | 18 | # Available environments 19 | list_envs() { 20 | grep env: platformio.ini | sed 's/\[env:\(.*\)\]/\1/g' 21 | } 22 | 23 | print_available() { 24 | echo "--------------------------------------------------------------" 25 | echo "Available environments:" 26 | for environment in $available; do 27 | echo "-> $environment" 28 | done 29 | } 30 | 31 | print_environments() { 32 | echo "--------------------------------------------------------------" 33 | echo "Current environments:" 34 | for environment in $environments; do 35 | echo "-> $environment" 36 | done 37 | } 38 | 39 | set_default_environments() { 40 | # Hook to build in parallel when using travis 41 | if [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Release" ]] && ${par_build}; then 42 | environments=$(echo ${available} | \ 43 | awk -v par_thread=${par_thread} -v par_total_threads=${par_total_threads} \ 44 | '{ for (i = 1; i <= NF; i++) if (++j % par_total_threads == par_thread ) print $i; }') 45 | return 46 | fi 47 | 48 | # Only build travis target 49 | if [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]]; then 50 | environments=$travis 51 | return 52 | fi 53 | 54 | # Fallback to all available environments 55 | environments=$available 56 | } 57 | 58 | build_webui() { 59 | cd ./tools/webfilesbuilder 60 | 61 | # Build system uses gulpscript.js to build web interface 62 | if [ ! -e node_modules/gulp/bin/gulp.js ]; then 63 | echo "--------------------------------------------------------------" 64 | echo "Installing dependencies..." 65 | npm ci 66 | fi 67 | 68 | # Recreate web interface - "node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd ./tools/webfilesbuilder" 69 | echo "--------------------------------------------------------------" 70 | echo "Building web interface..." 71 | node node_modules/gulp/bin/gulp.js || exit 72 | 73 | cd ../.. 74 | } 75 | 76 | build_environments() { 77 | echo "--------------------------------------------------------------" 78 | echo "Building firmware images..." 79 | # don't move to firmware folder until Travis fixed (see https://github.com/travis-ci/dpl/issues/846#issuecomment-547157406) 80 | # mkdir -p $destination 81 | 82 | for environment in $environments; do 83 | echo "* IRT-ESP-$version-$environment.bin" 84 | platformio run --silent --environment $environment || exit 1 85 | stat_bytes .pio/build/$environment/firmware.bin 86 | # mv .pio/build/$environment/firmware.bin $destination/EMS-ESP-$version-$environment.bin 87 | # mv .pio/build/$environment/firmware.bin EMS-ESP-$version-$environment.bin 88 | mv .pio/build/$environment/firmware.bin IRT-ESP-dev-$environment.bin 89 | done 90 | echo "--------------------------------------------------------------" 91 | } 92 | 93 | 94 | ####### MAIN 95 | 96 | destination=firmware 97 | version_file=./src/version.h 98 | version=$(grep -E '^#define APP_VERSION' $version_file | awk '{print $3}' | sed 's/"//g') 99 | 100 | if ${TRAVIS:-false}; then 101 | git_revision=${TRAVIS_COMMIT::7} 102 | git_tag=${TRAVIS_TAG} 103 | elif is_git; then 104 | git_revision=$(git rev-parse --short HEAD) 105 | git_tag=$(git tag --contains HEAD) 106 | else 107 | git_revision=unknown 108 | git_tag= 109 | fi 110 | 111 | echo $git_tag 112 | 113 | if [[ -n $git_tag ]]; then 114 | new_version=${version/-*} 115 | sed -i -e "s@$version@$new_version@" $version_file 116 | version=$new_version 117 | trap "git checkout -- $version_file" EXIT 118 | fi 119 | 120 | par_build=false 121 | par_thread=${BUILDER_THREAD:-0} 122 | par_total_threads=${BUILDER_TOTAL_THREADS:-4} 123 | if [ ${par_thread} -ne ${par_thread} -o \ 124 | ${par_total_threads} -ne ${par_total_threads} ]; then 125 | echo "Parallel threads should be a number." 126 | exit 127 | fi 128 | if [ ${par_thread} -ge ${par_total_threads} ]; then 129 | echo "Current thread is greater than total threads. Doesn't make sense" 130 | exit 131 | fi 132 | 133 | # travis platformio target is used for nightly Test 134 | travis=$(list_envs | grep travis | sort) 135 | 136 | # get all taregts, excluding travis and debug 137 | available=$(list_envs | grep -Ev -- 'travis|debug|release' | sort) 138 | 139 | export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS}" 140 | 141 | # get command line Parameters 142 | # l prints environments 143 | # 2 does parallel builds 144 | # d uses next arg as destination folder 145 | while getopts "lpd:" opt; do 146 | case $opt in 147 | l) 148 | print_available 149 | exit 150 | ;; 151 | p) 152 | par_build=true 153 | ;; 154 | d) 155 | destination=$OPTARG 156 | ;; 157 | esac 158 | done 159 | 160 | shift $((OPTIND-1)) 161 | 162 | # Welcome message 163 | echo "--------------------------------------------------------------" 164 | echo "IRT-ESP FIRMWARE BUILDER" 165 | echo "Building for version ${version}" ${git_revision:+($git_revision)} 166 | 167 | # Environments to build 168 | environments=$@ 169 | 170 | if [ $# -eq 0 ]; then 171 | set_default_environments 172 | fi 173 | 174 | if ${CI:-false}; then 175 | print_environments 176 | fi 177 | 178 | # for debugging 179 | echo "* git_revision = $git_revision" 180 | echo "* git_tag = $git_tag" 181 | echo "* TRAVIS_COMMIT = $TRAVIS_COMMIT" 182 | echo "* TRAVIS_TAG = $TRAVIS_TAG" 183 | echo "* TRAVIS_BRANCH = $TRAVIS_BRANCH" 184 | echo "* TRAVIS_BUILD_STAGE_NAME = $TRAVIS_BUILD_STAGE_NAME" 185 | 186 | build_webui 187 | build_environments 188 | -------------------------------------------------------------------------------- /src/TimeLib.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * customized version of Time library, originally Copyright (c) Michael Margolis 2009-2014 3 | * modified by Paul S https://github.com/PaulStoffregen/Time 4 | */ 5 | 6 | #include "TimeLib.h" 7 | 8 | static tmElements_t tm; // a cache of time elements 9 | static time_t cacheTime; // the time the cache was updated 10 | static uint32_t syncInterval = 300; // time sync will be attempted after this many seconds 11 | static uint32_t sysTime = 0; 12 | static uint32_t prevMillis = 0; 13 | static uint32_t nextSyncTime = 0; 14 | static timeStatus_t Status = timeNotSet; 15 | getExternalTime getTimePtr; // pointer to external sync function 16 | 17 | #define LEAP_YEAR(Y) (((1970 + (Y)) > 0) && !((1970 + (Y)) % 4) && (((1970 + (Y)) % 100) || !((1970 + (Y)) % 400))) 18 | static const uint8_t monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // API starts months from 1, this array starts from 0 19 | 20 | time_t now() { 21 | // calculate number of seconds passed since last call to now() 22 | while (millis() - prevMillis >= 1000) { 23 | // millis() and prevMillis are both unsigned ints thus the subtraction will always be the absolute value of the difference 24 | sysTime++; 25 | prevMillis += 1000; 26 | } 27 | if (nextSyncTime <= sysTime) { 28 | if (getTimePtr != 0) { 29 | time_t t = getTimePtr(); 30 | if (t != 0) { 31 | setTime(t); 32 | } else { 33 | nextSyncTime = sysTime + syncInterval; 34 | Status = (Status == timeNotSet) ? timeNotSet : timeNeedsSync; 35 | } 36 | } 37 | } 38 | return (time_t)sysTime; 39 | } 40 | 41 | void setSyncProvider(getExternalTime getTimeFunction) { 42 | getTimePtr = getTimeFunction; 43 | nextSyncTime = sysTime; 44 | now(); // this will sync the clock 45 | } 46 | 47 | void setSyncInterval(time_t interval) { // set the number of seconds between re-sync 48 | syncInterval = (uint32_t)interval; 49 | nextSyncTime = sysTime + syncInterval; 50 | } 51 | 52 | void breakTime(time_t timeInput, tmElements_t & tm) { 53 | // break the given time_t into time components 54 | // this is a more compact version of the C library localtime function 55 | // note that year is offset from 1970 !!! 56 | 57 | uint8_t year; 58 | uint8_t month, monthLength; 59 | uint32_t time; 60 | unsigned long days; 61 | 62 | time = (uint32_t)timeInput; 63 | tm.Second = time % 60; 64 | time /= 60; // now it is minutes 65 | tm.Minute = time % 60; 66 | time /= 60; // now it is hours 67 | tm.Hour = time % 24; 68 | time /= 24; // now it is days 69 | tm.Wday = ((time + 4) % 7) + 1; // Sunday is day 1 70 | 71 | year = 0; 72 | days = 0; 73 | while ((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) { 74 | year++; 75 | } 76 | tm.Year = year; // year is offset from 1970 77 | 78 | days -= LEAP_YEAR(year) ? 366 : 365; 79 | time -= days; // now it is days in this year, starting at 0 80 | 81 | month = 0; 82 | monthLength = 0; 83 | for (month = 0; month < 12; month++) { 84 | if (month == 1) { // february 85 | if (LEAP_YEAR(year)) { 86 | monthLength = 29; 87 | } else { 88 | monthLength = 28; 89 | } 90 | } else { 91 | monthLength = monthDays[month]; 92 | } 93 | 94 | if (time >= monthLength) { 95 | time -= monthLength; 96 | } else { 97 | break; 98 | } 99 | } 100 | tm.Month = month + 1; // jan is month 1 101 | tm.Day = time + 1; // day of month 102 | } 103 | 104 | // assemble time elements into time_t 105 | time_t makeTime(const tmElements_t & tm) { 106 | int i; 107 | uint32_t seconds; 108 | 109 | // seconds from 1970 till 1 jan 00:00:00 of the given year 110 | seconds = tm.Year * (SECS_PER_DAY * 365); 111 | for (i = 0; i < tm.Year; i++) { 112 | if (LEAP_YEAR(i)) { 113 | seconds += SECS_PER_DAY; // add extra days for leap years 114 | } 115 | } 116 | 117 | // add days for this year, months start from 1 118 | for (i = 1; i < tm.Month; i++) { 119 | if ((i == 2) && LEAP_YEAR(tm.Year)) { 120 | seconds += SECS_PER_DAY * 29; 121 | } else { 122 | seconds += SECS_PER_DAY * monthDays[i - 1]; // monthDay array starts from 0 123 | } 124 | } 125 | seconds += (tm.Day - 1) * SECS_PER_DAY; 126 | seconds += tm.Hour * SECS_PER_HOUR; 127 | seconds += tm.Minute * SECS_PER_MIN; 128 | seconds += tm.Second; 129 | return (time_t)seconds; 130 | } 131 | 132 | void refreshCache(time_t t) { 133 | if (t != cacheTime) { 134 | breakTime(t, tm); 135 | cacheTime = t; 136 | } 137 | } 138 | 139 | uint8_t to_second(time_t t) { // the second for the given time 140 | refreshCache(t); 141 | return tm.Second; 142 | } 143 | 144 | uint8_t to_minute(time_t t) { // the minute for the given time 145 | refreshCache(t); 146 | return tm.Minute; 147 | } 148 | 149 | uint8_t to_hour(time_t t) { // the hour for the given time 150 | refreshCache(t); 151 | return tm.Hour; 152 | } 153 | 154 | uint8_t to_day(time_t t) { // the day for the given time (0-6) 155 | refreshCache(t); 156 | return tm.Day; 157 | } 158 | 159 | uint8_t to_month(time_t t) { // the month for the given time 160 | refreshCache(t); 161 | return tm.Month; 162 | } 163 | 164 | uint8_t to_weekday(time_t t) { 165 | refreshCache(t); 166 | return tm.Wday; 167 | } 168 | 169 | uint16_t to_year(time_t t) { // the year for the given time 170 | refreshCache(t); 171 | return tm.Year + 1970; 172 | } 173 | 174 | void setTime(time_t t) { 175 | sysTime = (uint32_t)t; 176 | nextSyncTime = (uint32_t)t + syncInterval; 177 | Status = timeSet; 178 | prevMillis = millis(); // restart counting from now (thanks to Korman for this fix) 179 | } 180 | -------------------------------------------------------------------------------- /tools/webfilesbuilder/gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | EMS-ESP web server file system builder 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | /*eslint quotes: ['error', 'single']*/ 19 | /*eslint-env es6*/ 20 | 21 | const gulp = require('gulp'); 22 | const fs = require('fs'); 23 | const concat = require('gulp-concat'); 24 | const gzip = require('gulp-gzip'); 25 | const flatmap = require('gulp-flatmap'); 26 | const path = require('path'); 27 | const htmlmin = require('gulp-htmlmin'); 28 | const uglify = require('gulp-uglify'); 29 | const pump = require('pump'); 30 | const through = require('through2'); 31 | 32 | // file name includes extension 33 | var buildHeader = function (name) { 34 | 35 | return through.obj(function (source, encoding, callback) { 36 | 37 | var parts = source.path.split(path.sep); 38 | var filename = parts[parts.length - 1]; 39 | var extension = filename.split('.')[1]; 40 | 41 | console.info('Creating file: ' + filename); 42 | 43 | // var safename = name.split('.').join('_'); 44 | var safename = name.replace(/\.|-/g, "_"); 45 | 46 | var destination = "../../src/webh/" + filename + ".h"; 47 | 48 | // check for woff files which should be fonts 49 | if (extension === "woff") { 50 | extension = "fonts"; 51 | } 52 | 53 | // html files go into root 54 | if (extension === "html") { 55 | var source = "../../src/websrc/temp/gzipped/" + name + ".gz"; 56 | } else { 57 | var source = "../../src/websrc/temp/gzipped/" + extension + "/" + name + ".gz"; 58 | } 59 | 60 | var wstream = fs.createWriteStream(destination); 61 | wstream.on('error', function (err) { 62 | console.log(err); 63 | }); 64 | 65 | var data = fs.readFileSync(source); 66 | 67 | wstream.write('#define ' + safename + '_gz_len ' + data.length + '\n'); 68 | wstream.write('const uint8_t ' + safename + '_gz[] PROGMEM = {'); 69 | 70 | for (i = 0; i < data.length; i++) { 71 | if (i % 1000 == 0) wstream.write("\n"); 72 | wstream.write('0x' + ('00' + data[i].toString(16)).slice(-2)); 73 | if (i < data.length - 1) wstream.write(','); 74 | } 75 | 76 | wstream.write('\n};') 77 | wstream.end(); 78 | 79 | callback(null, destination); 80 | 81 | }); 82 | }; 83 | 84 | gulp.task('myespjs', function () { 85 | return gulp.src(['../../src/websrc/myesp.js', '../../src/custom.js']) 86 | .pipe(concat({ 87 | path: 'myesp.js', 88 | stat: { 89 | mode: 0666 90 | } 91 | })) 92 | .pipe(gulp.dest('../../src/websrc/temp/js')) 93 | .pipe(uglify()) 94 | .pipe(gulp.dest('../../src/websrc/temp/js/ugly')) 95 | .pipe(gzip({ 96 | append: true 97 | })) 98 | .pipe(gulp.dest('../../src/websrc/temp/gzipped/js')) 99 | .pipe(buildHeader('myesp.js')); 100 | }); 101 | 102 | gulp.task('requiredjs', function () { 103 | return gulp.src(['../../src/websrc/3rdparty/js/jquery-1.12.4.min.js', '../../src/websrc/3rdparty/js/bootstrap-3.3.7.min.js', '../../src/websrc/3rdparty/js/footable-3.1.6.min.js']) 104 | .pipe(concat({ 105 | path: 'required.js', 106 | stat: { 107 | mode: 0666 108 | } 109 | })) 110 | .pipe(gulp.dest('../../src/websrc/temp/js/')) 111 | .pipe(gzip({ 112 | append: true 113 | })) 114 | .pipe(gulp.dest('../../src/websrc/temp/gzipped/js/')) 115 | .pipe(buildHeader('required.js')); 116 | }); 117 | 118 | 119 | gulp.task('requiredcss', function () { 120 | return gulp.src(['../../src/websrc/3rdparty/css/bootstrap-3.3.7.min.css', '../../src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css', '../../src/websrc/3rdparty/css/sidebar.css']) 121 | .pipe(concat({ 122 | path: 'required.css', 123 | stat: { 124 | mode: 0666 125 | } 126 | })) 127 | .pipe(gulp.dest('../../src/websrc/temp/css/')) 128 | .pipe(gzip({ 129 | append: true 130 | })) 131 | .pipe(gulp.dest('../../src/websrc/temp/gzipped/css/')) 132 | .pipe(buildHeader('required.css')); 133 | }); 134 | 135 | gulp.task("fontwoff", function () { 136 | return gulp.src("../../src/websrc/3rdparty/fonts/*.*") 137 | .pipe(gulp.dest("../../src/websrc/temp/fonts/")) 138 | .pipe(gzip({ 139 | append: true 140 | })) 141 | .pipe(gulp.dest('../../src/websrc/temp/gzipped/fonts/')) 142 | .pipe(buildHeader('glyphicons-halflings-regular.woff')); 143 | }); 144 | 145 | gulp.task('myesphtml', function () { 146 | return gulp.src(['../../src/websrc/myesp.htm', '../../src/custom.htm']) 147 | .pipe(concat({ 148 | path: 'myesp.html', 149 | stat: { 150 | mode: 0666 151 | } 152 | })) 153 | .pipe(htmlmin({ collapseWhitespace: true, minifyJS: true })) 154 | .pipe(gulp.dest('../../src/websrc/temp/')) 155 | .pipe(gzip({ 156 | append: true 157 | })) 158 | .pipe(gulp.dest('../../src/websrc/temp/gzipped/')) 159 | .pipe(buildHeader('myesp.html')); 160 | 161 | }); 162 | 163 | gulp.task('indexhtml', function () { 164 | return gulp.src('../../src/websrc/index.html') 165 | .pipe(htmlmin({ collapseWhitespace: true, minifyJS: true })) 166 | .pipe(gulp.dest('../../src/websrc/temp/')) 167 | .pipe(gzip({ 168 | append: true 169 | })) 170 | .pipe(gulp.dest('../../src/websrc/temp/gzipped/')) 171 | .pipe(buildHeader('index.html')); 172 | }); 173 | 174 | gulp.task('default', gulp.parallel('myespjs', 'requiredjs', 'requiredcss', 'fontwoff', 'myesphtml', 'indexhtml')); 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IRT-ESP 2 | IRT-ESP is a project to build an electronic controller circuit using an Espressif ESP8266 micro controller to communicate with iRT based Boilers from the Nefit Ecomline HR Classic range and compatibles such as Buderus. 3 | 4 | This project is a fork of the [Proddy EMS-ESP](https://github.com/proddy/EMS-ESP) project. 5 | 6 | It can operate in two modes: active and passive. In passive mode it just decodes iRT messages and report the status via MQTT or the build-in web interface. In active mode it can maintain a set water temperature, but this is a work in progress. Currently the only supported hardware is a modified [EMS Bus gateway](https://bbqkees-electronics.nl/) from BBQKees. Without the hardware modification it will not work ! 7 | 8 | For more information about the iRT protocol have a look at the [wiki](https://github.com/Victor-Mo/IRT-EMS-ESP/wiki). 9 | 10 | ## Supported features 11 | In passive mode the firmware can decode the water temperature, if the boiler is running, what mode: heating or warm water, and if the pump is running. 12 | 13 | In the (very experimental) active mode it will start the burner and sets a max water temp. The burner power is a fixed value depended on the water temperature you set, '`set_water 35`' will set a maximum water temperature of 35 degrees Celsius and a very low burner power. '`set_water 90`' will run the boiler at full power and a maximum water temperature of 90 degrees Celsius. 14 | 15 | Please keep in mind this software is highly experimental. It works on my boiler, it may not work on yours. The active mode can seriously damage your boiler, for example running it at full power for extended time. Always monitor your boiler when running in active mode ! 16 | 17 | But if it does not work, create a log file (`log j`), create a ticket, and I will see what I can do. 18 | 19 | ## Building the software 20 | 21 | For building the software, have a look at the [EMS-ESP wiki](https://emsesp.github.io/docs/#/Building-firmware). 22 | 23 | ## First start, passive mode 24 | Because the software is kept compatible with the EMS-ESP, the TX-Mode needs to be set on the first run. Telnet to the device and issue the following commands: 25 | 26 | `set tx_mode 4` 27 | 28 | `restart` 29 | 30 | The device will now restart, if the device is back. Reconnect and enable logging (`log j`). The output should look like this: 31 | ``` 32 | log j 33 | 34 | System Logging set to Jabber mode 35 | (00:56:43.985) irt_parseTelegram: 00 05 35: 01 01 FE 90 90 E6 E6 D0 D0 2E 2E CF 30 82 82 6B 6B B8 B8 86 86 00 FF A3 A3 07 07 D3 D3 CA CA 03 FC A4 A4 7D 7D AC AC DC DC 2A D5 8A 8A A1 A1 98 98 83 83 FE 01 36 | (00:56:48.985) irt_parseTelegram: 00 05 35: 01 01 FE 90 90 40 40 5A 5A 61 61 CF 30 93 93 73 73 0E 0E 4D 4D FF 00 C9 C9 C3 C3 79 79 AB AB 00 FF F0 F0 01 01 CD CD ED ED 00 FF F0 F0 01 01 D8 D8 B9 B9 05 FA 37 | (00:56:54.025) irt_parseTelegram: 00 05 35: 01 01 FE 90 90 C3 C3 79 79 F2 F2 CF 30 82 82 C3 C3 79 79 E0 E0 00 FF A3 A3 C3 C3 79 79 C1 C1 03 FC A4 A4 C3 C3 79 79 C6 C6 2A D5 8A 8A C3 C3 79 79 E8 E8 FE 01 38 | (00:56:59.065) irt_parseTelegram: 00 05 35: 01 01 FE 90 90 C3 C3 79 79 F2 F2 CF 30 81 81 C3 C3 79 79 E3 E3 51 AE 86 86 C3 C3 79 79 E4 E4 E9 16 85 85 C3 C3 79 79 E7 E7 74 8B 83 83 C3 C3 79 79 E1 E1 A0 5F 39 | (00:57:03.625) irt_parseTelegram: 00 05 35: 01 01 FE 90 90 C3 C3 79 79 F2 F2 CF 30 82 82 C3 C3 79 79 E0 E0 00 FF A3 A3 C3 C3 79 79 C1 C1 03 FC A4 A4 C3 C3 79 79 C6 C6 2A D5 8A 8A C3 C3 79 79 E8 E8 FE 01 40 | ``` 41 | ## First start, active mode 42 | In order to work in active mode the IRT-EMS-ESP should be the only device on the bus ! Telnet to the device and issue the following commands: 43 | 44 | `set tx_mode 5` 45 | 46 | `restart` 47 | 48 | The device will now restart, if the device is back. Reconnect and enable logging (`log j`). The output should look like this: 49 | ``` 50 | log j 51 | 52 | System Logging set to Jabber mode 53 | (00:00:14.769) irt_rawTelegram: 00 05 01: 00 - "." 54 | (00:00:14.869) irt_rawTelegram: 00 05 01: 01 - "." 55 | (00:00:14.990) irt_rawTelegram: 00 05 01: 02 - "." 56 | (00:00:15.110) irt_rawTelegram: 00 05 01: 03 - "." 57 | (00:00:15.230) irt_rawTelegram: 00 05 01: 00 - "." 58 | (00:00:15.288) irt_tx: 01 05 1E: 90 A5 F0 28 00 00 82 A5 F0 3A 00 00 A3 A5 F0 1B 00 00 A4 A5 F0 1C 00 00 8A A5 F0 32 00 00 59 | (00:00:15.330) irt_rawTelegram: 00 05 01: 01 - "." 60 | (00:00:15.450) irt_rawTelegram: 00 05 01: 02 - "." 61 | (00:00:15.570) irt_rawTelegram: 00 05 01: 03 - "." 62 | (00:00:15.670) irt_rawTelegram: 00 05 01: 00 - "." 63 | set(00:00:16.571) irt_rawTelegram: 00 05 35: 01 01 FE 90 90 A5 A5 F0 F0 28 28 67 98 82 82 A5 A5 F0 F0 3A 3A 00 FF A3 A3 A5 A5 F0 F0 1B 1B 03 FC A4 A4 A5 A5 F0 F0 1C 1C 20 DF 8A 8A A5 A5 F0 F0 32 32 FE 01 64 | | 81:35 82:00 83:A0 85:74 86:E9 8A:FE 90:67 A3:03 A4:20 0H 65 | 66 | ``` 67 | 68 | Set the desired water temerature (`set_water 30`) and change the logging to only show the status line (`log s`): 69 | 70 | ``` 71 | log s 72 | 73 | System Logging set to Solar Module only 74 | 01:35 04:00 05:04 07:00 11:FF 14:00 15:04 17:FF 73:52 78:05 | 81:35 82:00 83:A0 85:74 86:E9 8A:FE 90:67 93:00 A3:03 A4:1F A6:1E C9:05 E8:05 ED:00 0H 75 | Starting scheduled query from EMS devices 76 | 01:35 04:00 05:04 07:00 11:FF 14:00 15:04 17:FF 73:52 78:01 | 81:35 82:00 83:A0 85:74 86:E9 8A:FE 90:67 93:00 A3:03 A4:1F A6:1E C9:05 E8:05 ED:00 0H 77 | 78 | set_water 30 79 | 80 | Irt set water temp, wc 1 81 | 82 | Water temp set to 30 (1e) 83 | 01:35 04:00 05:04 07:69 11:FF 14:00 15:04 17:FF 73:52 78:07 | 81:35 82:00 83:A0 85:74 86:E9 8A:FE 90:67 93:00 A3:03 A4:1F A6:1E C9:05 E8:05 ED:00 0H 84 | 01:23 04:00 05:04 07:75 11:FF 14:00 15:04 17:FF 73:52 78:05 | 81:35 82:84 83:00 85:04 86:E9 8A:FE 90:67 93:00 A3:53 A4:24 A6:1D C9:02 E8:05 ED:00 -H 85 | 01:23 04:00 05:04 07:75 11:FF 14:00 15:04 17:FF 73:52 78:05 | 81:35 82:84 83:00 85:04 86:E9 8A:FE 90:67 93:00 A3:53 A4:24 A6:1D C9:01 E8:05 ED:00 -H 86 | 87 | ``` 88 | 89 | 90 | ## Hardware change needed 91 | 92 | The software runs on a modified [EMS Bus Wi-Fi Gateway](https://bbqkees-electronics.nl/product/gateway-premium-ii/). In short; Remove the 100 KOhm resistor (R5) to ground en replace it with a 1.5 MOhm resistor to 5 Volt. **Without the modification it will not work !** 93 | 94 | The 1.5 MOhm resistor works for my setup where the Thermostat is 5 meters a way from the boiler. This value may have to be adapted for different distances. 95 | 96 | I adapted BBQKees's schematic to show the change: ![Modified Schematic](doc/schematics/IRT-V09_schema.png) 97 | 98 | BBQKees also sells an already modified [interface board](https://bbqkees-electronics.nl/product/irt-interface-board-experimental/). 99 | -------------------------------------------------------------------------------- /src/test_data.h: -------------------------------------------------------------------------------- 1 | 2 | #ifdef TESTS 3 | 4 | static const char * TEST_DATA[] = { 5 | 6 | "08 00 34 00 3E 02 1D 80 00 31 00 00 01 00 01 0B AE 02", // test 1 - EMS 7 | "10 00 FF 00 01 A5 80 00 01 30 28 00 30 28 01 54 03 03 01 01 54 02 A8 00 00 11 01 03 FF FF 00", // test 2 - RC310 ems+ 8 | "10 00 FF 19 01 A5 06 04 00 00 00 00 FF 64 37 00 3C 01 FF 01", // test 3 - RC310 ems+ 9 | "30 00 FF 00 02 62 00 A1 01 3F 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00", // test 4 - SM100 10 | "10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00", // test 5 - Moduline 1010 11 | "18 00 FF 00 01 A5 00 DD 21 23 00 00 23 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00", // test 6 - RC300 12 | "90 00 FF 00 00 6F 01 01 00 46 00 B9", // test 7 - FR10 13 | "30 00 FF 00 02 62 01 FB 01 9E 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 2B", // test 8 - SM100 14 | "30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0C 20 64 00 00 00 00 E9", // test 9 - SM100 15 | "30 09 FF 00 00 01", // test 10 - EMS+ 16 | "30 0B 97 00", // test 11 - SM100 17 | "30 00 FF 00 02 62 01 CA", // test 12 - SM100 18 | "30 00 FF 00 02 8E 00 00 00 00 00 00 05 19 00 00 75 D3", // test 13 - SM100 19 | "30 00 FF 00 02 63 80 00 80 00 00 00 80 00 80 00 80 00 00", // test 14 - SM100 20 | "30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0B 09 64 00 00 00 00", // test 15 - SM100 21 | "30 00 FF 00 02 62 01 CA 01 93 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00", // test 16 - SM100 22 | "30 00 FF 00 02 6A 03 03 03 00 03 03 03 03 03 00 03 03", // test 17 - SM100 23 | "30 00 FF 00 02 6A 03 03 03 00 03 03 03 03 03 00 04 03", // test 18 - SM100 24 | "30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 09 08 64 00 00 00 00", // test 19 - SM100 25 | "10 00 FF 07 01 A5 32", // test 20 - RC EMS+ 26 | "38 10 FF 00 03 2B 00 D1 08 2A 01", // test 21 - heatpump 27 | "38 10 FF 00 03 7B 08 24 00 4B", // test 22 - heatpump 28 | "08 00 FF 31 03 94 00 00 00 00 00 00 00", // test 23 - heatpump 29 | "08 00 FF 00 03 95 00 6D C5 0E 00 05 BA 7C 00 68 0A 92 00 00 00 00 00 00 00 00 00 00 00 CD", // test 24 - heatpump 30 | "08 00 FF 48 03 95 00 00 01 47 00 00 00 00 00 00 00 00", // test 25 - heatpump 31 | "08 00 FF 00 03 A2 10 01 02 02 00", // test 26 - heatpump 32 | "08 00 FF 00 03 A3 00 0B 00 00 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 00 08 00 00 00 0A", // test 27 - heatpump 33 | "30 00 FF 0A 02 6A 04", // test 28 - SM100 pump on 34 | "30 00 FF 0A 02 6A 03", // test 29 - SM100 pump off 35 | "48 90 02 00 01", // test 30 - version test 36 | "10 48 02 00 9E", // test 31 - version test 37 | "30 00 FF 00 02 8E 00 00 0C F3 00 00 06 02 00 00 76 33", // test 32 - SM100 energy 38 | "30 00 FF 00 02 8E 00 00 07 9E 00 00 06 C5 00 00 76 35", // test 33 - SM100 energy 39 | "30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35", // test 34 - SM100 energy 40 | "10 48 F9 00 FF 01 6C 08 4F 00 00 00 02 00 00 00 02 00 03 00 03 00 03 00 02", // test 35 - F9 41 | "48 90 F9 00 11 FF 01 6D 08", // test 36 - F9 42 | "10 48 F9 00 FF 01 6D 08", // test 37 - F9 43 | "10 00 F7 00 FF 01 B9 35 19", // test 38 - F7 44 | "30 00 FF 00 02 62 00 E7 01 AE 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00", // test 39 - SM100 45 | "30 00 FF 00 02 62 00 E4", // test 40 - SM100 46 | "10 48 F7 00 FF 01 A5 DF FF F7 7F 1F", // test 41 - gateway 47 | "30 00 FF 09 02 64 1E", // test 42 - SM100 48 | "08 00 18 00 05 03 30 00 00 00 00 04 40 80 00 02 17 80 00 00 00 FF 30 48 00 CB 00 00 00", // test 43 - sys pressure 49 | "90 00 FF 00 00 6F 03 01 00 BE 00 BF", // test 44 - FR10 50 | "08 00 E3 00 01 00 01 00 00 00 00 00 00 00 00 00 DF 00 64 55", // test 45 - heatpump Enviline 51 | "08 00 E5 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0A", // test 46 - heatpump Enviline 52 | "38 10 FF 00 03 2B 00 C7 07 C3 01", // test 47 - heatpump Enviline 53 | "08 0B 19 00 00 F7 80 00 80 00 00 00 00 00 03 58 97 0C 7B 1F 00 00 00 06 C4 DF 02 64 48 80 00", // test 48 - outdoor temp check 54 | "88 00 19 00 00 DC 80 00 80 00 FF FF 00 00 00 21 9A 06 E1 7C 00 00 00 06 C2 13 00 1E 90 80 00", // test 49 - check max length 55 | "30 00 FF 00 02 8E 00 00 41 82 00 00 28 36 00 00 82 21", // test 50 - SM100 56 | "10 00 FF 08 01 B9 26", // test 51 - EMS+ 0x1B9 set temp 57 | "10 00 F7 00 FF 01 B9 21 E9", // test 52 - EMS+ 0x1B9 F7 test 58 | "08 00 D1 00 00 80" // test 53 - outdoor temp 59 | 60 | 61 | }; 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/ds18.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dallas support for external settings 3 | * Copied from Espurna - Copyright (C) 2017-2018 by Xose Pérez 4 | * 5 | * Paul Derbyshire - https://github.com/proddy/EMS-ESP 6 | * 7 | */ 8 | 9 | #include "ds18.h" 10 | 11 | std::vector _devices; 12 | 13 | DS18::DS18() { 14 | _wire = nullptr; 15 | _count = 0; 16 | _gpio = GPIO_NONE; 17 | _parasite = 0; 18 | } 19 | 20 | DS18::~DS18() { 21 | if (_wire) { 22 | delete _wire; 23 | } 24 | } 25 | 26 | // init 27 | void DS18::setup(uint8_t gpio, bool parasite) { 28 | _gpio = gpio; 29 | _parasite = (parasite ? 1 : 0); 30 | 31 | // OneWire 32 | if (_wire) { 33 | delete _wire; 34 | } 35 | _wire = new OneWire(_gpio); 36 | } 37 | 38 | // clear list and scan for devices 39 | uint8_t DS18::scan() { 40 | _devices.clear(); 41 | 42 | uint8_t count = loadDevices(); // start the search 43 | 44 | // If no devices found check again pulling up the line 45 | if (count == 0) { 46 | pinMode(_gpio, INPUT_PULLUP); 47 | count = loadDevices(); 48 | } 49 | 50 | _count = count; 51 | 52 | return count; 53 | } 54 | 55 | void DS18::loop() { 56 | // we either start a conversion or read the scratchpad 57 | static bool conversion = true; 58 | if (conversion) { 59 | _wire->reset(); 60 | _wire->skip(); 61 | _wire->write(DS18_CMD_START_CONVERSION, _parasite); 62 | } else { 63 | // Read scratchpads 64 | for (unsigned char index = 0; index < _devices.size(); index++) { 65 | if (_wire->reset() == 0) { 66 | _devices[index].data[0] = _devices[index].data[0] + 1; // Force a CRC check error 67 | return; 68 | } 69 | 70 | // Read each scratchpad 71 | _wire->select(_devices[index].address); 72 | _wire->write(DS18_CMD_READ_SCRATCHPAD); 73 | 74 | uint8_t data[DS18_DATA_SIZE]; 75 | for (unsigned char i = 0; i < DS18_DATA_SIZE; i++) { 76 | data[i] = _wire->read(); 77 | } 78 | 79 | if (_wire->reset() != 1) { 80 | _devices[index].data[0] = _devices[index].data[0] + 1; // Force a CRC check error 81 | return; 82 | } 83 | 84 | memcpy(_devices[index].data, data, DS18_DATA_SIZE); 85 | } 86 | } 87 | 88 | conversion = !conversion; 89 | } 90 | 91 | // return string of the device, with name and address 92 | char * DS18::getDeviceType(char * buffer, unsigned char index) { 93 | uint8_t size = 128; 94 | if (index < _count) { 95 | unsigned char chip_id = chip(index); 96 | if (chip_id == DS18_CHIP_DS18S20) { 97 | strlcpy(buffer, "DS18S20", size); 98 | } else if (chip_id == DS18_CHIP_DS18B20) { 99 | strlcpy(buffer, "DS18B20", size); 100 | } else if (chip_id == DS18_CHIP_DS1822) { 101 | strlcpy(buffer, "DS1822", size); 102 | } else if (chip_id == DS18_CHIP_DS1825) { 103 | strlcpy(buffer, "DS1825", size); 104 | } else { 105 | strlcpy(buffer, "Unknown", size); 106 | } 107 | } else { 108 | strlcpy(buffer, "invalid", size); 109 | } 110 | 111 | return buffer; 112 | } 113 | 114 | // return string of the device, with name and address 115 | char * DS18::getDeviceID(char * buffer, unsigned char index) { 116 | uint8_t size = 128; 117 | if (index < _count) { 118 | uint8_t * address = _devices[index].address; 119 | char a[30] = {0}; 120 | snprintf(a, sizeof(a), "%02X%02X%02X%02X%02X%02X%02X%02X", address[0], address[1], address[2], address[3], address[4], address[5], address[6], address[7]); 121 | 122 | strlcpy(buffer, a, size); 123 | } else { 124 | strlcpy(buffer, "invalid", size); 125 | } 126 | 127 | return buffer; 128 | } 129 | 130 | /* 131 | * Read sensor values 132 | * 133 | * Registers: 134 | byte 0: temperature LSB 135 | byte 1: temperature MSB 136 | byte 2: high alarm temp 137 | byte 3: low alarm temp 138 | byte 4: DS18S20: store for crc 139 | DS18B20 & DS1822: configuration register 140 | byte 5: internal use & crc 141 | byte 6: DS18S20: COUNT_REMAIN 142 | DS18B20 & DS1822: store for crc 143 | byte 7: DS18S20: COUNT_PER_C 144 | DS18B20 & DS1822: store for crc 145 | byte 8: SCRATCHPAD_CRC 146 | */ 147 | int16_t DS18::getRawValue(unsigned char index) { 148 | if (index >= _count) { 149 | return 0; 150 | } 151 | 152 | uint8_t * data = _devices[index].data; 153 | 154 | if (OneWire::crc8(data, DS18_DATA_SIZE - 1) != data[DS18_DATA_SIZE - 1]) { 155 | return DS18_CRC_ERROR; 156 | } 157 | 158 | int16_t raw = (data[1] << 8) | data[0]; 159 | if (chip(index) == DS18_CHIP_DS18S20) { 160 | raw = raw << 3; // 9 bit resolution default 161 | if (data[7] == 0x10) { 162 | raw = (raw & 0xFFF0) + 12 - data[6]; // "count remain" gives full 12 bit resolution 163 | } 164 | } else { 165 | byte cfg = (data[4] & 0x60); 166 | if (cfg == 0x00) { 167 | raw = raw & ~7; // 9 bit res, 93.75 ms 168 | } else if (cfg == 0x20) { 169 | raw = raw & ~3; // 10 bit res, 187.5 ms 170 | } else if (cfg == 0x40) { 171 | raw = raw & ~1; // 11 bit res, 375 ms 172 | // 12 bit res, 750 ms 173 | } 174 | } 175 | 176 | return raw; 177 | } 178 | 179 | // return real value as a float, rounded to 2 decimal places 180 | float DS18::getValue(unsigned char index) { 181 | int16_t raw_value = getRawValue(index); 182 | 183 | // check if valid 184 | if ((raw_value == DS18_CRC_ERROR) || (raw_value == DS18_DISCONNECTED)) { 185 | return (float)DS18_DISCONNECTED; 186 | } 187 | 188 | // The raw temperature data is in units of sixteenths of a degree, 189 | // so the value must first be divided by 16 in order to convert it to degrees. 190 | float new_value = (float)(raw_value / 16.0); 191 | 192 | // round to 2 decimal places 193 | // https://arduinojson.org/v6/faq/how-to-configure-the-serialization-of-floats/ 194 | return (int)(new_value * 100 + 0.5) / 100.0; 195 | } 196 | 197 | // check for a supported DS chip version 198 | bool DS18::validateID(unsigned char id) { 199 | return (id == DS18_CHIP_DS18S20) || (id == DS18_CHIP_DS18B20) || (id == DS18_CHIP_DS1822) || (id == DS18_CHIP_DS1825); 200 | } 201 | 202 | // return the type 203 | unsigned char DS18::chip(unsigned char index) { 204 | if (index < _count) { 205 | return _devices[index].address[0]; 206 | } 207 | return 0; 208 | } 209 | 210 | // scan for DS sensors and load into the vector 211 | uint8_t DS18::loadDevices() { 212 | uint8_t address[8]; 213 | _wire->reset(); 214 | _wire->reset_search(); 215 | 216 | while (_wire->search(address)) { 217 | // Check CRC 218 | if (_wire->crc8(address, 7) == address[7]) { 219 | // Check ID 220 | if (validateID(address[0])) { 221 | ds_device_t device; 222 | memcpy(device.address, address, 8); 223 | _devices.push_back(device); 224 | } 225 | } 226 | } 227 | return (_devices.size()); 228 | } 229 | -------------------------------------------------------------------------------- /src/custom.js: -------------------------------------------------------------------------------- 1 | var custom_config = { 2 | "command": "custom_configfile", 3 | "settings": { 4 | "led": true, 5 | "led_gpio": 2, 6 | "dallas_gpio": 14, 7 | "dallas_parasite": false, 8 | "listen_mode": false, 9 | "shower_timer": false, 10 | "shower_alert": false, 11 | "publish_time": 10, 12 | "tx_mode": 1 13 | } 14 | }; 15 | 16 | function listcustom() { 17 | 18 | document.getElementById("led_gpio").value = custom_config.settings.led_gpio; 19 | document.getElementById("dallas_gpio").value = custom_config.settings.dallas_gpio; 20 | document.getElementById("publish_time").value = custom_config.settings.publish_time; 21 | document.getElementById("tx_mode").value = custom_config.settings.tx_mode; 22 | 23 | if (custom_config.settings.led) { 24 | $("input[name=\"led\"][value=\"1\"]").prop("checked", true); 25 | } 26 | 27 | if (custom_config.settings.dallas_parasite) { 28 | $("input[name=\"dallas_parasite\"][value=\"1\"]").prop("checked", true); 29 | } 30 | 31 | if (custom_config.settings.listen_mode) { 32 | $("input[name=\"listen_mode\"][value=\"1\"]").prop("checked", true); 33 | } 34 | 35 | if (custom_config.settings.shower_timer) { 36 | $("input[name=\"shower_timer\"][value=\"1\"]").prop("checked", true); 37 | } 38 | 39 | if (custom_config.settings.shower_alert) { 40 | $("input[name=\"shower_alert\"][value=\"1\"]").prop("checked", true); 41 | } 42 | } 43 | 44 | function savecustom() { 45 | custom_config.settings.led_gpio = parseInt(document.getElementById("led_gpio").value); 46 | custom_config.settings.dallas_gpio = parseInt(document.getElementById("dallas_gpio").value); 47 | 48 | custom_config.settings.dallas_parasite = false; 49 | if (parseInt($("input[name=\"dallas_parasite\"]:checked").val()) === 1) { 50 | custom_config.settings.dallas_parasite = true; 51 | } 52 | 53 | custom_config.settings.listen_mode = false; 54 | if (parseInt($("input[name=\"listen_mode\"]:checked").val()) === 1) { 55 | custom_config.settings.listen_mode = true; 56 | } 57 | 58 | custom_config.settings.shower_timer = false; 59 | if (parseInt($("input[name=\"shower_timer\"]:checked").val()) === 1) { 60 | custom_config.settings.shower_timer = true; 61 | } 62 | 63 | custom_config.settings.shower_alert = false; 64 | if (parseInt($("input[name=\"shower_alert\"]:checked").val()) === 1) { 65 | custom_config.settings.shower_alert = true; 66 | } 67 | 68 | custom_config.settings.led = false; 69 | if (parseInt($("input[name=\"led\"]:checked").val()) === 1) { 70 | custom_config.settings.led = true; 71 | } 72 | 73 | custom_config.settings.publish_time = parseInt(document.getElementById("publish_time").value); 74 | custom_config.settings.tx_mode = parseInt(document.getElementById("tx_mode").value); 75 | 76 | custom_saveconfig(); 77 | } 78 | 79 | function listCustomStats() { 80 | document.getElementById("msg").innerHTML = ajaxobj.emsbus.msg; 81 | if (ajaxobj.emsbus.ok) { 82 | document.getElementById("msg").className = "alert alert-success"; 83 | } else { 84 | document.getElementById("msg").className = "alert alert-danger"; 85 | document.getElementById("devicesshow").style.display = "none"; 86 | document.getElementById("thermostat_show").style.display = "none"; 87 | document.getElementById("boiler_show").style.display = "none"; 88 | document.getElementById("sm_show").style.display = "none"; 89 | document.getElementById("hp_show").style.display = "none"; 90 | return; 91 | } 92 | 93 | var list = document.getElementById("devices"); 94 | var obj = ajaxobj.emsbus.devices; 95 | 96 | document.getElementById("devicesshow").style.display = "block"; 97 | 98 | for (var i = 0; i < obj.length; i++) { 99 | var l = document.createElement("li"); 100 | var type = obj[i].type; 101 | var color = ""; 102 | if (type === "Boiler") { 103 | color = "list-group-item-success"; 104 | } else if (type === "Thermostat") { 105 | color = "list-group-item-info"; 106 | } else if (type === "Solar Module") { 107 | color = "list-group-item-warning"; 108 | } else if (type === "Heat Pump") { 109 | color = "list-group-item-success"; 110 | } 111 | //l.innerHTML = obj[i].model + " (DeviceID: 0x" + obj[i].deviceid + ", ProductID: " + obj[i].productid + ", Version: " + obj[i].version + ")"; 112 | //l.className = "list-group-item " + color; 113 | //list.appendChild(l); 114 | } 115 | 116 | if (ajaxobj.boiler.ok) { 117 | document.getElementById("boiler_show").style.display = "block"; 118 | 119 | document.getElementById("bm").innerHTML = ajaxobj.boiler.bm; 120 | document.getElementById("b1").innerHTML = ajaxobj.boiler.b1; 121 | document.getElementById("b2").innerHTML = ajaxobj.boiler.b2; 122 | document.getElementById("b3").innerHTML = ajaxobj.boiler.b3 + " %"; 123 | document.getElementById("b4").innerHTML = ajaxobj.boiler.b4 + " ℃"; 124 | document.getElementById("b5").innerHTML = ajaxobj.boiler.b5 + " ℃"; 125 | document.getElementById("b6").innerHTML = ajaxobj.boiler.b6 + " ℃"; 126 | document.getElementById("b7").innerHTML = ajaxobj.boiler.b7 + " ℃"; 127 | document.getElementById("b8").innerHTML = ajaxobj.boiler.b8 + " ℃"; 128 | } else { 129 | document.getElementById("boiler_show").style.display = "none"; 130 | } 131 | 132 | if (ajaxobj.thermostat.ok) { 133 | document.getElementById("thermostat_show").style.display = "block"; 134 | 135 | document.getElementById("tm").innerHTML = ajaxobj.thermostat.tm; 136 | document.getElementById("ts").innerHTML = ajaxobj.thermostat.ts + " ℃"; 137 | document.getElementById("tc").innerHTML = ajaxobj.thermostat.tc + " ℃"; 138 | document.getElementById("tmode").innerHTML = ajaxobj.thermostat.tmode; 139 | } else { 140 | document.getElementById("thermostat_show").style.display = "none"; 141 | } 142 | 143 | if (ajaxobj.sm.ok) { 144 | document.getElementById("sm_show").style.display = "block"; 145 | 146 | document.getElementById("sm").innerHTML = ajaxobj.sm.sm; 147 | document.getElementById("sm1").innerHTML = ajaxobj.sm.sm1 + " ℃"; 148 | document.getElementById("sm2").innerHTML = ajaxobj.sm.sm2 + " ℃"; 149 | document.getElementById("sm3").innerHTML = ajaxobj.sm.sm3 + " %"; 150 | document.getElementById("sm4").innerHTML = ajaxobj.sm.sm4; 151 | document.getElementById("sm5").innerHTML = ajaxobj.sm.sm5 + " Wh"; 152 | document.getElementById("sm6").innerHTML = ajaxobj.sm.sm6 + " Wh"; 153 | document.getElementById("sm7").innerHTML = ajaxobj.sm.sm7 + " KWh"; 154 | } else { 155 | document.getElementById("sm_show").style.display = "none"; 156 | } 157 | 158 | if (ajaxobj.hp.ok) { 159 | document.getElementById("hp_show").style.display = "block"; 160 | 161 | document.getElementById("hm").innerHTML = ajaxobj.hp.hm; 162 | document.getElementById("hp1").innerHTML = ajaxobj.hp.hp1 + " %"; 163 | document.getElementById("hp2").innerHTML = ajaxobj.hp.hp2 + " %"; 164 | } else { 165 | document.getElementById("hp_show").style.display = "none"; 166 | } 167 | 168 | 169 | } 170 | 171 | -------------------------------------------------------------------------------- /src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css: -------------------------------------------------------------------------------- 1 | table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}.footable-filtering-external.footable-filtering-right,table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}.footable-filtering-external.footable-filtering-left,table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}.footable-filtering-external.footable-filtering-center,.footable-paging-external.footable-paging-center,table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}.footable-filtering-external ul.dropdown-menu>li>a.checkbox,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}.footable-filtering-external ul.dropdown-menu>li>a.checkbox>label,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}.footable-filtering-external ul.dropdown-menu>li>a.checkbox input[type=checkbox],table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}.footable-paging-external ul.pagination,table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}.footable-paging-external span.label,table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}.footable-paging-external.footable-paging-left,table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}.footable-paging-external.footable-paging-right,table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/Timezone.cpp: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------* 2 | * Arduino Timezone Library * 3 | * Jack Christensen Mar 2012 * 4 | * * 5 | * Stripped down for myESP by Paul Derbyshire * 6 | * * 7 | * Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and * 8 | * licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html * 9 | *----------------------------------------------------------------------*/ 10 | 11 | #include "Timezone.h" 12 | 13 | /*----------------------------------------------------------------------* 14 | * Create a Timezone object from the given time change rules. * 15 | *----------------------------------------------------------------------*/ 16 | Timezone::Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart) 17 | : m_dst(dstStart) 18 | , m_std(stdStart) { 19 | initTimeChanges(); 20 | } 21 | 22 | /*----------------------------------------------------------------------* 23 | * Create a Timezone object for a zone that does not observe * 24 | * daylight time. * 25 | *----------------------------------------------------------------------*/ 26 | Timezone::Timezone(TimeChangeRule stdTime) 27 | : m_dst(stdTime) 28 | , m_std(stdTime) { 29 | initTimeChanges(); 30 | } 31 | 32 | /*----------------------------------------------------------------------* 33 | * Convert the given UTC time to local time, standard or * 34 | * daylight time, as appropriate. * 35 | *----------------------------------------------------------------------*/ 36 | time_t Timezone::toLocal(time_t utc) { 37 | // recalculate the time change points if needed 38 | if (to_year(utc) != to_year(m_dstUTC)) 39 | calcTimeChanges(to_year(utc)); 40 | 41 | if (utcIsDST(utc)) 42 | return utc + m_dst.offset * SECS_PER_MIN; 43 | else 44 | return utc + m_std.offset * SECS_PER_MIN; 45 | } 46 | 47 | /*----------------------------------------------------------------------* 48 | * Convert the given UTC time to local time, standard or * 49 | * daylight time, as appropriate, and return a pointer to the time * 50 | * change rule used to do the conversion. The caller must take care * 51 | * not to alter this rule. * 52 | *----------------------------------------------------------------------*/ 53 | time_t Timezone::toLocal(time_t utc, TimeChangeRule ** tcr) { 54 | // recalculate the time change points if needed 55 | if (to_year(utc) != to_year(m_dstUTC)) 56 | calcTimeChanges(to_year(utc)); 57 | 58 | if (utcIsDST(utc)) { 59 | *tcr = &m_dst; 60 | return utc + m_dst.offset * SECS_PER_MIN; 61 | } else { 62 | *tcr = &m_std; 63 | return utc + m_std.offset * SECS_PER_MIN; 64 | } 65 | } 66 | 67 | /*----------------------------------------------------------------------* 68 | * Convert the given local time to UTC time. * 69 | * * 70 | * WARNING: * 71 | * This function is provided for completeness, but should seldom be * 72 | * needed and should be used sparingly and carefully. * 73 | * * 74 | * Ambiguous situations occur after the Standard-to-DST and the * 75 | * DST-to-Standard time transitions. When changing to DST, there is * 76 | * one hour of local time that does not exist, since the clock moves * 77 | * forward one hour. Similarly, when changing to standard time, there * 78 | * is one hour of local times that occur twice since the clock moves * 79 | * back one hour. * 80 | * * 81 | * This function does not test whether it is passed an erroneous time * 82 | * value during the Local -> DST transition that does not exist. * 83 | * If passed such a time, an incorrect UTC time value will be returned. * 84 | * * 85 | * If passed a local time value during the DST -> Local transition * 86 | * that occurs twice, it will be treated as the earlier time, i.e. * 87 | * the time that occurs before the transistion. * 88 | * * 89 | * Calling this function with local times during a transition interval * 90 | * should be avoided! * 91 | *----------------------------------------------------------------------*/ 92 | time_t Timezone::toUTC(time_t local) { 93 | // recalculate the time change points if needed 94 | if (to_year(local) != to_year(m_dstLoc)) 95 | calcTimeChanges(to_year(local)); 96 | 97 | if (locIsDST(local)) 98 | return local - m_dst.offset * SECS_PER_MIN; 99 | else 100 | return local - m_std.offset * SECS_PER_MIN; 101 | } 102 | 103 | /*----------------------------------------------------------------------* 104 | * Determine whether the given UTC time_t is within the DST interval * 105 | * or the Standard time interval. * 106 | *----------------------------------------------------------------------*/ 107 | bool Timezone::utcIsDST(time_t utc) { 108 | // recalculate the time change points if needed 109 | if (to_year(utc) != to_year(m_dstUTC)) 110 | calcTimeChanges(to_year(utc)); 111 | 112 | if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz 113 | return false; 114 | else if (m_stdUTC > m_dstUTC) // northern hemisphere 115 | return (utc >= m_dstUTC && utc < m_stdUTC); 116 | else // southern hemisphere 117 | return !(utc >= m_stdUTC && utc < m_dstUTC); 118 | } 119 | 120 | /*----------------------------------------------------------------------* 121 | * Determine whether the given Local time_t is within the DST interval * 122 | * or the Standard time interval. * 123 | *----------------------------------------------------------------------*/ 124 | bool Timezone::locIsDST(time_t local) { 125 | // recalculate the time change points if needed 126 | if (to_year(local) != to_year(m_dstLoc)) 127 | calcTimeChanges(to_year(local)); 128 | 129 | if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz 130 | return false; 131 | else if (m_stdLoc > m_dstLoc) // northern hemisphere 132 | return (local >= m_dstLoc && local < m_stdLoc); 133 | else // southern hemisphere 134 | return !(local >= m_stdLoc && local < m_dstLoc); 135 | } 136 | 137 | /*----------------------------------------------------------------------* 138 | * Calculate the DST and standard time change points for the given * 139 | * given year as local and UTC time_t values. * 140 | *----------------------------------------------------------------------*/ 141 | void Timezone::calcTimeChanges(int yr) { 142 | m_dstLoc = toTime_t(m_dst, yr); 143 | m_stdLoc = toTime_t(m_std, yr); 144 | m_dstUTC = m_dstLoc - m_std.offset * SECS_PER_MIN; 145 | m_stdUTC = m_stdLoc - m_dst.offset * SECS_PER_MIN; 146 | } 147 | 148 | /*----------------------------------------------------------------------* 149 | * Initialize the DST and standard time change points. * 150 | *----------------------------------------------------------------------*/ 151 | void Timezone::initTimeChanges() { 152 | m_dstLoc = 0; 153 | m_stdLoc = 0; 154 | m_dstUTC = 0; 155 | m_stdUTC = 0; 156 | } 157 | 158 | /*----------------------------------------------------------------------* 159 | * Convert the given time change rule to a time_t value * 160 | * for the given year. * 161 | *----------------------------------------------------------------------*/ 162 | time_t Timezone::toTime_t(TimeChangeRule r, int yr) { 163 | uint8_t m = r.month; // temp copies of r.month and r.week 164 | uint8_t w = r.week; 165 | if (w == 0) // is this a "Last week" rule? 166 | { 167 | if (++m > 12) // yes, for "Last", go to the next month 168 | { 169 | m = 1; 170 | ++yr; 171 | } 172 | w = 1; // and treat as first week of next month, subtract 7 days later 173 | } 174 | 175 | // calculate first day of the month, or for "Last" rules, first day of the next month 176 | tmElements_t tm; 177 | tm.Hour = r.hour; 178 | tm.Minute = 0; 179 | tm.Second = 0; 180 | tm.Day = 1; 181 | tm.Month = m; 182 | tm.Year = yr - 1970; 183 | time_t t = makeTime(tm); 184 | 185 | // add offset from the first of the month to r.dow, and offset for the given week 186 | t += ((r.dow - to_weekday(t) + 7) % 7 + (w - 1) * 7) * SECS_PER_DAY; 187 | // back up a week if this is a "Last" rule 188 | if (r.week == 0) 189 | t -= 7 * SECS_PER_DAY; 190 | return t; 191 | } 192 | 193 | /*----------------------------------------------------------------------* 194 | * Read or update the daylight and standard time rules from RAM. * 195 | *----------------------------------------------------------------------*/ 196 | void Timezone::setRules(TimeChangeRule dstStart, TimeChangeRule stdStart) { 197 | m_dst = dstStart; 198 | m_std = stdStart; 199 | initTimeChanges(); // force calcTimeChanges() at next conversion call 200 | } 201 | -------------------------------------------------------------------------------- /tools/wsemulator/wserver.js: -------------------------------------------------------------------------------- 1 | console.log("[INFO] Starting MyESP WebSocket Emulation Server"); 2 | 3 | const WebSocket = require("ws"); 4 | 5 | console.log("[INFO] You can connect to ws://localhost or load URL .../src/websrc/temp/index.html"); 6 | console.log("[INFO] Password is 'neo'"); 7 | 8 | const wss = new WebSocket.Server({ 9 | port: 80 10 | }); 11 | 12 | wss.broadcast = function broadcast(data) { 13 | wss.clients.forEach(function each(client) { 14 | if (client.readyState === WebSocket.OPEN) { 15 | client.send(JSON.stringify(data)); 16 | } 17 | }); 18 | }; 19 | 20 | var networks = { 21 | "command": "ssidlist", 22 | "list": [{ 23 | "ssid": "Company's Network", 24 | "bssid": "4c:f4:39:a1:41", 25 | "rssi": "-84" 26 | }, 27 | { 28 | "ssid": "Home Router", 29 | "bssid": "8a:e6:63:a8:15", 30 | "rssi": "-42" 31 | }, 32 | { 33 | "ssid": "SSID Shown Here", 34 | "bssid": "8a:f5:86:c3:12", 35 | "rssi": "-77" 36 | }, 37 | { 38 | "ssid": "Great Wall of WPA", 39 | "bssid": "9c:f1:90:c5:15", 40 | "rssi": "-80" 41 | }, 42 | { 43 | "ssid": "Not Internet", 44 | "bssid": "8c:e4:57:c5:16", 45 | "rssi": "-87" 46 | } 47 | ] 48 | } 49 | 50 | var eventlog = { 51 | "command": "eventlist", 52 | "page": 1, 53 | "haspages": 1, 54 | "list": [ 55 | "{\"type\":\"WARN\",\"src\":\"system\",\"desc\":\"test data\",\"data\":\"Record #1\",\"time\": 1563371160}", 56 | "{\"type\":\"WARN\",\"src\":\"system\",\"desc\":\"test data\",\"data\":\"Record #2\",\"time\":0}", 57 | "{\"type\":\"INFO\",\"src\":\"system\",\"desc\":\"System booted\",\"data\":\"\",\"time\":1568660479}", 58 | "{\"type\":\"WARN\",\"src\":\"system\",\"desc\":\"test data\",\"data\":\"Record #3\",\"time\":0}" 59 | ] 60 | } 61 | 62 | var configfile = { 63 | "command": "configfile", 64 | "network": { 65 | "ssid": "myssid", 66 | "wmode": 0, 67 | "password": "password" 68 | }, 69 | "general": { 70 | "hostname": "myesp", 71 | "password": "admin", 72 | "serial": true, 73 | "log_events": true 74 | }, 75 | "mqtt": { 76 | "enabled": false, 77 | "ip": "10.10.10.10", 78 | "port": 1883, 79 | "qos": 1, 80 | "keepalive": 60, 81 | "retain": true, 82 | "base": "base", 83 | "user": "user", 84 | "password": "password", 85 | "heartbeat": false 86 | }, 87 | "ntp": { 88 | "server": "pool.ntp.org", 89 | "interval": "30", 90 | "enabled": false 91 | } 92 | }; 93 | 94 | var custom_configfile = { 95 | "command": "custom_configfile", 96 | "settings": { 97 | "led": true, 98 | "led_gpio": 2, 99 | "dallas_gpio": 14, 100 | "dallas_parasite": false, 101 | "listen_mode": false, 102 | "shower_timer": true, 103 | "shower_alert": false, 104 | "publish_time": 120, 105 | "tx_mode": 1 106 | } 107 | }; 108 | 109 | function sendEventLog() { 110 | wss.broadcast(eventlog); 111 | var res = { 112 | "command": "result", 113 | "resultof": "eventlist", 114 | "result": true 115 | }; 116 | wss.broadcast(res); 117 | } 118 | 119 | function sendStatus() { 120 | var stats = { 121 | "command": "status", 122 | "availspiffs": 948, 123 | "spiffssize": 957, 124 | "initheap": 25392, 125 | "heap": 13944, 126 | "sketchsize": 673, 127 | "availsize": 2469, 128 | "ip": "10.10.10.198", 129 | "ssid": "my_ssid", 130 | "mac": "DC:4F:11:22:93:06", 131 | "signalstr": 62, 132 | "systemload": 0, 133 | "mqttconnected": true, 134 | "mqttheartbeat": false, 135 | "uptime": "0 days 0 hours 1 minute 45 seconds", 136 | "mqttloghdr": "home/ems-esp/", 137 | "mqttlog": [ 138 | { "topic": "start", "payload": "start", "time": 1565956388 }, 139 | { "topic": "shower_timer", "payload": "1", "time": 1565956388 }, 140 | { "topic": "shower_alert", "payload": "0", "time": 1565956388 }, 141 | { "topic": "boiler_data", "payload": "{\"wWComfort\":\"Hot\",\"wWSelTemp\":60,\"selFlowTemp\":5,\"selBurnPow\":0,\"curBurnPow\":0,\"pumpMod\":0,\"wWCurTmp\":48.4,\"wWCurFlow\":0,\"curFlowTemp\":49.3,\"retTemp\":49.3,\"sysPress\":1.8,\"boilTemp\":50.5,\"wWActivated\":\"on\",\"burnGas\":\"off\",\"heatPmp\":\"off\",\"fanWork\":\"off\",\"ignWork\":\"off\",\"wWCirc\":\"off\",\"wWHeat\":\"on\",\"burnStarts\":223397,\"burnWorkMin\":366019,\"heatWorkMin\":294036,\"ServiceCode\":\"0H\",\"ServiceCodeNumber\":203}", "time": 1565956463 }, 142 | { "topic": "tapwater_active", "payload": "0", "time": 1565956408 }, 143 | { "topic": "heating_active", "payload": "0", "time": 1565956408 }, 144 | { "topic": "thermostat_data", "payload": "{\"thermostat_hc\":\"1\",\"thermostat_seltemp\":15,\"thermostat_currtemp\":23,\"thermostat_mode\":\"auto\"}", "time": 1565956444 } 145 | ] 146 | }; 147 | 148 | wss.broadcast(stats); 149 | } 150 | 151 | function sendCustomStatus() { 152 | var stats = { 153 | "command": "custom_status", 154 | "version": "1.9.1", 155 | "customname": "EMS-ESP", 156 | "appurl": "https://github.com/proddy/EMS-ESP", 157 | "updateurl": "https://api.github.com/repos/proddy/EMS-ESP/releases/latest", 158 | 159 | "emsbus": { 160 | "ok": true, 161 | "msg": "EMS Bus Connected with both Rx and Tx active.", 162 | "devices": [ 163 | { "type": 1, "model": "Buderus GB172/Nefit Trendline/Junkers Cerapur", "version": "06.01", "productid": 123, "deviceid": "8" }, 164 | { "type": 5, "model": "BC10 Base Controller", "version": "01.03", "productid": 190, "deviceid": "9" }, 165 | { "type": 2, "model": "RC20/Nefit Moduline 300", "version": "03.03", "productid": 77, "deviceid": "17" }, 166 | { "type": 3, "model": "SM100 Solar Module", "version": "01.01", "productid": 163, "deviceid": "30" }, 167 | { "type": 4, "model": "HeatPump Module", "version": "01.01", "productid": 252, "deviceid": "38" } 168 | ] 169 | }, 170 | 171 | "thermostat": { 172 | "ok": true, 173 | "tm": "RC20/Nefit Moduline 300", 174 | "ts": 15, 175 | "tc": 24.5, 176 | "tmode": "auto" 177 | }, 178 | 179 | "boiler": { 180 | "ok": true, 181 | "bm": "Buderus GB172/Nefit Trendline/Junkers Cerapur", 182 | "b1": "off", 183 | "b2": "off", 184 | "b3": 0, 185 | "b4": 53, 186 | "b5": 54.4, 187 | "b6": 53.3 188 | }, 189 | 190 | "sm": { 191 | "ok": true, 192 | "sm": "SM100 Solar Module", 193 | "sm1": 34, 194 | "sm2": 24, 195 | "sm3": 60, 196 | "sm4": "on", 197 | "sm5": 2000, 198 | "sm6": 3000, 199 | "sm7": 123456 200 | }, 201 | 202 | "hp": { 203 | "ok": true, 204 | "hm": "HeatPump Module", 205 | "hp1": 66, 206 | "hp2": 77 207 | } 208 | 209 | }; 210 | 211 | wss.broadcast(stats); 212 | } 213 | 214 | wss.on('connection', function connection(ws) { 215 | ws.on("error", () => console.log("[WARN] WebSocket Error - Assume a client is disconnected.")); 216 | ws.on('message', function incoming(message) { 217 | var obj = JSON.parse(message); 218 | console.log("[INFO] Got Command: " + obj.command); 219 | switch (obj.command) { 220 | case "configfile": 221 | console.log("[INFO] New system config received"); 222 | configfile = obj; 223 | break; 224 | case "custom_configfile": 225 | console.log("[INFO] New custom config received"); 226 | custom_configfile = obj; 227 | break; 228 | case "status": 229 | console.log("[INFO] Sending Fake Emulator Status"); 230 | sendStatus(); 231 | break; 232 | case "custom_status": 233 | console.log("[INFO] Sending custom status"); 234 | sendCustomStatus(); 235 | break; 236 | case "scan": 237 | console.log("[INFO] Sending Fake Wireless Networks"); 238 | wss.broadcast(networks); 239 | break; 240 | case "gettime": 241 | console.log("[INFO] Sending time"); 242 | var res = {}; 243 | res.command = "gettime"; 244 | res.epoch = Math.floor((new Date).getTime() / 1000); 245 | //res.epoch = 1567107755; 246 | wss.broadcast(res); 247 | break; 248 | case "settime": 249 | console.log("[INFO] Setting time (fake)"); 250 | var res = {}; 251 | res.command = "gettime"; 252 | res.epoch = Math.floor((new Date).getTime() / 1000); 253 | wss.broadcast(res); 254 | break; 255 | case "getconf": 256 | console.log("[INFO] Sending system configuration file (if set any)"); 257 | wss.broadcast(configfile); 258 | break; 259 | case "geteventlog": 260 | console.log("[INFO] Sending eventlog"); 261 | sendEventLog(); 262 | break; 263 | case "clearevent": 264 | console.log("[INFO] Clearing eventlog"); 265 | break; 266 | case "restart": 267 | console.log("[INFO] Restart"); 268 | break; 269 | case "destroy": 270 | console.log("[INFO] Destroy"); 271 | break; 272 | case "forcentp": 273 | console.log("[INFO] getting ntp time"); 274 | break; 275 | default: 276 | console.log("[WARN] Unknown command"); 277 | break; 278 | } 279 | }); 280 | }); -------------------------------------------------------------------------------- /src/ems_utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Generic utils 3 | * 4 | * Paul Derbyshire - https://github.com/proddy/EMS-ESP 5 | * 6 | */ 7 | #define EMS_UTILS_INCLUDE_DEBUG_MACRO 8 | 9 | #include "ems_utils.h" 10 | 11 | // convert float to char 12 | char * _float_to_char(char * a, float f, uint8_t precision) { 13 | long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; 14 | 15 | char * ret = a; 16 | long whole = (long)f; 17 | itoa(whole, a, 10); 18 | while (*a != '\0') 19 | a++; 20 | *a++ = '.'; 21 | long decimal = abs((long)((f - whole) * p[precision])); 22 | itoa(decimal, a, 10); 23 | 24 | return ret; 25 | } 26 | 27 | // convert bool to text. bools are stored as bytes 28 | char * _bool_to_char(char * s, uint8_t value) { 29 | if ((value == EMS_VALUE_BOOL_ON) || (value == EMS_VALUE_BOOL_ON2)) { 30 | strlcpy(s, "on", sizeof(s)); 31 | } else if (value == EMS_VALUE_BOOL_OFF) { 32 | strlcpy(s, "off", sizeof(s)); 33 | } else { // EMS_VALUE_BOOL_NOTSET 34 | strlcpy(s, "?", sizeof(s)); 35 | } 36 | return s; 37 | } 38 | 39 | // convert short (two bytes) to text string and returns string 40 | // decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 41 | // negative values are assumed stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) 42 | char * _short_to_char(char * s, int16_t value, uint8_t decimals) { 43 | // remove errors or invalid values 44 | if (value <= EMS_VALUE_SHORT_NOTSET) { 45 | strlcpy(s, "?", 10); 46 | return (s); 47 | } 48 | 49 | // just print 50 | if (decimals == 0) { 51 | ltoa(value, s, 10); 52 | return (s); 53 | } 54 | 55 | // check for negative values 56 | if (value < 0) { 57 | strlcpy(s, "-", 10); 58 | value *= -1; // convert to positive 59 | } else { 60 | strlcpy(s, "", 10); 61 | } 62 | 63 | // do floating point 64 | char s2[10] = {0}; 65 | if (decimals == 2) { 66 | // divide by 2 67 | strlcat(s, ltoa(value / 2, s2, 10), 10); 68 | strlcat(s, ".", 10); 69 | strlcat(s, ((value & 0x01) ? "5" : "0"), 10); 70 | 71 | } else { 72 | strlcat(s, ltoa(value / (decimals * 10), s2, 10), 10); 73 | strlcat(s, ".", 10); 74 | strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10); 75 | } 76 | 77 | return s; 78 | } 79 | 80 | // convert unsigned short (two bytes) to text string and prints it 81 | // decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 82 | char * _ushort_to_char(char * s, uint16_t value, uint8_t decimals) { 83 | // remove errors or invalid values 84 | if (value >= EMS_VALUE_USHORT_NOTSET) { // 0x7D00 85 | strlcpy(s, "?", 10); 86 | return (s); 87 | } 88 | 89 | // just print 90 | if (decimals == 0) { 91 | ltoa(value, s, 10); 92 | return (s); 93 | } 94 | 95 | // do floating point 96 | char s2[10] = {0}; 97 | 98 | if (decimals == 2) { 99 | // divide by 2 100 | strlcpy(s, ltoa(value / 2, s2, 10), 10); 101 | strlcat(s, ".", 10); 102 | strlcat(s, ((value & 0x01) ? "5" : "0"), 10); 103 | 104 | } else { 105 | strlcpy(s, ltoa(value / (decimals * 10), s2, 10), 10); 106 | strlcat(s, ".", 10); 107 | strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10); 108 | } 109 | 110 | return s; 111 | } 112 | 113 | // takes a signed short value (2 bytes), converts to a fraction and prints it 114 | // decimals: 0=no division, 1=divide value by 10 (default), 2=divide by 2, 10=divide value by 100 115 | void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals) { 116 | static char buffer[200] = {0}; 117 | static char s[20] = {0}; 118 | strlcpy(buffer, " ", sizeof(buffer)); 119 | strlcat(buffer, prefix, sizeof(buffer)); 120 | strlcat(buffer, ": ", sizeof(buffer)); 121 | 122 | strlcat(buffer, _short_to_char(s, value, decimals), sizeof(buffer)); 123 | 124 | if (postfix != nullptr) { 125 | strlcat(buffer, " ", sizeof(buffer)); 126 | strlcat(buffer, postfix, sizeof(buffer)); 127 | } 128 | 129 | myDebug(buffer); 130 | } 131 | 132 | // takes a unsigned short value (2 bytes), converts to a fraction and prints it 133 | // decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 134 | void _renderUShortValue(const char * prefix, const char * postfix, uint16_t value, uint8_t decimals) { 135 | static char buffer[200] = {0}; 136 | static char s[20] = {0}; 137 | strlcpy(buffer, " ", sizeof(buffer)); 138 | strlcat(buffer, prefix, sizeof(buffer)); 139 | strlcat(buffer, ": ", sizeof(buffer)); 140 | 141 | strlcat(buffer, _ushort_to_char(s, value, decimals), sizeof(buffer)); 142 | 143 | if (postfix != nullptr) { 144 | strlcat(buffer, " ", sizeof(buffer)); 145 | strlcat(buffer, postfix, sizeof(buffer)); 146 | } 147 | 148 | myDebug(buffer); 149 | } 150 | 151 | // convert int (single byte) to text value and returns it 152 | char * _int_to_char(char * s, uint8_t value, uint8_t div) { 153 | s[0] = '\0'; // reset 154 | if (value == EMS_VALUE_INT_NOTSET) { 155 | strlcpy(s, "?", sizeof(s)); 156 | return (s); 157 | } 158 | 159 | static char s2[5] = {0}; 160 | 161 | switch (div) { 162 | case 1: 163 | itoa(value, s, 10); 164 | break; 165 | 166 | case 2: 167 | strlcpy(s, itoa(value >> 1, s2, 10), 5); 168 | strlcat(s, ".", sizeof(s)); 169 | strlcat(s, ((value & 0x01) ? "5" : "0"), 5); 170 | break; 171 | 172 | case 10: 173 | strlcpy(s, itoa(value / 10, s2, 10), 5); 174 | strlcat(s, ".", sizeof(s)); 175 | strlcat(s, itoa(value % 10, s2, 10), 5); 176 | break; 177 | 178 | default: 179 | itoa(value, s, 10); 180 | break; 181 | } 182 | 183 | return s; 184 | } 185 | 186 | // takes an int value (1 byte), converts to a fraction and prints 187 | void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div) { 188 | static char buffer[200] = {0}; 189 | static char s[20] = {0}; 190 | strlcpy(buffer, " ", sizeof(buffer)); 191 | strlcat(buffer, prefix, sizeof(buffer)); 192 | strlcat(buffer, ": ", sizeof(buffer)); 193 | 194 | strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer)); 195 | 196 | if (postfix != nullptr) { 197 | strlcat(buffer, " ", sizeof(buffer)); 198 | strlcat(buffer, postfix, sizeof(buffer)); 199 | } 200 | 201 | myDebug(buffer); 202 | } 203 | 204 | // takes a long value at prints it to debug log and prints 205 | void _renderLongValue(const char * prefix, const char * postfix, uint32_t value) { 206 | static char buffer[200] = {0}; 207 | strlcpy(buffer, " ", sizeof(buffer)); 208 | strlcat(buffer, prefix, sizeof(buffer)); 209 | strlcat(buffer, ": ", sizeof(buffer)); 210 | 211 | if (value == EMS_VALUE_LONG_NOTSET) { 212 | strlcat(buffer, "?", sizeof(buffer)); 213 | } else { 214 | char s[20] = {0}; 215 | strlcat(buffer, ltoa(value, s, 10), sizeof(buffer)); 216 | } 217 | 218 | if (postfix != nullptr) { 219 | strlcat(buffer, " ", sizeof(buffer)); 220 | strlcat(buffer, postfix, sizeof(buffer)); 221 | } 222 | 223 | myDebug(buffer); 224 | } 225 | 226 | // takes a bool value at prints it to debug log and prints 227 | void _renderBoolValue(const char * prefix, uint8_t value) { 228 | static char buffer[200] = {0}; 229 | static char s[20] = {0}; 230 | strlcpy(buffer, " ", sizeof(buffer)); 231 | strlcat(buffer, prefix, sizeof(buffer)); 232 | strlcat(buffer, ": ", sizeof(buffer)); 233 | 234 | strlcat(buffer, _bool_to_char(s, value), sizeof(buffer)); 235 | 236 | myDebug(buffer); 237 | } 238 | 239 | // like itoa but for hex, and quicker 240 | char * _hextoa(uint8_t value, char * buffer) { 241 | char * p = buffer; 242 | byte nib1 = (value >> 4) & 0x0F; 243 | byte nib2 = (value >> 0) & 0x0F; 244 | *p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA; 245 | *p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA; 246 | *p = '\0'; // null terminate just in case 247 | return buffer; 248 | } 249 | 250 | // for decimals 0 to 99, printed as a 2 char string 251 | char * _smallitoa(uint8_t value, char * buffer) { 252 | buffer[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0'; 253 | buffer[1] = (value % 10) + '0'; 254 | buffer[2] = '\0'; 255 | return buffer; 256 | } 257 | 258 | /* for decimals 0 to 999, printed as a string 259 | */ 260 | char * _smallitoa3(uint16_t value, char * buffer) { 261 | buffer[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0'; 262 | buffer[1] = (((value % 100) / 10) == 0) ? '0' : ((value % 100) / 10) + '0'; 263 | buffer[2] = (value % 10) + '0'; 264 | buffer[3] = '\0'; 265 | return buffer; 266 | } 267 | #ifdef nuniet 268 | // used to read the next string from an input buffer and convert to an 8 bit int 269 | uint8_t _readIntNumber() { 270 | char * numTextPtr = strtok(nullptr, ", \n"); 271 | if (numTextPtr == nullptr) { 272 | return 0; 273 | } 274 | return atoi(numTextPtr); 275 | } 276 | 277 | // used to read the next string from an input buffer and convert to a float 278 | float _readFloatNumber() { 279 | char * numTextPtr = strtok(nullptr, ", \n"); 280 | if (numTextPtr == nullptr) { 281 | return 0; 282 | } 283 | return atof(numTextPtr); 284 | } 285 | 286 | // used to read the next string from an input buffer as a hex value and convert to a 16 bit int 287 | uint16_t _readHexNumber() { 288 | char * numTextPtr = strtok(nullptr, ", \n"); 289 | if (numTextPtr == nullptr) { 290 | return 0; 291 | } 292 | return (uint16_t)strtol(numTextPtr, 0, 16); 293 | } 294 | 295 | // used to read the next string from an input buffer 296 | char * _readWord() { 297 | char * word = strtok(nullptr, ", \n"); 298 | return word; 299 | } 300 | #endif 301 | 302 | size_t _parse_cmd_line(char *cmd_line, char **argv, size_t max_argv) 303 | { 304 | /* split-up a cmd line in to arguments 305 | Adds terminating '0', buffer will be modified ! 306 | */ 307 | 308 | size_t i, len, cnt; 309 | uint8_t flag; 310 | 311 | for (i=0; i ' ') { 322 | if (cmd_line[i] == '\"') { 323 | if (cnt < max_argv) { 324 | argv[cnt++] = &cmd_line[i + 1]; 325 | } 326 | flag = 2; 327 | } else { 328 | if (cnt < max_argv) { 329 | argv[cnt++] = &cmd_line[i]; 330 | } 331 | flag = 1; 332 | } 333 | } 334 | break; 335 | case 1: // parsing data 336 | if (cmd_line[i] <= ' ') { 337 | cmd_line[i] = 0; 338 | flag = 0; 339 | } 340 | break; 341 | case 2: // parsing data 342 | if (cmd_line[i] == '\"') { 343 | cmd_line[i] = 0; 344 | flag = 0; 345 | } 346 | break; 347 | } 348 | } 349 | 350 | 351 | return cnt; 352 | } 353 | 354 | -------------------------------------------------------------------------------- /src/TelnetSpy.h: -------------------------------------------------------------------------------- 1 | /* 2 | * TELNET SERVER FOR ESP8266 / ESP32 3 | * Cloning the serial port via Telnet. 4 | * 5 | * Written by Wolfgang Mattis (arduino@yasheena.de). 6 | * Version 1.1 / September 7, 2018. 7 | * MIT license, all text above must be included in any redistribution. 8 | */ 9 | 10 | /* 11 | * DESCRIPTION 12 | * 13 | * This module allows you "Debugging over the air". So if you already use 14 | * ArduinoOTA this is a helpful extension for wireless development. Use 15 | * "TelnetSpy" instead of "Serial" to send data to the serial port and a copy 16 | * to a telnet connection. There is a circular buffer which allows to store the 17 | * data while the telnet connection is not established. So its possible to 18 | * collect data even when the Wifi and Telnet connections are still not 19 | * established. Its also possible to create a telnet session only if it is 20 | * neccessary: then you will get the already collected data as far as it is 21 | * still stored in the circular buffer. Data send from telnet terminal to 22 | * ESP8266 / ESP32 will be handled as data received by serial port. It is also 23 | * possible to use more than one instance of TelnetSpy, for example to send 24 | * control information on the first instance and data dumps on the second 25 | * instance. 26 | * 27 | * USAGE 28 | * 29 | * Add the following line to your sketch: 30 | * #include 31 | * TelnetSpy LOG; 32 | * 33 | * Add the following line to your initialisation block ( void setup() ): 34 | * LOG.begin(); 35 | * 36 | * Add the following line at the beginning of your main loop ( void loop() ): 37 | * LOG.handle(); 38 | * 39 | * Use the following functions of the TelnetSpy object to modify behavior 40 | * 41 | * Change the port number of this telnet server. If a client is already 42 | * connected it will be disconnected. 43 | * Default: 23 44 | * void setPort(uint16_t portToUse); 45 | * 46 | * Change the message which will be send to the telnet client after a session 47 | * is established. 48 | * Default: "Connection established via TelnetSpy.\n" 49 | * void setWelcomeMsg(char* msg); 50 | * 51 | * Change the message which will be send to the telnet client if another 52 | * session is already established. 53 | * Default: "TelnetSpy: Only one connection possible.\n" 54 | * void setRejectMsg(char* msg); 55 | * 56 | * Change the amount of characters to collect before sending a telnet block. 57 | * Default: 64 58 | * void setMinBlockSize(uint16_t minSize); 59 | * 60 | * Change the time (in ms) to wait before sending a telnet block if its size is 61 | * less than (defined by setMinBlockSize). 62 | * Default: 100 63 | * void setCollectingTime(uint16_t colTime); 64 | * 65 | * Change the maximum size of the telnet packets to send. 66 | * Default: 512 67 | * void setMaxBlockSize(uint16_t maxSize); 68 | * 69 | * Change the size of the ring buffer. Set it to 0 to disable buffering. 70 | * Changing size tries to preserve the already collected data. If the new 71 | * buffer size is too small the youngest data will be preserved only. Returns 72 | * false if the requested buffer size cannot be set. 73 | * Default: 3000 74 | * bool setBufferSize(uint16_t newSize); 75 | * 76 | * This function returns the actual size of the ring buffer. 77 | * uint16_t getBufferSize(); 78 | * 79 | * Enable / disable storing new data in the ring buffer if no telnet connection 80 | * is established. This function allows you to store important data only. You 81 | * can do this by disabling "storeOffline" for sending less important data. 82 | * Default: true 83 | * void setStoreOffline(bool store); 84 | * 85 | * Get actual state of storing data when offline. 86 | * bool getStoreOffline(); 87 | * 88 | * If no data is sent via TelnetSpy the detection of a disconnected client has 89 | * a long timeout. Use setPingTime to define the time (in ms) without traffic 90 | * after which a ping (chr(0)) is sent to the telnet client to detect a 91 | * disconnect earlier. Use 0 as parameter to disable pings. 92 | * Default: 1500 93 | * void setPingTime(uint16_t pngTime); 94 | * 95 | * Set the serial port you want to use with this object (especially for ESP32) 96 | * or NULL if no serial port should be used (telnet only). 97 | * Default: Serial 98 | * void setSerial(HardwareSerial* usedSerial); 99 | * 100 | * This function returns true, if a telnet client is connected. 101 | * bool isClientConnected(); 102 | * 103 | * This function installs a callback function which will be called on every 104 | * telnet connect of this object (except rejected connect tries). Use NULL to 105 | * remove the callback. 106 | * Default: NULL 107 | * void setCallbackOnConnect(void (*callback)()); 108 | * 109 | * This function installs a callback function which will be called on every 110 | * telnet disconnect of this object (except rejected connect tries). Use NULL 111 | * to remove the callback. 112 | * Default: NULL 113 | * void setCallbackOnDisconnect(void (*callback)()); 114 | * 115 | * HINT 116 | * 117 | * Add the following lines to your sketch: 118 | * #define SERIAL TelnetSpy 119 | * //#define SERIAL Serial 120 | * 121 | * Replace "Serial" with "SERIAL" in your sketch. Now you can switch between 122 | * serial only and serial with telnet by changing the comments of the defines 123 | * only. 124 | * 125 | * IMPORTANT 126 | * 127 | * To connect to the telnet server you have to: 128 | * - establish the Wifi connection 129 | * - execute "TelnetSpy.begin(WhatEverYouWant);" 130 | * 131 | * The order is not important. 132 | * 133 | * All you do with "Serial" you can also do with "TelnetSpy", but remember: 134 | * Transfering data also via telnet will need more performance than the serial 135 | * port only. So time critical things may be influenced. 136 | * 137 | * It is not possible to establish more than one telnet connection at the same 138 | * time. But its possible to use more than one instance of TelnetSpy. 139 | * 140 | * If you have problems with low memory you may reduce the value of the define 141 | * TELNETSPY_BUFFER_LEN for a smaller ring buffer on initialisation. 142 | * 143 | * Usage of void setDebugOutput(bool) to enable / disable of capturing of 144 | * os_print calls when you have more than one TelnetSpy instance: That 145 | * TelnetSpy object will handle this functionality where you used 146 | * setDebugOutput at last. On default TelnetSpy has the capturing of OS_print 147 | * calls enabled. So if you have more instances the last created instance will 148 | * handle the capturing. 149 | */ 150 | 151 | #ifndef TelnetSpy_h 152 | #define TelnetSpy_h 153 | 154 | #define TELNETSPY_BUFFER_LEN 1000 // was 3000 155 | #define TELNETSPY_MIN_BLOCK_SIZE 64 156 | #define TELNETSPY_COLLECTING_TIME 100 157 | #define TELNETSPY_MAX_BLOCK_SIZE 512 158 | #define TELNETSPY_PING_TIME 1500 159 | #define TELNETSPY_PORT 23 160 | #define TELNETSPY_CAPTURE_OS_PRINT false 161 | #define TELNETSPY_WELCOME_MSG "Connection established via Telnet.\n" 162 | #define TELNETSPY_REJECT_MSG "Telnet: Only one connection possible.\n" 163 | 164 | #ifdef ESP8266 165 | #include 166 | #else // ESP32 167 | #include 168 | #endif 169 | #include 170 | 171 | class TelnetSpy : public Stream { 172 | public: 173 | TelnetSpy(); 174 | ~TelnetSpy(); 175 | void handle(void); 176 | void setPort(uint16_t portToUse); 177 | void setWelcomeMsg(const char * msg); 178 | void setRejectMsg(const char * msg); 179 | void setMinBlockSize(uint16_t minSize); 180 | void setCollectingTime(uint16_t colTime); 181 | void setMaxBlockSize(uint16_t maxSize); 182 | bool setBufferSize(uint16_t newSize); 183 | uint16_t getBufferSize(); 184 | void setStoreOffline(bool store); 185 | bool getStoreOffline(); 186 | void setPingTime(uint16_t pngTime); 187 | void setSerial(HardwareSerial * usedSerial); 188 | bool isClientConnected(); 189 | void serialPrint(char c); 190 | 191 | void disconnectClient(); // added by Proddy 192 | typedef std::function telnetSpyCallback; // added by Proddy 193 | void setCallbackOnConnect(telnetSpyCallback callback); // changed by proddy 194 | void setCallbackOnDisconnect(telnetSpyCallback callback); // changed by proddy 195 | 196 | // Functions offered by HardwareSerial class: 197 | #ifdef ESP8266 198 | void begin(unsigned long baud) { 199 | begin(baud, SERIAL_8N1, SERIAL_FULL, 1); 200 | } 201 | void begin(unsigned long baud, SerialConfig config) { 202 | begin(baud, config, SERIAL_FULL, 1); 203 | } 204 | void begin(unsigned long baud, SerialConfig config, SerialMode mode) { 205 | begin(baud, config, mode, 1); 206 | } 207 | void begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin); 208 | #else // ESP32 209 | void begin(unsigned long baud, uint32_t config = SERIAL_8N1, int8_t rxPin = -1, int8_t txPin = -1, bool invert = false); 210 | #endif 211 | void end(); 212 | #ifdef ESP8266 213 | void swap() { 214 | swap(1); 215 | } 216 | void swap(uint8_t tx_pin); 217 | void set_tx(uint8_t tx_pin); 218 | void pins(uint8_t tx, uint8_t rx); 219 | bool isTxEnabled(void); 220 | bool isRxEnabled(void); 221 | #endif 222 | int available(void) override; 223 | int peek(void) override; 224 | int read(void) override; 225 | int availableForWrite(void); 226 | void flush(void) override; 227 | size_t write(uint8_t) override; 228 | inline size_t write(unsigned long n) { 229 | return write((uint8_t)n); 230 | } 231 | inline size_t write(long n) { 232 | return write((uint8_t)n); 233 | } 234 | inline size_t write(unsigned int n) { 235 | return write((uint8_t)n); 236 | } 237 | inline size_t write(int n) { 238 | return write((uint8_t)n); 239 | } 240 | using Print::write; 241 | operator bool() const; 242 | void setDebugOutput(bool); 243 | uint32_t baudRate(void); 244 | 245 | bool isSerialAvailable(void); 246 | 247 | protected: 248 | void sendBlock(void); 249 | void addTelnetBuf(char c); 250 | char pullTelnetBuf(); 251 | char peekTelnetBuf(); 252 | int telnetAvailable(); 253 | WiFiServer * telnetServer; 254 | WiFiClient client; 255 | uint16_t port; 256 | HardwareSerial * usedSer; 257 | bool storeOffline; 258 | bool started; 259 | bool listening; 260 | bool firstMainLoop; 261 | unsigned long waitRef; 262 | unsigned long pingRef; 263 | uint16_t pingTime; 264 | char * welcomeMsg; 265 | char * rejectMsg; 266 | uint16_t minBlockSize; 267 | uint16_t collectingTime; 268 | uint16_t maxBlockSize; 269 | bool debugOutput; 270 | char * telnetBuf; 271 | uint16_t bufLen; 272 | uint16_t bufUsed; 273 | uint16_t bufRdIdx; 274 | uint16_t bufWrIdx; 275 | bool connected; 276 | 277 | telnetSpyCallback callbackConnect; // added by proddy 278 | telnetSpyCallback callbackDisconnect; // added by proddy 279 | }; 280 | 281 | #endif 282 | -------------------------------------------------------------------------------- /scripts/decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ESP Exception Decoder 4 | 5 | github: https://github.com/janLo/EspArduinoExceptionDecoder 6 | license: GPL v3 7 | author: Jan Losinski 8 | """ 9 | 10 | import argparse 11 | import re 12 | import subprocess 13 | from collections import namedtuple 14 | 15 | import sys 16 | 17 | import os 18 | 19 | EXCEPTIONS = [ 20 | "Illegal instruction", 21 | "SYSCALL instruction", 22 | "InstructionFetchError: Processor internal physical address or data error during instruction fetch", 23 | "LoadStoreError: Processor internal physical address or data error during load or store", 24 | "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register", 25 | "Alloca: MOVSP instruction, if caller's registers are not in the register file", 26 | "IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero", 27 | "reserved", 28 | "Privileged: Attempt to execute a privileged operation when CRING ? 0", 29 | "LoadStoreAlignmentCause: Load or store to an unaligned address", 30 | "reserved", 31 | "reserved", 32 | "InstrPIFDataError: PIF data error during instruction fetch", 33 | "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", 34 | "InstrPIFAddrError: PIF address error during instruction fetch", 35 | "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", 36 | "InstTLBMiss: Error during Instruction TLB refill", 37 | "InstTLBMultiHit: Multiple instruction TLB entries matched", 38 | "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level less than CRING", 39 | "reserved", 40 | "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch", 41 | "reserved", 42 | "reserved", 43 | "reserved", 44 | "LoadStoreTLBMiss: Error during TLB refill for a load or store", 45 | "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", 46 | "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less than CRING", 47 | "reserved", 48 | "LoadProhibited: A load referenced a page mapped with an attribute that does not permit loads", 49 | "StoreProhibited: A store referenced a page mapped with an attribute that does not permit stores" 50 | ] 51 | 52 | PLATFORMS = { 53 | "ESP8266": "lx106", 54 | "ESP32": "esp32" 55 | } 56 | 57 | EXCEPTION_REGEX = re.compile("^Exception \\((?P[0-9]*)\\):$") 58 | COUNTER_REGEX = re.compile('^epc1=(?P0x[0-9a-f]+) epc2=(?P0x[0-9a-f]+) epc3=(?P0x[0-9a-f]+) ' 59 | 'excvaddr=(?P0x[0-9a-f]+) depc=(?P0x[0-9a-f]+)$') 60 | CTX_REGEX = re.compile("^ctx: (?P.+)$") 61 | POINTER_REGEX = re.compile('^sp: (?P[0-9a-f]+) end: (?P[0-9a-f]+) offset: (?P[0-9a-f]+)$') 62 | STACK_BEGIN = '>>>stack>>>' 63 | STACK_END = '<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$') 66 | 67 | StackLine = namedtuple("StackLine", ["offset", "content"]) 68 | 69 | 70 | class ExceptionDataParser(object): 71 | def __init__(self): 72 | self.exception = None 73 | 74 | self.epc1 = None 75 | self.epc2 = None 76 | self.epc3 = None 77 | self.excvaddr = None 78 | self.depc = None 79 | 80 | self.ctx = None 81 | 82 | self.sp = None 83 | self.end = None 84 | self.offset = None 85 | 86 | self.stack = [] 87 | 88 | def _parse_exception(self, line): 89 | match = EXCEPTION_REGEX.match(line) 90 | if match is not None: 91 | self.exception = int(match.group('exc')) 92 | return self._parse_counters 93 | return self._parse_exception 94 | 95 | def _parse_counters(self, line): 96 | match = COUNTER_REGEX.match(line) 97 | if match is not None: 98 | self.epc1 = match.group("epc1") 99 | self.epc2 = match.group("epc2") 100 | self.epc3 = match.group("epc3") 101 | self.excvaddr = match.group("excvaddr") 102 | self.depc = match.group("depc") 103 | return self._parse_ctx 104 | return self._parse_counters 105 | 106 | def _parse_ctx(self, line): 107 | match = CTX_REGEX.match(line) 108 | if match is not None: 109 | self.ctx = match.group("ctx") 110 | return self._parse_pointers 111 | return self._parse_ctx 112 | 113 | def _parse_pointers(self, line): 114 | match = POINTER_REGEX.match(line) 115 | if match is not None: 116 | self.sp = match.group("sp") 117 | self.end = match.group("end") 118 | self.offset = match.group("offset") 119 | return self._parse_stack_begin 120 | return self._parse_pointers 121 | 122 | def _parse_stack_begin(self, line): 123 | if line == STACK_BEGIN: 124 | return self._parse_stack_line 125 | return self._parse_stack_begin 126 | 127 | def _parse_stack_line(self, line): 128 | if line != STACK_END: 129 | match = STACK_REGEX.match(line) 130 | if match is not None: 131 | self.stack.append(StackLine(offset=match.group("off"), 132 | content=(match.group("c1"), match.group("c2"), match.group("c3"), 133 | match.group("c4")))) 134 | return self._parse_stack_line 135 | return None 136 | 137 | def parse_file(self, file, stack_only=False): 138 | func = self._parse_exception 139 | if stack_only: 140 | func = self._parse_stack_begin 141 | 142 | for line in file: 143 | func = func(line.strip()) 144 | if func is None: 145 | break 146 | 147 | if func is not None: 148 | print("ERROR: Parser not complete!") 149 | sys.exit(1) 150 | 151 | 152 | class AddressResolver(object): 153 | def __init__(self, tool_path, elf_path): 154 | self._tool = tool_path 155 | self._elf = elf_path 156 | self._address_map = {} 157 | 158 | def _lookup(self, addresses): 159 | cmd = [self._tool, "-aipfC", "-e", self._elf] + [addr for addr in addresses if addr is not None] 160 | 161 | if sys.version_info[0] < 3: 162 | output = subprocess.check_output(cmd) 163 | else: 164 | output = subprocess.check_output(cmd, encoding="utf-8") 165 | 166 | line_regex = re.compile("^(?P[0-9a-fx]+): (?P.+)$") 167 | 168 | last = None 169 | for line in output.splitlines(): 170 | line = line.strip() 171 | match = line_regex.match(line) 172 | 173 | if match is None: 174 | if last is not None and line.startswith('(inlined by)'): 175 | line = line [12:].strip() 176 | self._address_map[last] += ("\n \-> inlined by: " + line) 177 | continue 178 | 179 | if match.group("result") == '?? ??:0': 180 | continue 181 | 182 | self._address_map[match.group("addr")] = match.group("result") 183 | last = match.group("addr") 184 | 185 | def fill(self, parser): 186 | addresses = [parser.epc1, parser.epc2, parser.epc3, parser.excvaddr, parser.sp, parser.end, parser.offset] 187 | for line in parser.stack: 188 | addresses.extend(line.content) 189 | 190 | self._lookup(addresses) 191 | 192 | def _sanitize_addr(self, addr): 193 | if addr.startswith("0x"): 194 | addr = addr[2:] 195 | 196 | fill = "0" * (8 - len(addr)) 197 | return "0x" + fill + addr 198 | 199 | def resolve_addr(self, addr): 200 | out = self._sanitize_addr(addr) 201 | 202 | if out in self._address_map: 203 | out += ": " + self._address_map[out] 204 | 205 | return out 206 | 207 | def resolve_stack_addr(self, addr, full=True): 208 | addr = self._sanitize_addr(addr) 209 | if addr in self._address_map: 210 | return addr + ": " + self._address_map[addr] 211 | 212 | if full: 213 | return "[DATA (0x" + addr + ")]" 214 | 215 | return None 216 | 217 | 218 | def print_addr(name, value, resolver): 219 | print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) 220 | 221 | 222 | def print_stack_full(lines, resolver): 223 | print("stack:") 224 | for line in lines: 225 | print(line.offset + ":") 226 | for content in line.content: 227 | print(" " + resolver.resolve_stack_addr(content)) 228 | 229 | 230 | def print_stack(lines, resolver): 231 | print("stack:") 232 | for line in lines: 233 | for content in line.content: 234 | out = resolver.resolve_stack_addr(content, full=False) 235 | if out is None: 236 | continue 237 | print(out) 238 | 239 | 240 | def print_result(parser, resolver, full=True, stack_only=False): 241 | if not stack_only: 242 | print('Exception: {} ({})'.format(parser.exception, EXCEPTIONS[parser.exception])) 243 | 244 | print("") 245 | print_addr("epc1", parser.epc1, resolver) 246 | print_addr("epc2", parser.epc2, resolver) 247 | print_addr("epc3", parser.epc3, resolver) 248 | print_addr("excvaddr", parser.excvaddr, resolver) 249 | print_addr("depc", parser.depc, resolver) 250 | 251 | print("") 252 | print("ctx: " + parser.ctx) 253 | 254 | print("") 255 | print_addr("sp", parser.sp, resolver) 256 | print_addr("end", parser.end, resolver) 257 | print_addr("offset", parser.offset, resolver) 258 | 259 | print("") 260 | if full: 261 | print_stack_full(parser.stack, resolver) 262 | else: 263 | print_stack(parser.stack, resolver) 264 | 265 | 266 | def parse_args(): 267 | parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") 268 | 269 | parser.add_argument("-p", "--platform", help="The platform to decode from", choices=PLATFORMS.keys(), 270 | default="ESP8266") 271 | parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", 272 | default="~/.platformio/packages/toolchain-xtensa/") 273 | parser.add_argument("-e", "--elf", help="path to elf file", required=True) 274 | parser.add_argument("-f", "--full", help="Print full stack dump", action="store_true") 275 | parser.add_argument("-s", "--stack_only", help="Decode only a stractrace", action="store_true") 276 | parser.add_argument("file", help="The file to read the exception data from ('-' for STDIN)", default="-") 277 | 278 | return parser.parse_args() 279 | 280 | 281 | if __name__ == "__main__": 282 | args = parse_args() 283 | 284 | if args.file == "-": 285 | file = sys.stdin 286 | else: 287 | if not os.path.exists(args.file): 288 | print("ERROR: file " + args.file + " not found") 289 | sys.exit(1) 290 | file = open(args.file, "r") 291 | 292 | addr2line = os.path.join(os.path.abspath(os.path.expanduser(args.tool)), 293 | "bin/xtensa-" + PLATFORMS[args.platform] + "-elf-addr2line.exe") 294 | if not os.path.exists(addr2line): 295 | print("ERROR: addr2line not found (" + addr2line + ")") 296 | 297 | elf_file = os.path.abspath(os.path.expanduser(args.elf)) 298 | if not os.path.exists(elf_file): 299 | print("ERROR: elf file not found (" + elf_file + ")") 300 | 301 | parser = ExceptionDataParser() 302 | resolver = AddressResolver(addr2line, elf_file) 303 | 304 | parser.parse_file(file, args.stack_only) 305 | resolver.fill(parser) 306 | 307 | print_result(parser, resolver, args.full, args.stack_only) 308 | -------------------------------------------------------------------------------- /scripts/decoder_linux.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ESP Exception Decoder 4 | 5 | github: https://github.com/janLo/EspArduinoExceptionDecoder 6 | license: GPL v3 7 | author: Jan Losinski 8 | """ 9 | 10 | import argparse 11 | import re 12 | import subprocess 13 | from collections import namedtuple 14 | 15 | import sys 16 | 17 | import os 18 | 19 | EXCEPTIONS = [ 20 | "Illegal instruction", 21 | "SYSCALL instruction", 22 | "InstructionFetchError: Processor internal physical address or data error during instruction fetch", 23 | "LoadStoreError: Processor internal physical address or data error during load or store", 24 | "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register", 25 | "Alloca: MOVSP instruction, if caller's registers are not in the register file", 26 | "IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero", 27 | "reserved", 28 | "Privileged: Attempt to execute a privileged operation when CRING ? 0", 29 | "LoadStoreAlignmentCause: Load or store to an unaligned address", 30 | "reserved", 31 | "reserved", 32 | "InstrPIFDataError: PIF data error during instruction fetch", 33 | "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", 34 | "InstrPIFAddrError: PIF address error during instruction fetch", 35 | "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", 36 | "InstTLBMiss: Error during Instruction TLB refill", 37 | "InstTLBMultiHit: Multiple instruction TLB entries matched", 38 | "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level less than CRING", 39 | "reserved", 40 | "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch", 41 | "reserved", 42 | "reserved", 43 | "reserved", 44 | "LoadStoreTLBMiss: Error during TLB refill for a load or store", 45 | "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", 46 | "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less than CRING", 47 | "reserved", 48 | "LoadProhibited: A load referenced a page mapped with an attribute that does not permit loads", 49 | "StoreProhibited: A store referenced a page mapped with an attribute that does not permit stores" 50 | ] 51 | 52 | PLATFORMS = { 53 | "ESP8266": "lx106", 54 | "ESP32": "esp32" 55 | } 56 | 57 | EXCEPTION_REGEX = re.compile("^Exception \\((?P[0-9]*)\\):$") 58 | COUNTER_REGEX = re.compile('^epc1=(?P0x[0-9a-f]+) epc2=(?P0x[0-9a-f]+) epc3=(?P0x[0-9a-f]+) ' 59 | 'excvaddr=(?P0x[0-9a-f]+) depc=(?P0x[0-9a-f]+)$') 60 | CTX_REGEX = re.compile("^ctx: (?P.+)$") 61 | POINTER_REGEX = re.compile('^sp: (?P[0-9a-f]+) end: (?P[0-9a-f]+) offset: (?P[0-9a-f]+)$') 62 | STACK_BEGIN = '>>>stack>>>' 63 | STACK_END = '<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$') 66 | 67 | StackLine = namedtuple("StackLine", ["offset", "content"]) 68 | 69 | 70 | class ExceptionDataParser(object): 71 | def __init__(self): 72 | self.exception = None 73 | 74 | self.epc1 = None 75 | self.epc2 = None 76 | self.epc3 = None 77 | self.excvaddr = None 78 | self.depc = None 79 | 80 | self.ctx = None 81 | 82 | self.sp = None 83 | self.end = None 84 | self.offset = None 85 | 86 | self.stack = [] 87 | 88 | def _parse_exception(self, line): 89 | match = EXCEPTION_REGEX.match(line) 90 | if match is not None: 91 | self.exception = int(match.group('exc')) 92 | return self._parse_counters 93 | return self._parse_exception 94 | 95 | def _parse_counters(self, line): 96 | match = COUNTER_REGEX.match(line) 97 | if match is not None: 98 | self.epc1 = match.group("epc1") 99 | self.epc2 = match.group("epc2") 100 | self.epc3 = match.group("epc3") 101 | self.excvaddr = match.group("excvaddr") 102 | self.depc = match.group("depc") 103 | return self._parse_ctx 104 | return self._parse_counters 105 | 106 | def _parse_ctx(self, line): 107 | match = CTX_REGEX.match(line) 108 | if match is not None: 109 | self.ctx = match.group("ctx") 110 | return self._parse_pointers 111 | return self._parse_ctx 112 | 113 | def _parse_pointers(self, line): 114 | match = POINTER_REGEX.match(line) 115 | if match is not None: 116 | self.sp = match.group("sp") 117 | self.end = match.group("end") 118 | self.offset = match.group("offset") 119 | return self._parse_stack_begin 120 | return self._parse_pointers 121 | 122 | def _parse_stack_begin(self, line): 123 | if line == STACK_BEGIN: 124 | return self._parse_stack_line 125 | return self._parse_stack_begin 126 | 127 | def _parse_stack_line(self, line): 128 | if line != STACK_END: 129 | match = STACK_REGEX.match(line) 130 | if match is not None: 131 | self.stack.append(StackLine(offset=match.group("off"), 132 | content=(match.group("c1"), match.group("c2"), match.group("c3"), 133 | match.group("c4")))) 134 | return self._parse_stack_line 135 | return None 136 | 137 | def parse_file(self, file, stack_only=False): 138 | func = self._parse_exception 139 | if stack_only: 140 | func = self._parse_stack_begin 141 | 142 | for line in file: 143 | func = func(line.strip()) 144 | if func is None: 145 | break 146 | 147 | if func is not None: 148 | print("ERROR: Parser not complete!") 149 | sys.exit(1) 150 | 151 | 152 | class AddressResolver(object): 153 | def __init__(self, tool_path, elf_path): 154 | self._tool = tool_path 155 | self._elf = elf_path 156 | self._address_map = {} 157 | 158 | def _lookup(self, addresses): 159 | cmd = [self._tool, "-aipfC", "-e", self._elf] + [addr for addr in addresses if addr is not None] 160 | 161 | if sys.version_info[0] < 3: 162 | output = subprocess.check_output(cmd) 163 | else: 164 | output = subprocess.check_output(cmd, encoding="utf-8") 165 | 166 | line_regex = re.compile("^(?P[0-9a-fx]+): (?P.+)$") 167 | 168 | last = None 169 | for line in output.splitlines(): 170 | line = line.strip() 171 | match = line_regex.match(line) 172 | 173 | if match is None: 174 | if last is not None and line.startswith('(inlined by)'): 175 | line = line [12:].strip() 176 | self._address_map[last] += ("\n \-> inlined by: " + line) 177 | continue 178 | 179 | if match.group("result") == '?? ??:0': 180 | continue 181 | 182 | self._address_map[match.group("addr")] = match.group("result") 183 | last = match.group("addr") 184 | 185 | def fill(self, parser): 186 | addresses = [parser.epc1, parser.epc2, parser.epc3, parser.excvaddr, parser.sp, parser.end, parser.offset] 187 | for line in parser.stack: 188 | addresses.extend(line.content) 189 | 190 | self._lookup(addresses) 191 | 192 | def _sanitize_addr(self, addr): 193 | if addr.startswith("0x"): 194 | addr = addr[2:] 195 | 196 | fill = "0" * (8 - len(addr)) 197 | return "0x" + fill + addr 198 | 199 | def resolve_addr(self, addr): 200 | out = self._sanitize_addr(addr) 201 | 202 | if out in self._address_map: 203 | out += ": " + self._address_map[out] 204 | 205 | return out 206 | 207 | def resolve_stack_addr(self, addr, full=True): 208 | addr = self._sanitize_addr(addr) 209 | if addr in self._address_map: 210 | return addr + ": " + self._address_map[addr] 211 | 212 | if full: 213 | return "[DATA (0x" + addr + ")]" 214 | 215 | return None 216 | 217 | 218 | def print_addr(name, value, resolver): 219 | print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) 220 | 221 | 222 | def print_stack_full(lines, resolver): 223 | print("stack:") 224 | for line in lines: 225 | print(line.offset + ":") 226 | for content in line.content: 227 | print(" " + resolver.resolve_stack_addr(content)) 228 | 229 | 230 | def print_stack(lines, resolver): 231 | print("stack:") 232 | for line in lines: 233 | for content in line.content: 234 | out = resolver.resolve_stack_addr(content, full=False) 235 | if out is None: 236 | continue 237 | print(out) 238 | 239 | 240 | def print_result(parser, resolver, full=True, stack_only=False): 241 | if not stack_only: 242 | print('Exception: {} ({})'.format(parser.exception, EXCEPTIONS[parser.exception])) 243 | 244 | print("") 245 | print_addr("epc1", parser.epc1, resolver) 246 | print_addr("epc2", parser.epc2, resolver) 247 | print_addr("epc3", parser.epc3, resolver) 248 | print_addr("excvaddr", parser.excvaddr, resolver) 249 | print_addr("depc", parser.depc, resolver) 250 | 251 | print("") 252 | print("ctx: " + parser.ctx) 253 | 254 | print("") 255 | print_addr("sp", parser.sp, resolver) 256 | print_addr("end", parser.end, resolver) 257 | print_addr("offset", parser.offset, resolver) 258 | 259 | print("") 260 | if full: 261 | print_stack_full(parser.stack, resolver) 262 | else: 263 | print_stack(parser.stack, resolver) 264 | 265 | 266 | def parse_args(): 267 | parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") 268 | 269 | parser.add_argument("-p", "--platform", help="The platform to decode from", choices=PLATFORMS.keys(), 270 | default="ESP8266") 271 | parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", 272 | default="~/.platformio/packages/toolchain-xtensa/") 273 | parser.add_argument("-e", "--elf", help="path to elf file", required=True) 274 | parser.add_argument("-f", "--full", help="Print full stack dump", action="store_true") 275 | parser.add_argument("-s", "--stack_only", help="Decode only a stractrace", action="store_true") 276 | parser.add_argument("file", help="The file to read the exception data from ('-' for STDIN)", default="-") 277 | 278 | return parser.parse_args() 279 | 280 | 281 | if __name__ == "__main__": 282 | args = parse_args() 283 | 284 | if args.file == "-": 285 | file = sys.stdin 286 | else: 287 | if not os.path.exists(args.file): 288 | print("ERROR: file " + args.file + " not found") 289 | sys.exit(1) 290 | file = open(args.file, "r") 291 | 292 | addr2line = os.path.join(os.path.abspath(os.path.expanduser(args.tool)), 293 | "bin/xtensa-" + PLATFORMS[args.platform] + "-elf-addr2line") 294 | if not os.path.exists(addr2line): 295 | print("ERROR: addr2line not found (" + addr2line + ")") 296 | 297 | elf_file = os.path.abspath(os.path.expanduser(args.elf)) 298 | if not os.path.exists(elf_file): 299 | print("ERROR: elf file not found (" + elf_file + ")") 300 | 301 | parser = ExceptionDataParser() 302 | resolver = AddressResolver(addr2line, elf_file) 303 | 304 | parser.parse_file(file, args.stack_only) 305 | resolver.fill(parser) 306 | 307 | print_result(parser, resolver, args.full, args.stack_only) 308 | -------------------------------------------------------------------------------- /src/websrc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 93 | 94 |
95 | 99 |
100 |
101 | 102 | 132 | 133 | 150 | 151 | 181 | 182 | 233 |
234 | 235 |
236 |
237 |
238 |
239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /src/custom.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 | Custom Settings 4 |
Please refer to the Help for configuration options
5 |
6 | 7 |
8 | 12 |
13 |
14 | 16 | 18 |
19 |
20 |
21 | 22 |
23 | 27 | 28 | 37 | 38 |
39 | 40 |
41 | 45 |
46 |
47 | 49 | 51 |
52 |
53 |
54 | 55 |
56 | 60 | 61 | 69 | 70 |
71 | 72 |
73 | 76 |
77 |
78 | 80 | 82 |
83 |
84 |
85 | 86 |
87 | 90 |
91 |
92 | 94 | 96 |
97 |
98 |
99 | 100 |
101 | 104 |
105 |
106 | 108 | 110 |
111 |
112 |
113 | 114 |
115 | 118 | 119 | 121 | 122 |
123 |
124 | 125 |
126 | 130 | 131 | 139 | 140 |
141 | 142 |
143 |
144 |
145 | 146 |
147 |
148 | 149 |
Note: any setting marked with a requires a system restart after saving. 151 |
152 |
153 | 154 |
155 |
156 |
157 |
158 |

iRT Dashboard

159 |
Real-time values from the iRT-ESP device are shown here
160 |
161 |
162 |
163 | 164 | 165 | 166 | 171 | 172 | 173 | 174 | 179 | 180 |
iRT Bus Status
167 | 168 | 169 | 170 |
175 |
    176 |
    177 |
178 |
181 |
182 |
183 |
Boiler
184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 |
Hot Tap Water:Central Heating:
Burner power:Current Flow Temperature:
Tap Water Temperature:Return Temperature:
Selected Flow Temperature:Outside temperature:
210 |
211 | 212 |
213 |
Thermostat
214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 |
Setpoint Temperature:Current Temperature:
Mode:
226 |
227 | 228 |
229 |
Solar Module
230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 |
Colector Temperature:Bottom Temperature:
Pump Modulation:Pump Active:
Energy Last Hour:Energy Today:Energy Total:
252 |
253 | 254 |
255 |
Heat Pump
256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 |
Pump Modulation:Pump Speed:
264 |
265 | 266 |
267 |
268 |
269 | 270 |
271 |
272 |
--------------------------------------------------------------------------------