├── .gitattributes ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ ├── platform-io-ci.yml │ ├── platform-io-release.yml │ ├── scorecards.yml │ └── codeql.yml ├── dashboard.json ├── platformio.ini ├── LICENSE ├── src ├── rc_switch_output.h └── main.cpp ├── configuration.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | time: "04:00" 10 | assignees: 11 | - pfichtner 12 | 13 | -------------------------------------------------------------------------------- /dashboard.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "WiFi_RSSI", 4 | "type": "int8_t", 5 | "direction": "display", 6 | "display": "graph", 7 | "xaxis": 50 8 | }, 9 | { 10 | "name": "MQTT_Connected", 11 | "type": "bool", 12 | "direction": "display" 13 | }, 14 | { 15 | "name": "dialInProgress", 16 | "type": "bool", 17 | "direction": "display" 18 | }, 19 | { 20 | "name": "rcswitch_value", 21 | "type": "uint32_t", 22 | "length": 8, 23 | "direction": "display" 24 | }, 25 | { 26 | "name": "rcswitch_protocol", 27 | "type": "uint8_t", 28 | "length": 8, 29 | "direction": "display" 30 | } 31 | 32 | ] -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:nodemcuv2] 12 | platform = espressif8266@3.0.0 13 | board = nodemcuv2 14 | framework = arduino 15 | board_build.f_cpu = 160000000L 16 | upload_speed = 921600 17 | monitor_speed = 115200 18 | build_type = debug 19 | monitor_filters = esp8266_exception_decoder 20 | lib_deps = 21 | ESP8266 IoT Framework 22 | ArduinoJson 23 | me-no-dev/ESP Async WebServer 24 | knolleary/PubSubClient 25 | https://github.com/1technophile/rc-switch.git 26 | https://github.com/pfichtner/ArduinoSIP.git#dtmf-signaling 27 | 28 | build_flags = -DCONFIG_PATH=configuration.json -DDASHBOARD_PATH=dashboard.json -DREBUILD_HTML 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Peter Fichtner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Harden Runner 18 | uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 19 | with: 20 | egress-policy: audit 21 | 22 | - name: 'Checkout Repository' 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | - name: 'Dependency Review' 25 | uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 26 | -------------------------------------------------------------------------------- /.github/workflows/platform-io-ci.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO CI 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | schedule: 7 | # each first day of month 8 | - cron: "0 0 1 * *" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Harden Runner 16 | uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 17 | with: 18 | egress-policy: audit 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | - name: Cache pip 21 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 25 | restore-keys: | 26 | ${{ runner.os }}-pip- 27 | - name: Cache PlatformIO 28 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 29 | with: 30 | path: ~/.platformio 31 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 32 | - name: Install python 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: '3.11' 36 | - run: pip install platformio==6.1.5 37 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 38 | with: 39 | node-version: 18 40 | - name: Run PlatformIO 41 | # run: pio run -e -e -e 42 | run: pio run 43 | - name: Archive built firmware image 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: firmware.bin 47 | path: .pio/build/nodemcuv2/firmware.bin 48 | -------------------------------------------------------------------------------- /.github/workflows/platform-io-release.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO Release 2 | 3 | on: 4 | # release: 5 | # types: [published] 6 | push: 7 | tags: 8 | - '*' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 21 | with: 22 | egress-policy: audit 23 | 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - name: Cache pip 26 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 27 | with: 28 | path: ~/.cache/pip 29 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 30 | restore-keys: | 31 | ${{ runner.os }}-pip- 32 | - name: Cache PlatformIO 33 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 34 | with: 35 | path: ~/.platformio 36 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 37 | - name: Install python 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: '3.11' 41 | - run: pip install platformio==6.1.5 42 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 43 | with: 44 | node-version: 18 45 | - name: Archive built firmware image 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: firmware.bin 49 | path: .pio/build/nodemcuv2/firmware.bin 50 | - name: Run PlatformIO 51 | # run: pio run -e -e -e 52 | run: pio run 53 | - name: Release 54 | uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 55 | with: 56 | artifacts: ".pio/build/nodemcuv2/firmware.bin" 57 | 58 | -------------------------------------------------------------------------------- /src/rc_switch_output.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static const char *bin2tristate(const char *bin); 4 | static char *dec2binWzerofill(unsigned long Dec, unsigned int bitLength); 5 | 6 | void output(unsigned long decimal, unsigned int length, unsigned int delay, unsigned int *raw, unsigned int protocol) 7 | { 8 | const char *b = dec2binWzerofill(decimal, length); 9 | Serial.print("Decimal: "); 10 | Serial.print(decimal); 11 | Serial.print(" ("); 12 | Serial.print(length); 13 | Serial.print("Bit) Binary: "); 14 | Serial.print(b); 15 | Serial.print(" Tri-State: "); 16 | Serial.print(bin2tristate(b)); 17 | Serial.print(" PulseLength: "); 18 | Serial.print(delay); 19 | Serial.print(" microseconds"); 20 | Serial.print(" Protocol: "); 21 | Serial.println(protocol); 22 | 23 | Serial.print("Raw data: "); 24 | for (unsigned int i = 0; i <= length * 2; i++) 25 | { 26 | Serial.print(raw[i]); 27 | Serial.print(","); 28 | } 29 | Serial.println(); 30 | Serial.println(); 31 | } 32 | 33 | static const char *bin2tristate(const char *bin) 34 | { 35 | static char returnValue[50]; 36 | int pos = 0; 37 | int pos2 = 0; 38 | while (bin[pos] != '\0' && bin[pos + 1] != '\0') 39 | { 40 | if (bin[pos] == '0' && bin[pos + 1] == '0') 41 | { 42 | returnValue[pos2] = '0'; 43 | } 44 | else if (bin[pos] == '1' && bin[pos + 1] == '1') 45 | { 46 | returnValue[pos2] = '1'; 47 | } 48 | else if (bin[pos] == '0' && bin[pos + 1] == '1') 49 | { 50 | returnValue[pos2] = 'F'; 51 | } 52 | else 53 | { 54 | return "not applicable"; 55 | } 56 | pos = pos + 2; 57 | pos2++; 58 | } 59 | returnValue[pos2] = '\0'; 60 | return returnValue; 61 | } 62 | 63 | static char *dec2binWzerofill(unsigned long Dec, unsigned int bitLength) 64 | { 65 | static char bin[64]; 66 | unsigned int i = 0; 67 | 68 | while (Dec > 0) 69 | { 70 | bin[32 + i++] = ((Dec & 1) > 0) ? '1' : '0'; 71 | Dec = Dec >> 1; 72 | } 73 | 74 | for (unsigned int j = 0; j < bitLength; j++) 75 | { 76 | if (j >= bitLength - i) 77 | { 78 | bin[j] = bin[31 + i - (j - (bitLength - i))]; 79 | } 80 | else 81 | { 82 | bin[j] = '0'; 83 | } 84 | } 85 | bin[bitLength] = '\0'; 86 | 87 | return bin; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["main"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | 32 | steps: 33 | - name: Harden Runner 34 | uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 35 | with: 36 | egress-policy: audit 37 | 38 | - name: "Checkout code" 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: "Upload artifact" 66 | uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: "Upload to code-scanning" 74 | uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["main"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["main"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["cpp"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Harden Runner 44 | uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 45 | with: 46 | egress-policy: audit 47 | 48 | - name: Checkout repository 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 61 | # If this step fails, then you should remove it and run the build manually (see below) 62 | # - name: Autobuild 63 | # uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 64 | 65 | # ℹ️ Command-line programs to run using the OS shell. 66 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 67 | 68 | # If the Autobuild fails above, remove it and uncomment the following three lines. 69 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 70 | 71 | # - run: | 72 | # echo "Run, Build Application using script" 73 | # ./location_of_script_within_repo/buildscript.sh 74 | 75 | - name: Cache PlatformIO 76 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 77 | with: 78 | path: ~/.platformio 79 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 80 | 81 | - name: Install python 82 | uses: actions/setup-python@v5 83 | with: 84 | python-version: '3.11' 85 | - run: pip install platformio==6.1.5 86 | 87 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 88 | with: 89 | node-version: 18 90 | - name: Run PlatformIO 91 | run: pio run 92 | 93 | - name: Perform CodeQL Analysis 94 | uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 95 | with: 96 | category: "/language:${{matrix.language}}" 97 | -------------------------------------------------------------------------------- /configuration.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "projectName", 4 | "label": "Project Name", 5 | "type": "char", 6 | "disabled": true, 7 | "hidden": true, 8 | "length": 32, 9 | "value": "Doorbell" 10 | }, 11 | { 12 | "name": "language", 13 | "type": "char", 14 | "length": 3, 15 | "value": "en", 16 | "hidden": true 17 | }, 18 | 19 | 20 | { 21 | "name": "mqtt_server", 22 | "label": "MQTT Server IP/FQDN", 23 | "type": "char", 24 | "length": 20, 25 | "value": "mqtt" 26 | }, 27 | { 28 | "name": "mqtt_port", 29 | "label": "MQTT Port", 30 | "type": "int16_t", 31 | "value": 1883 32 | }, 33 | { 34 | "name": "mqtt_user", 35 | "label": "MQTT User Name", 36 | "type": "char", 37 | "length": 20, 38 | "value": "" 39 | }, 40 | { 41 | "name": "mqtt_password", 42 | "label": "MQTT Password", 43 | "type": "char", 44 | "length": 20, 45 | "value": "" 46 | }, 47 | { 48 | "name": "wifi_hostname", 49 | "label": "WiFi Hostname", 50 | "type": "char", 51 | "length": 20, 52 | "value": "doorbell" 53 | }, 54 | 55 | 56 | { 57 | "name": "rcswitch_gpiopin", 58 | "label": "RC Receiver GPIO Pin", 59 | "type": "int16_t", 60 | "value": "5" 61 | }, 62 | { 63 | "name": "rcswitch_protocol", 64 | "label": "RC protocol (-1 for any)", 65 | "type": "int16_t", 66 | "value": "-1" 67 | }, 68 | { 69 | "name": "rcswitch_value", 70 | "label": "RC code as decimal", 71 | "type": "uint32_t", 72 | "length": 8, 73 | "value": "0" 74 | }, 75 | { 76 | "name": "button_gpiopin", 77 | "label": "GPIO Pin to connect a push button which triggers a call", 78 | "type": "int16_t", 79 | "value": "-1" 80 | }, 81 | { 82 | "name": "button_gpiopin_mode", 83 | "label": "Input mode of the GPIO Pin to connect a push button which triggers a call", 84 | "type": "char", 85 | "length": 20, 86 | "control": "select", 87 | "options": ["", "PULLUP", "PULLUP (internal)", "PULLDOWN"], 88 | "value": "" 89 | }, 90 | { 91 | "name": "switch_gpiopin", 92 | "label": "GPIO Pin switched HIGH when button press detected", 93 | "type": "int16_t", 94 | "value": "-1" 95 | }, 96 | 97 | 98 | { 99 | "name": "sip_server", 100 | "label": "SIP Server IP/FQDN", 101 | "type": "char", 102 | "length": 20, 103 | "value": "fritz.box" 104 | }, 105 | { 106 | "name": "sip_port", 107 | "label": "SIP Port", 108 | "type": "int16_t", 109 | "value": 5060 110 | }, 111 | { 112 | "name": "sip_user", 113 | "label": "SIP User Name", 114 | "type": "char", 115 | "length": 20, 116 | "value": "" 117 | }, 118 | { 119 | "name": "sip_password", 120 | "label": "SIP Password", 121 | "type": "char", 122 | "length": 20, 123 | "value": "" 124 | }, 125 | { 126 | "name": "sip_ringsecs", 127 | "label": "Ring Time Seconds", 128 | "type": "int16_t", 129 | "value": "15" 130 | }, 131 | { 132 | "name": "sip_numbertodial", 133 | "label": "SIP Number To Dial", 134 | "type": "char", 135 | "length": 20, 136 | "value": "**9" 137 | }, 138 | { 139 | "name": "sip_callername", 140 | "label": "SIP Name of Caller", 141 | "type": "char", 142 | "length": 20, 143 | "value": "Doorbell #1" 144 | } 145 | 146 | ] 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PlatformIO CI](https://github.com/pfichtner/SipDoorbell/actions/workflows/platform-io-ci.yml/badge.svg)](https://github.com/pfichtner/SipDoorbell/actions/workflows/platform-io-ci.yml) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | 4 | SipDoorBell 5 | ----------- 6 | 7 | ESP8266-based doorbell that does initiate SIP calls. 8 | 9 | Why not...? 10 | ----------- 11 | - Wifi Türklingel, ESP-Überallklingel and others: 12 | - no 433 MHz support 13 | - projects rely on TR-064 14 | - hard coded values (WiFi settings, SIP settings, ...) (on the other side: This is a security feature) 15 | 16 | - ESPHome 17 | - 433 MHz support very limited 18 | - cannot configure variables that should be on the node (at least not via an included HTML UI) 19 | - couldn't find a way to easily pass parameters (SIP-User, SIP-Password, ...) to an ESPHome custom component 20 | - Perhaps the last two items are wrong, see 21 | - https://esphome.io/web-api/index.html#number (configure values stored on the node) 22 | - https://gist.github.com/liads/c702fd4b8529991af9cd52d03b694814 (configuring an ESPHome custom component) 23 | 24 | Features 25 | -------- 26 | - Configurable via Web-Interface so no need to compile your own firmware, you just need to download and to flash the microcontroller (see https://tasmota.github.io/docs/Getting-Started/#flashing for example for how to flash a D1 mini). Please note: Those values can be changed by anyone having access to your network since there is no authentication/authorization (yet) implemented! 27 | - Acts as normal SIP-Client so it can be used not only with TR064-enabled devices but every SIP-Server/SIP-enabled hardware 28 | - Can can be initiated via 433 MHz signal (e.g. wireless doorbell sender) 29 | - Can can be initiated via push button (momentary switch) (e.g. a simple push button and/or optocoppler to make an existent doorbell a smart doorbell). Supported pin modes are pullup (with and w/o internal resistor, LOW indicates button pressed) and pulldown (HIGH indicates button pressed) 30 | - Can switch GPIO pin on ring (e.g. to visualize via led and/or do some action on another microcontroller) 31 | - Can publish mqtt messages on ring so other tasks can be done via integration platforms like ioBroker, home assistant, node red, ... 32 | 33 | Schematics 34 | ---------- 35 | ![doorbell-breadboard.png](https://pfichtner.github.io/assets/SipDoorbell/doorbell-breadboard.png) 36 | **Each of the following "modules" is optional (except the ESP :smirk:)!** 37 | - left side hand there is the 433 MHz receiver 38 | - in the middle there is a push button (momentary switch). Alternatively you can use an optocoppler to attach an existing door bell (see e.g. https://www.reichelt.de/magazin/projekte/smarte-tuerklingel/). In the example a pull-down resistor is used, you can configure SipDoorBell also to use the bultin resistor or to do pull-up. 39 | - on the right hand side there is a led visualizing a ring. You can configure a GPIO pin to become "HIGH" in case the push button has been pressed or the configured 433 MHz signal was detected. Also here could be used an optocoppler to switch any other device, e.g. another microcontroller 40 | 41 | Upload/flash ESP8266 42 | -------------------- 43 | You can flash the firmware like other firmwares (e.g. [tasmota](https://tasmota.github.io/docs/)) using tools like 44 | [esptool](https://pypi.org/project/esptool/) or [tasmotizer](https://github.com/tasmota/tasmotizer). 45 | Further updates (or even flashing of another firmware) can then be done via the included OTA (over-the-air) updater using the builtin WebUI. 46 | 47 | TODOs 48 | ----- 49 | - protect HTTP access 50 | - MQTT status updates (online/offline) via LWT 51 | - Listen for offhooking and do not hangup by ourself then (otherwise the call gets killed and the door could possible not being opened) 52 | - Learn mode for 433 MHz signals? (could be realized via hw-button as well via sw-button on the dashboard) 53 | - Support for door relais, see https://www.mikrocontroller.net/topic/444994#5814241 https://de.wikipedia.org/wiki/Mehrfrequenzwahlverfahren#%C3%9Cbertragungsmodi_bei_Internettelefonie 54 | - Could include TR-064 as well as alternative to SIP 55 | 56 | Projects used 57 | ------------- 58 | - [ESP8266 IoT Framework](https://github.com/maakbaas/esp8266-iot-framework) 59 | - [PubSubClient](https://github.com/knolleary/pubsubclient) 60 | - [rc-switch](https://github.com/1technophile/rc-switch) 61 | - Fork of [ArduinoSIP](https://github.com/dl9sec/ArduinoSIP) (https://github.com/pfichtner/ArduinoSIP) 62 | 63 | Possible useful projects 64 | ------------------------ 65 | - https://github.com/RetepRelleum/SipMachine 66 | - https://github.com/rousir/ArduinoVoip 67 | - https://github.com/chrta/sip_call 68 | 69 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "LittleFS.h" 4 | #include "WiFiManager.h" 5 | #include "webServer.h" 6 | #include "updater.h" 7 | #include "configManager.h" 8 | #include "dashboard.h" 9 | #include 10 | #include 11 | 12 | #include "rc_switch_output.h" 13 | 14 | #define DTMF_HANDLING 15 | 16 | // WIFI 17 | WiFiClient espClient; 18 | String localIp; 19 | 20 | // MQTT 21 | PubSubClient mqttClient(espClient); 22 | 23 | // SIP 24 | char acSipIn[2048]; 25 | char acSipOut[2048]; 26 | Sip aSip(acSipOut, sizeof(acSipOut)); 27 | unsigned long dialingStartedAt; 28 | 29 | // RCSWITCH 30 | RCSwitch mySwitch = RCSwitch(); 31 | 32 | // gpio input can be configured pullup and pulldown, so HIGH or LOW could signal a button press 33 | int pin_pressed_is = HIGH; 34 | 35 | // Tasks 36 | struct task 37 | { 38 | unsigned long rate; 39 | unsigned long previous; 40 | }; 41 | 42 | task taskA = {.rate = 10 * 1000, .previous = 0}; 43 | 44 | void mqttPublish(boolean state) 45 | { 46 | String stateTopic = String(configManager.data.wifi_hostname) + "/sensor/state"; 47 | mqttClient.publish(stateTopic.c_str(), state ? "ON" : "OFF"); 48 | } 49 | 50 | void mqttReconnect(void) 51 | { 52 | dash.data.MQTT_Connected = mqttClient.connected(); 53 | if (!dash.data.MQTT_Connected) 54 | { 55 | String clientId = String(configManager.data.projectName); 56 | mqttClient.connect(clientId.c_str(), configManager.data.mqtt_user, configManager.data.mqtt_password); 57 | Serial.print(F("Connected to MQTT Broker, clientId ")); 58 | Serial.println(clientId); 59 | } 60 | } 61 | 62 | void sipBegin(void) 63 | { 64 | // TODO use random port? 65 | // int localPort = random(1 * 1024, 64 * 1024); 66 | int localPort = 5060; 67 | 68 | Serial.print(F("Configuring SIP connection from ")); 69 | Serial.print(localIp); 70 | Serial.print(":"); 71 | Serial.print(localPort); 72 | Serial.print(F(" to ")); 73 | Serial.print(configManager.data.sip_server); 74 | Serial.print(":"); 75 | Serial.println(configManager.data.sip_port); 76 | 77 | aSip.Init(configManager.data.sip_server, configManager.data.sip_port, localIp.c_str(), localPort, configManager.data.sip_user, configManager.data.sip_password, configManager.data.sip_ringsecs); 78 | } 79 | 80 | void mqttBegin(void) 81 | { 82 | if (strlen(configManager.data.mqtt_server) == 0 || configManager.data.mqtt_port <= 0) 83 | { 84 | Serial.println("MQTT not configured"); 85 | } 86 | else 87 | { 88 | mqttClient.setServer(configManager.data.mqtt_server, configManager.data.mqtt_port); 89 | Serial.print(F("Configuring MQTT Broker ")); 90 | Serial.print(configManager.data.mqtt_server); 91 | Serial.print(":"); 92 | Serial.println(configManager.data.mqtt_port); 93 | } 94 | } 95 | 96 | void inputPinBegin(void) 97 | { 98 | if (configManager.data.button_gpiopin > 0) 99 | { 100 | Serial.print(F("GPIO ")); 101 | Serial.print(configManager.data.button_gpiopin); 102 | Serial.print(F(" configured for input button ")); 103 | pinMode(configManager.data.button_gpiopin, INPUT); 104 | 105 | if (strcmp("PULLUP", configManager.data.button_gpiopin_mode) == 0) 106 | { 107 | Serial.println(F("pullup")); 108 | pin_pressed_is = LOW; 109 | } 110 | else if (strcmp("PULLUP (internal)", configManager.data.button_gpiopin_mode) == 0) 111 | { 112 | Serial.println(F("pullup (internal)")); 113 | pinMode(configManager.data.button_gpiopin, INPUT_PULLUP); 114 | pin_pressed_is = LOW; 115 | } 116 | else 117 | { 118 | // PULLDOWN 119 | Serial.println(F("pulldown")); 120 | pin_pressed_is = HIGH; 121 | } 122 | } 123 | else 124 | { 125 | Serial.println("no input button configured"); 126 | } 127 | } 128 | 129 | void outputPinBegin(void) 130 | { 131 | if (configManager.data.switch_gpiopin > 0) 132 | { 133 | pinMode(configManager.data.switch_gpiopin, OUTPUT); 134 | digitalWrite(configManager.data.switch_gpiopin, LOW); 135 | } 136 | } 137 | 138 | void rcSwitchBegin(void) 139 | { 140 | mySwitch.enableReceive(configManager.data.rcswitch_gpiopin); 141 | } 142 | 143 | void configDependendBegins(void) 144 | { 145 | mqttBegin(); 146 | sipBegin(); 147 | 148 | inputPinBegin(); 149 | outputPinBegin(); 150 | rcSwitchBegin(); 151 | } 152 | 153 | void setup() 154 | { 155 | Serial.begin(115200); 156 | 157 | LittleFS.begin(); 158 | GUI.begin(); 159 | configManager.begin(); 160 | configManager.setConfigSaveCallback(configDependendBegins); 161 | WiFiManager.begin(configManager.data.projectName); 162 | dash.begin(500); 163 | WiFi.hostname(configManager.data.wifi_hostname); 164 | WiFi.begin(); 165 | 166 | localIp = WiFi.localIP().toString(); 167 | Serial.println(F("Connected")); 168 | 169 | configDependendBegins(); 170 | } 171 | 172 | void switchPin(boolean state) 173 | { 174 | if (configManager.data.switch_gpiopin > 0) 175 | { 176 | digitalWrite(configManager.data.switch_gpiopin, state ? HIGH : LOW); 177 | Serial.print(F("switched pin ")); 178 | Serial.print(configManager.data.button_gpiopin); 179 | Serial.println(": "); 180 | Serial.println(state ? "HIGH" : "LOW"); 181 | } 182 | } 183 | 184 | void setDialInProgress(boolean dialInProgress) 185 | { 186 | if (dash.data.dialInProgress != dialInProgress) 187 | { 188 | dash.data.dialInProgress = dialInProgress; 189 | switchPin(dialInProgress); 190 | mqttPublish(dialInProgress); 191 | } 192 | } 193 | 194 | void dial(void) 195 | { 196 | if (dash.data.dialInProgress) 197 | { 198 | Serial.println(F("Dialing already in progress")); 199 | return; 200 | } 201 | setDialInProgress(true); 202 | dialingStartedAt = millis(); 203 | Serial.print("dialing "); 204 | Serial.print(configManager.data.sip_numbertodial); 205 | Serial.print(" as "); 206 | Serial.println(configManager.data.sip_callername); 207 | aSip.Dial(configManager.data.sip_numbertodial, configManager.data.sip_callername); 208 | } 209 | 210 | void buttonLoop(void) 211 | { 212 | if (digitalRead(configManager.data.button_gpiopin) == pin_pressed_is) 213 | { 214 | Serial.print(F("GPIO ")); 215 | Serial.print(configManager.data.button_gpiopin); 216 | Serial.println(F(" button press detected")); 217 | dial(); 218 | } 219 | } 220 | 221 | void sipLoop(void) 222 | { 223 | aSip.Processing(acSipIn, sizeof(acSipIn)); 224 | #ifdef DTMF_HANDLING 225 | // preparation for possible opener 226 | char iSignal = aSip.GetSignal(); 227 | if (iSignal) 228 | { 229 | Serial.print("Signal received: "); 230 | Serial.println(iSignal); 231 | } 232 | #endif 233 | } 234 | 235 | void rcSwitchLoop(void) 236 | { 237 | if (mySwitch.available()) 238 | { 239 | dash.data.rcswitch_value = mySwitch.getReceivedValue(); 240 | dash.data.rcswitch_protocol = mySwitch.getReceivedProtocol(); 241 | output(mySwitch.getReceivedValue(), mySwitch.getReceivedBitlength(), mySwitch.getReceivedDelay(), mySwitch.getReceivedRawdata(), mySwitch.getReceivedProtocol()); 242 | if (mySwitch.getReceivedValue() == configManager.data.rcswitch_value && (configManager.data.rcswitch_protocol < 0 || mySwitch.getReceivedProtocol() == configManager.data.rcswitch_protocol)) 243 | { 244 | Serial.println(F("rc-code matching")); 245 | dial(); 246 | } 247 | else 248 | { 249 | Serial.println(F("rc-code does not match")); 250 | } 251 | mySwitch.resetAvailable(); 252 | } 253 | } 254 | 255 | void loop() 256 | { 257 | // framework things 258 | WiFiManager.loop(); 259 | updater.loop(); 260 | configManager.loop(); 261 | dash.loop(); 262 | mqttClient.loop(); 263 | 264 | unsigned long now = millis(); 265 | 266 | if (dash.data.dialInProgress && now >= dialingStartedAt + configManager.data.sip_ringsecs * 1000) 267 | { 268 | setDialInProgress(false); 269 | } 270 | 271 | buttonLoop(); 272 | sipLoop(); 273 | rcSwitchLoop(); 274 | 275 | // tasks 276 | if (taskA.previous == 0 || (now - taskA.previous > taskA.rate)) 277 | { 278 | taskA.previous = now; 279 | dash.data.WiFi_RSSI = WiFi.RSSI(); 280 | mqttReconnect(); 281 | } 282 | } 283 | --------------------------------------------------------------------------------