├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ansible.cfg ├── coachproxyos.yml ├── docs ├── Build_Device.md ├── Build_Image.md ├── Download_Image.md ├── Roadmap.md ├── Software_Overview.md └── Tiffin_RVC_Notes.md ├── images ├── ansible-1.png ├── canbus_wiring.jpg ├── flows-1.jpg └── ui-interior.png ├── inventory └── hosts.yml └── roles ├── base_os ├── files │ ├── logrotate.conf │ └── public_keys │ │ └── root-coachproxy └── tasks │ └── main.yml ├── coachproxy ├── files │ ├── VERSION │ ├── bin │ │ ├── apply_remote_access_settings.sh │ │ ├── coachproxy_watchdog.sh │ │ ├── cplog.sh │ │ ├── reset_firewall.sh │ │ ├── rvc2mqtt.pl │ │ ├── safe_reboot │ │ ├── safe_shutdown │ │ ├── serialnumber.sh │ │ ├── version.sh │ │ ├── wifi_link_quality.sh │ │ └── wifi_mqtt.pl │ ├── configurator │ │ ├── cp_config.pl │ │ ├── features.json │ │ ├── flows_coachproxy-template.json │ │ ├── node_changer.pl │ │ └── node_remover.pl │ ├── etc │ │ ├── cron.d │ │ │ └── coachproxy │ │ ├── dpkg │ │ │ └── dpkg.cfg.d │ │ │ │ └── 01_nodoc │ │ ├── hosts │ │ ├── mosquitto │ │ │ └── mosquitto.conf │ │ ├── nginx │ │ │ └── sites-available │ │ │ │ ├── 10-CoachProxy.conf │ │ │ │ └── 20-SSLRedirect.conf │ │ ├── ngrok.conf-TEMPLATE │ │ ├── profile.d │ │ │ └── coachproxy.sh │ │ ├── rc.local.coachproxy │ │ ├── resolv.conf │ │ ├── resolvconf.conf │ │ ├── rvc-spec.yml │ │ ├── systemd │ │ │ ├── network │ │ │ │ ├── 04-eth0.network │ │ │ │ └── 12-wlan0.network │ │ │ └── system │ │ │ │ └── habridge.service │ │ └── vim │ │ │ └── vimrc.local │ ├── ha-bridge │ │ ├── device.db │ │ ├── device.db-blank │ │ ├── disable │ │ ├── enable │ │ ├── ha-bridge-5.2.1.jar │ │ ├── ha-bridge.jar │ │ ├── habridge.config.template │ │ ├── re-init │ │ └── re-ip.sh │ ├── log │ │ └── .gitignore │ ├── node-red │ │ ├── coachproxy.sqlite-base │ │ ├── flows_coachproxy-base.json │ │ ├── js │ │ │ ├── ats_source_decode.js │ │ │ ├── ceiling_fan_decode.js │ │ │ ├── command_dc_dimmer.js │ │ │ ├── decode_chassis_mobility.js │ │ │ ├── decode_dc_dimmer_light.js │ │ │ ├── decode_dc_source.js │ │ │ ├── decode_fan.js │ │ │ ├── decode_floor_ambient_status.js │ │ │ ├── decode_floor_status.js │ │ │ ├── decode_tank.js │ │ │ ├── decode_vent.js │ │ │ ├── emails_parse.js │ │ │ ├── emails_store.js │ │ │ ├── floor_heat_manager.js │ │ │ ├── form_field_save.js │ │ │ ├── function_floor_power.js │ │ │ ├── function_floor_setpoint.js │ │ │ ├── furnace_status_decode.js │ │ │ ├── genset_clear_buffer.js │ │ │ ├── genset_countdown.js │ │ │ ├── genset_safety.js │ │ │ ├── initialize_global_context.js │ │ │ ├── levels_display.js │ │ │ ├── levels_parse.js │ │ │ ├── levels_store.js │ │ │ ├── misc_display.js │ │ │ ├── misc_parse.js │ │ │ ├── misc_store.js │ │ │ ├── network_info.js │ │ │ ├── notification_deliver.js │ │ │ ├── notification_menu_options.js │ │ │ ├── notification_save.js │ │ │ ├── notification_state_monitor.js │ │ │ ├── notification_submenu_options.js │ │ │ ├── notification_value_monitor.js │ │ │ ├── power_flow_function.js │ │ │ ├── preset_delete.js │ │ │ ├── preset_restore.js │ │ │ ├── preset_save.js │ │ │ ├── thermostat_ambient_decode.js │ │ │ ├── thermostat_ambient_decode_ext.js │ │ │ ├── thermostat_command.js │ │ │ ├── thermostat_status_decode.js │ │ │ ├── uptime_parse.js │ │ │ ├── wifi_direct_cancel.js │ │ │ ├── wifi_direct_parse.js │ │ │ ├── wifi_direct_save.js │ │ │ ├── wifi_password_blank.js │ │ │ ├── wifi_scanner_parse.js │ │ │ ├── wifi_static_cancel.js │ │ │ ├── wifi_static_parse.js │ │ │ ├── wifi_static_save.js │ │ │ ├── wifi_wpa_cancel.js │ │ │ ├── wifi_wpa_parse.js │ │ │ └── wifi_wpa_save.js │ │ └── settings.js │ └── rv-c │ │ ├── ac_load.pl │ │ ├── ceiling_fan.pl │ │ ├── dc_ac_combo.pl │ │ ├── dc_dimmer.pl │ │ ├── dc_dimmer_pair.pl │ │ ├── elec_aquahot.pl │ │ ├── floor_heat.pl │ │ ├── generator.pl │ │ ├── lift.pl │ │ ├── locks.pl │ │ ├── master.pl │ │ ├── panel_lights.pl │ │ ├── thermostats.pl │ │ ├── vent_fan.pl │ │ ├── vent_fan_new.pl │ │ ├── vent_lid.pl │ │ └── window_shade.pl └── tasks │ └── main.yml ├── locale-us ├── files │ ├── keyboard │ └── locale └── tasks │ └── main.yml ├── misc ├── files │ ├── CoachProxy_logo.jpg │ ├── icon120x120.png │ ├── icon192x192.png │ └── icon64x64.png └── tasks │ └── main.yml ├── networking └── tasks │ └── main.yml └── node-red ├── files └── font-awesome-animation.min.css └── tasks └── main.yml /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome 2 | 3 | Welcome to CoachProxyOS, the Open Source version of the CoachProxy RV control 4 | system. CoachProxyOS is not under _active_ development, but small improvements 5 | are still being made from time to time. 6 | 7 | Want to help out? Great! Here's what you need to know... 8 | 9 | ## Technologies 10 | 11 | The main technologies CoachProxyOS uses are: 12 | 13 | * Node-RED - an event-driven node.js framework which manages most of the 14 | logic within CoachProxyOS. 15 | * Node-RED Dashboard - a drag-and-drop user interface builder for Node-RED. 16 | * Javascript - for developing the real-time logic within Node-RED. 17 | * Perl - for back-end communication with the RV CAN bus network. 18 | * Perl and JQ - for building the custom user interface for the selected 19 | RV year, model, and floor plan. 20 | * MQTT - a message broker protocol used to pass data between various 21 | components of CoachProxyOS. 22 | * Shell script - for some operating system management scripts. 23 | * Debian Linux - the operating system CoachProxyOS runs on top of. 24 | * NGINX - a reverse-proxy web front-end for the user interface. 25 | * Ansible - to automate the build of the CoachProxyOS SD card images. 26 | 27 | ## Contributing to CoachProxyOS 28 | 29 | When contributing to this repository, please first discuss the change you wish 30 | to make via issue, email, or other method with the owner of this repository. 31 | 32 | * Create issues for any enhancements or fixes you wish to make. 33 | * Before creating a new issue, ensure a similar existing issue doesn't already exist. 34 | * Keep contributions as small as possible, preferably one new feature or fix per version. 35 | 36 | Current issues: https://github.com/rvc-proxy/coachproxy-os/issues 37 | 38 | ## License 39 | 40 | By contributing to this project, you agree that your contributions will 41 | be licensed under its GPL v3.0 License. 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CoachProxyOS 2 | ============ 3 | 4 | CoachProxy**OS** is the **O**pen **S**ource version of the software behind 5 | [CoachProxy](https://coachproxy.com), a device for interfacing 6 | with, monitoring, and managing Tiffin motorhomes. 7 | 8 | This repository provides a set of 9 | [Ansible](https://docs.ansible.com/ansible/latest/index.html) playbooks 10 | to create a functioning CoachProxyOS system image from a base Raspberry 11 | Pi operating system. 12 | 13 | Differences from the commercial CoachProxy Software 14 | --------------------------------------------------- 15 | 16 | CoachProxyOS's RV monitoring and control capabilities are [identical to 17 | those of the commercial CoachProxy 18 | system](https://coachproxy.com/instructions/). However, several changes 19 | have been made to make it suitable for an Open Source Project. The major 20 | changes are: 21 | 22 | * To enable e-mail notifications, users must configure their own 23 | SMTP email server settings in the CoachProxyOS interface (SMTP 24 | server, port number, username, and password). 25 | * To configure WiFi network information, a file must be edited on 26 | the boot partition of the CoachProxy operating system image by 27 | inserting the microSD card in a computer to edit the file. 28 | * CoachProxy's memory card was configured with a read-only filesystem 29 | to prevent corruption of the SD card, for example if power was lost 30 | while a file was being written. This configuration been removed from 31 | CoachProxyOS to reduce complexity and make DIY changes easier. 32 | 33 | Documentation 34 | ------------- 35 | 36 | [Build_Device](docs/Build_Device.md): instructions for assembling a 37 | CoachProxyOS device from a Raspberry Pi computer. 38 | 39 | [Build_Image](docs/Build_Image.md): instructions for creating a 40 | CoachProxyOS image from source using Ansible. 41 | 42 | [Download_Image](docs/Download_Image.md): instructions for downloading 43 | a pre-built image and installing it on a CoachProxyOS device. 44 | 45 | [Software_Overview](docs/Software_Overview.md): information on how the device 46 | and software communicates with an RV, and how the CoachProxyOS software 47 | works. 48 | 49 | [Roadmap](docs/Roadmap.md): information on what future changes would benefit 50 | the project. 51 | 52 | Screenshots 53 | ----------- 54 | 55 | ![Interior](images/ui-interior.png) 56 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = inventory/hosts.yml 3 | command_warnings = False 4 | -------------------------------------------------------------------------------- /coachproxyos.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | - hosts: all 5 | roles: 6 | - locale-us 7 | - base_os 8 | - coachproxy 9 | - node-red # Must be after 'coachproxy' role 10 | - networking 11 | - misc 12 | -------------------------------------------------------------------------------- /docs/Build_Device.md: -------------------------------------------------------------------------------- 1 | Hardware Build 2 | ============== 3 | 4 | To build an RV-C interface device like CoachProxy, a few parts are 5 | required: 6 | 7 | * A Raspberry Pi 3B computer board ($40) 8 | * A PiCAN 2 communications board ($60) 9 | * A case for the combined boards ($10) 10 | * A microSD memory card, any size ($10) 11 | * An unshielded twisted pair (UTP) cable with 2 or 4 wires 12 | * A crimp-on cable connector for the Tiffin/Spyder network port 13 | 14 | Details 15 | ------- 16 | 17 | The Raspberry Pi (RPi) 3 Model B is the recommended hardware choice. An 18 | RPi 3B+ should also work, but has not been tested. An RPi 4 requires 19 | additional power and is _not_ recommended, nor has it been tested. 20 | 21 | To connect and communicate with the RV-C network in the motorhome, a CAN 22 | Bus board must be added to the RPi. A board with built-in SMPS (switch 23 | mode power supply) will enable the RPi to be powered by the CAN Bus 24 | network, eliminating the need for a separate power supply. For example: 25 | https://copperhilltech.com/pican2-can-interface-for-raspberry-pi-with-smps/ 26 | 27 | _Note: If purchasing this PiCan2 board from Copperhill Tech, ensure that 28 | the 120 ohm termination resistor is disabled. To do this, remove any 29 | jumper installed in the JP3 pins on the board (see section 1.6 of the 30 | owners manual that comes with the PiCan2 board)._ 31 | 32 | For connection to a Tiffin motorhome, a 2- or 4-conductor CAN Bus cable 33 | will need to be connected to the PiCAN2 board. A length of Category 6 34 | ethernet cable (contaiing four pairs of wires) can be used, with one 35 | pair of wires used for CAN communication. A second pair of wires may be 36 | usable for 12V power, depending on the wire's gauge. see section 2.1.1 37 | of the [RVC Specification](http://www.rv-c.com/?q=node/75) for more 38 | information (excerpt attached below): 39 | 40 | ![RVC Spec 2.1.1](../images/canbus_wiring.jpg) 41 | 42 | A [3M Mini-Clamp 43 | Plug](https://www.digikey.com/product-detail/en/3m/37104-2165-000%20FL%20100/3M155844-ND/1238214) 44 | connector should be added to the other end of the cable. This connector 45 | will plug into the Tiffin network panel, usually in the bedroom, 46 | bathroom, or closet of the RV. 47 | 48 | Purchasing options 49 | ------------------ 50 | 51 | An RPi 3B+ with PiCAN2 board already attached can be purchased at: 52 | https://copperhilltech.com/raspberry-pi-3-b-system-with-single-can-bus-interface/ 53 | for $150, rather than purchasing the two boards separately and 54 | assembling them. 55 | 56 | If planning to power the device through the Spider network cable, be 57 | sure to select the "Extended Input Power Range" option. 58 | 59 | -------------------------------------------------------------------------------- /docs/Build_Image.md: -------------------------------------------------------------------------------- 1 | Ansible playbooks to build a CoachProxyOS Raspberry Pi image 2 | ============================================================ 3 | 4 | The CoachProxy image build is automated using the 5 | [Ansible](https://docs.ansible.com/ansible/latest/index.html) system 6 | configuration tool. Installing and and configuring Ansible is outside 7 | the scope of these instructions. 8 | 9 | Base Image Setup 10 | ---------------- 11 | 12 | Installing the CoachProxy software requires a network-accessible 13 | Raspberry Pi (hereafter referred to as RPi) running the Raspbian 14 | operating system. Installing Raspbian is outside the scope of this 15 | documentation, however a few links and tips are provided below: 16 | 17 | * At this time, the `2019-09-26-raspbian-buster-lite.img` image is the 18 | latest version that has been tested with CoachProxy, though any version 19 | of Raspbian Buster should work. It can be downloaded at: 20 | [raspbian_lite-2019-09-26](https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-09-30/). 21 | * Instructions for copying the Raspbian image onto an SD card are at: 22 | [Installing Images](https://www.raspberrypi.org/documentation/installation/installing-images/). 23 | * Your local WiFi network SSID and passkey must be configured on the Raspbian 24 | image, and SSH enabled to allow remote login to the RPi. Instructions for 25 | these settings are at: 26 | [Setting up a RPi Headless](https://www.raspberrypi.org/documentation/configuration/wireless/headless.md). 27 | You can optionally connect the RPi via an ethernet cable and skip the WiFi 28 | configuration, but must still enable SSH access via the above 29 | instructions. 30 | * After the base Raspbian image is installed and your WiFi settings and SSH 31 | are configured, insert the SD card into an RPi and power it on. 32 | 33 | Once the RPi is connected to your network, determine its IP address by 34 | checking your router client list, and install your SSH authorized key. 35 | This will allow you (and Ansible) to connect to the RPi without a 36 | password. You'll need to enter the [default Raspbian 37 | password](https://www.raspberrypi.org/documentation/linux/usage/users.md) 38 | to install the key: 39 | 40 | ``` 41 | ssh-copy-id -i ~/.ssh/id_rsa pi@ 42 | ``` 43 | 44 | Next, update the `inventory/hosts.yml` file in this repository with the 45 | RPi IP address so Ansible can connect and continue the configuration. 46 | 47 | Lastly, update the `roles/base_os/files/public_keys/root-coachproxy` file 48 | with your public SSH key. This key will be installed into the root 49 | account on the RPi, enabling you to SSH into the device's root account 50 | if needed. 51 | 52 | CoachProxyOS Ansible Playbook 53 | ----------------------------- 54 | 55 | Run the `coachproxyos.yml` playbook to perform the majority of the 56 | installation and configuration. The install will take 20-40 minutes 57 | and will download quite a bit of data over your Internet connection. 58 | 59 | ``` 60 | $ ansible-playbook coachproxyos.yml 61 | ``` 62 | 63 | ![Ansible playbook output](images/ansible-1.png) 64 | 65 | Note: the install changes the default editor from `nano` to `vim`. If 66 | you are not familiar with `vim`, you should delete the bottom code block 67 | (_Set vim as default editor instead of nano_) from the 68 | `roles/base_os/tasks/main.yml` file before running `ansible-playbook`. 69 | 70 | Creating Reusable Image 71 | ----------------------- 72 | 73 | The device is now ready to use. However, it's advisable to capture the 74 | completed image to a file, so it can be restored in the future rather 75 | than repeating all the above steps. 76 | 77 | To do this, `ssh` into the RPi and issue a `sudo shutdown now` to shut 78 | it down. Move the SD card to another computer and use software on that 79 | computer to save the full SD card image to a file. 80 | 81 | For example, to capture the SD card image on an Ubuntu Linux server, 82 | with the SD card mounted as disk `sdd`: 83 | 84 | ``` 85 | sudo umount /dev/sdd1 /dev/sdd2 86 | sudo dd bs=4M if=/dev/sdd of=coachproxyos_1.0.img 87 | ``` 88 | 89 | After capturing the image, a utility like 90 | [PiShrink](https://github.com/Drewsif/PiShrink) can be used to shrink 91 | the image for use on smaller SD cards. 92 | -------------------------------------------------------------------------------- /docs/Download_Image.md: -------------------------------------------------------------------------------- 1 | Using a Pre-Built Image 2 | ======================= 3 | 4 | For those who don't wish to create their CoachProxyOS operating system 5 | from scratch using Ansible, a ready-to-use downloadable image of the 6 | latest release is usually available. 7 | 8 | Download Software Image 9 | ----------------------- 10 | 11 | Visit the CoachProxyOS 12 | [Releases](https://github.com/rvc-proxy/coachproxy-os/releases) page and 13 | download the latest `.img.zip` file. The image is large (approximately 14 | 1GB). 15 | 16 | Install Image on microSD Card 17 | ----------------------------- 18 | 19 | Download and install the free `Etcher` program from https://etcher.io/ 20 | 21 | Insert a microSD card into your computer or USB card reader. 22 | 23 | Run Etcher, select your downloaded CoachProxyOS_#.img.zip file, select your SD card, and click `Flash`. 24 | 25 | Configure WiFi 26 | -------------- 27 | 28 | After the image has been written, you must edit one file on the `boot` 29 | partition of the microSD card to configure your WiFi network 30 | information. 31 | 32 | _Note: Etcher may have ejected the `boot` partition when it finished writing 33 | the image. If so, just remove the microSD card and re-insert it into the 34 | computer._ 35 | 36 | Follow the instructions at [Setting up a RPi 37 | Headless](https://www.raspberrypi.org/documentation/configuration/wireless/headless.md) 38 | to create the WiFi configuration file for your home network's WiFi SSID 39 | and password. 40 | 41 | The instructions will need you to locate the microSD card's `boot` 42 | partition mounted on your computer and create a new file there called 43 | `wpa_supplicant.conf`. Here is an example of what that file might look 44 | like: 45 | 46 | ~~~ 47 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 48 | update_config=1 49 | country=US 50 | 51 | network={ 52 | ssid="MyWiFiNetworkName" 53 | psk="mywifipassword" 54 | } 55 | ~~~ 56 | 57 | When finished creating the WiFi configuration file, eject the `boot` 58 | partition and remove the microSD card. 59 | 60 | Insert microSD Card into Device 61 | ------------------------------- 62 | 63 | When holding the Raspberry Pi upside down, the label of the microSD card will 64 | be toward you, and the metal pins toward the computer's board. 65 | 66 | You're now ready to connect the device to your RV's network. After the 67 | device starts up, you'll need to determine what network IP address your 68 | router assigned to it, and enter that address into any web browser on 69 | the same WiFi network. 70 | -------------------------------------------------------------------------------- /docs/Roadmap.md: -------------------------------------------------------------------------------- 1 | Roadmap 2 | ======= 3 | 4 | CoachProxyOS is _not_ being actively developed. However, the following 5 | is a rough outline of potential improvements that could benefit the 6 | project: 7 | 8 | * 2020 Tiffin support: Most of this could be accomplished by updating 9 | the [features.json](roles/coachproxy/files/configurator/features.json) 10 | file with the correct IDs for 2020 RVs. However, some more complicated 11 | changes will almost certainly be needed in other files as well. 12 | 13 | * Simplify management of configuration information. Currently a sqlite3 14 | database is used, but data is stored in several unrelated and 15 | differently managed tables. A consistent method should be designed and 16 | all code updated. One approach may be to eliminate the sqlite3 17 | database entirely and use 18 | [Persistent Context](https://discourse.nodered.org/t/a-guide-to-understanding-persistent-context/4115). 19 | 20 | * Write documentation for various features, such as Remote 21 | Access, notifications, presets, and Alexa integration. 22 | -------------------------------------------------------------------------------- /docs/Tiffin_RVC_Notes.md: -------------------------------------------------------------------------------- 1 | Thermostats 2 | ----------- 3 | 4 | Thermostats were first added in 2018. 5 | 6 | Thermostat differences: 7 | 8 | * Open Road and RED use instances 0, 1, and 2 for front, middle, and 9 | rear roof unit zones. 10 | * Phaeton, Bus, and Zephyr use instances 2, 3, and 4 for front, middle, 11 | and rear roof units. Everything is two higher because these coaches 12 | reserve instances 0 and 1 for the front and rear heated floors. 13 | 14 | Instance numbers apply to `THERMOSTAT_AMBIENT_STATUS`, `THERMOSTAT_STATUS`, 15 | `THERMOSTAT_COMMAND`, and related commands. 16 | 17 | Furnace differences: 18 | 19 | * Open Road and Allegro RED use instances 3 and 4 for front and rear furnace zones. 20 | * Phaeton, Bus, and Zephyr use instances 5 and 6 for front and rear furnace zones. 21 | 22 | First furnace instance is always 3 plus first roof unit instance. 23 | 24 | Furnaces do not report `THERMOSTAT_AMBIENT_STATUS`, so the instances only 25 | apply to `THERMOSTAT_STATUS` and `THERMOSTAT_COMMAND`. 26 | 27 | Thermistors: 28 | 29 | * Phaeton, Bus, and Zephyr use instances 6 and 7 for 30 | `THERMOSTAT_AMBIENT_STATUS` readings from two exterior thermistors: wet 31 | bay, and generator bay. 32 | 33 | Shorter coaches may only have two HVAC zones. In these cases, the middle 34 | zone is the missing zone. 35 | 36 | Allegro REDs with three zones do not have a heat pump in the center 37 | zone, just an air conditioner. 38 | 39 | 40 | Vent lids 41 | --------- 42 | 43 | Vent lids use reversing loads, and require two commands to make them go 44 | up or down. For example, in the 2018 Phaeton 40 IH, the rear vent lid 45 | uses dimmer loads 33 and 34. To open the lid: 46 | 47 | `DC_DIMMER_COMMAND_2` is sent to load ID 34 with values: 48 | 49 | * Brightness: 0 50 | * Command: Off (Delay) 51 | * Duration: 0 52 | 53 | `DC_DMMER_COMMAND_2` is then also sent to load ID 33 with values: 54 | 55 | * Brightness: 100 56 | * Command: On (Duration) 57 | * Duration: 20 seconds 58 | 59 | To close the lid, ID 33 is set to "0, Off, 0" and 34 is set to "100, On, 20". 60 | 61 | For coaches before the 2018 Phaeton, a separate command must also be 62 | sent to update the indicator light on the Spyder keypad. 63 | 64 | `GENERIC_INDICATOR_COMMAND` is sent to the indicator ID with one of these 65 | sets of parameters: 66 | 67 | * Function: 03 - LED 1 off, LED 2 on 68 | * Function: 02 - LED 1 on, LED 2 off 69 | 70 | 71 | Panel Lights 72 | ------------ 73 | 74 | Panel lights are turned on, off, and dimmed as follows: 75 | 76 | `GENERIC_INDICATOR_COMMAND` is sent with values: 77 | 78 | * Group: (panel ID) 79 | * Brightness: (desired brightness) 80 | * Function: 00 (set brightness for LED 1 and LED 2) 81 | -------------------------------------------------------------------------------- /images/ansible-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/images/ansible-1.png -------------------------------------------------------------------------------- /images/canbus_wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/images/canbus_wiring.jpg -------------------------------------------------------------------------------- /images/flows-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/images/flows-1.jpg -------------------------------------------------------------------------------- /images/ui-interior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/images/ui-interior.png -------------------------------------------------------------------------------- /inventory/hosts.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | cp-build: 4 | ansible_host: 192.168.50.114 5 | ansible_user: pi 6 | ansible_become_pass: raspberry 7 | -------------------------------------------------------------------------------- /roles/base_os/files/logrotate.conf: -------------------------------------------------------------------------------- 1 | # see "man logrotate" for details 2 | daily 3 | 4 | # keep 4 weeks worth of backlogs 5 | rotate 7 6 | 7 | # rotate at 1MB even if a full day hasn't elapsed 8 | maxsize 1M 9 | 10 | # create new (empty) log files after rotating old ones 11 | create 12 | 13 | # uncomment this if you want your log files compressed 14 | #compress 15 | 16 | # packages drop log rotation information into this directory 17 | include /etc/logrotate.d 18 | 19 | # no packages own wtmp, or btmp -- we'll rotate them here 20 | /var/log/wtmp { 21 | missingok 22 | weekly 23 | create 0664 root utmp 24 | rotate 1 25 | } 26 | 27 | /var/log/btmp { 28 | missingok 29 | weekly 30 | create 0660 root utmp 31 | rotate 1 32 | } 33 | 34 | # system-specific logs may be configured here 35 | /coachproxy/log/cp.log { 36 | size 100k 37 | rotate 3 38 | missingok 39 | } 40 | -------------------------------------------------------------------------------- /roles/base_os/files/public_keys/root-coachproxy: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYhQ6Q1zHuzj1o4GF5EFLp69PnUGUjt4JzxgNMOU9aLvAs2NbLDyKMnNLiQQLjTl9ofAKYhAZnkNGlz60Vm0V+wJwauJZ16Q0KE00gkKdwzOZqminiR/yv1KpQ2gPh8UP4M/tby5lWAQrOg9iwTBgbYTHplk1yNNG7D3+o6nEgGTXZqIb4bv4Eq5RwrmEb+Ted5l0b3pr9RXUDRK0InYKz9kD0vwmvbBzNYrQ5Kv12s78vv/Fn4ygztSPQON1qBCCCuR4vObIqHXHprSjX1NJvfrYqCRKy3weDvbJUNVMfGO65R4iXpJZ/vNSr20aCSn6g/Q1TD+iVeFKl8Q8Xk+QV root@coachproxy 2 | -------------------------------------------------------------------------------- /roles/base_os/tasks/main.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | - name: Set hostname to coachproxyos 5 | copy: 6 | content: coachproxyos 7 | dest: /etc/hostname 8 | become: true 9 | 10 | - name: Disable automatic daily APT updates 11 | systemd: 12 | name: "{{ item }}" 13 | enabled: false 14 | state: stopped 15 | become: true 16 | with_items: 17 | - apt-daily.timer 18 | - apt-daily.service 19 | - apt-daily-upgrade.timer 20 | - apt-daily-upgrade.service 21 | 22 | - name: Ensure existing packages are up to date 23 | apt: 24 | upgrade: dist 25 | update_cache: true 26 | become: true 27 | 28 | # This fails when mixed in with the other packages, so it's handled separately here. 29 | - name: Ensure Java 8 JRE is installed 30 | become: true 31 | apt: 32 | name: openjdk-8-jre-headless 33 | state: present 34 | 35 | - name: Ensure required packages are installed 36 | become: true 37 | apt: 38 | name: "{{ packages }}" 39 | state: present 40 | vars: 41 | packages: 42 | # Convenience tools used for managing the system 43 | - tmux 44 | - tcpdump 45 | - vim 46 | - rsync 47 | # sqlite3 is used to store user preferences 48 | - sqlite3 49 | - libsqlite3-0 50 | # can-utils is required to interact with the can bus 51 | - can-utils 52 | # cpanminus used to install required Perl modules 53 | - cpanminus 54 | # ipcalc is used by static IP code to ensure user didn't enter invalid gw 55 | - ipcalc 56 | # jq is used to edit nodered json flows files 57 | - jq 58 | # mosquitto is the MQTT message broker used for communications 59 | - mosquitto 60 | # nginx is used as a reverse web proxy in front of node-red 61 | - nginx 62 | # ntp and ntpdate set the system clock via the Internet 63 | - ntp 64 | - ntpdate 65 | # lockfile-progs is used in various scripts 66 | - lockfile-progs 67 | 68 | - name: Remove unneeded packages to save space 69 | become: true 70 | apt: 71 | name: "{{ packages }}" 72 | state: absent 73 | autoremove: true 74 | autoclean: true 75 | purge: true 76 | vars: 77 | packages: 78 | - libnss-mdns 79 | - avahi-daemon 80 | - manpages-dev 81 | - debconf-i18n 82 | 83 | - name: Remove apt package cache (usually 100+ MB) 84 | command: apt-get clean 85 | become: true 86 | 87 | - name: Remove /var/cache/man (usually 20+ MB) 88 | file: 89 | path: /var/cache/man 90 | state: absent 91 | become: true 92 | 93 | - name: Install required Perl modules 94 | cpanm: 95 | name: "{{ item }}" 96 | system_lib: true 97 | become: true 98 | with_items: 99 | - Switch 100 | - Wifi::WpaCtrl 101 | - Net::MQTT::Simple 102 | - JSON 103 | - JSON::Parse 104 | - DBD::SQLite 105 | - YAML::Tiny 106 | 107 | - name: Install root authorized_keys 108 | authorized_key: 109 | user: root 110 | state: present 111 | key: "{{ item }}" 112 | become: true 113 | with_file: 114 | - public_keys/root-coachproxy 115 | 116 | - name: Set up agressive log file rotation and pruning 117 | copy: 118 | src: logrotate.conf 119 | dest: /etc/logrotate.conf 120 | become: true 121 | 122 | - name: Check default editor 123 | command: update-alternatives --query editor 124 | register: current_default_editor 125 | changed_when: false 126 | 127 | - name: Set vim as default editor instead of nano 128 | command: update-alternatives --set editor /usr/bin/vim.basic 129 | when: "current_default_editor.stdout.find('Value: /usr/bin/vim.basic') == -1" 130 | become: true 131 | -------------------------------------------------------------------------------- /roles/coachproxy/files/VERSION: -------------------------------------------------------------------------------- 1 | 1.1 2 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/apply_remote_access_settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # apply_remote_access_settings.sh 19 | # Read the Remote Access settings from the sqlite database, update 20 | # the ngrok.conf file with the new values, and stop or start the 21 | # tunnel. 22 | 23 | log=/coachproxy/bin/cplog.sh 24 | script=$(basename $0 .sh) 25 | conf=/coachproxy/etc/ngrok.conf 26 | 27 | log () { 28 | if [[ "$silent" = false ]]; then 29 | $log "$script $1 Stopping ngrok tunnel." 30 | fi 31 | } 32 | 33 | send_message () { 34 | /usr/local/bin/mqtt-simple -h localhost -p "CP/MESSAGE/REMOTEACCESS" -m "$1" 35 | } 36 | 37 | disable () { 38 | result=$(echo "INSERT OR REPLACE INTO settings2 (key, value) VALUES('remote_access', 'false')" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 39 | } 40 | 41 | abort () { 42 | send_message "$1" 43 | echo "$1" 44 | log "$script $1 Stopping ngrok tunnel." 45 | sudo killall ngrok 46 | disable 47 | exit 0; 48 | } 49 | 50 | silent=false 51 | if [[ $1 == "--silent" ]]; then 52 | silent=true 53 | else 54 | send_message "Applying remote access settings..." 55 | fi 56 | 57 | # Wait a second to ensure node-red finishes updating the database 58 | sleep 1 59 | 60 | # Get the settings from the database. 61 | user=$(echo "select value from settings2 where key='remote_username';" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 62 | pass=$(echo "select value from settings2 where key='remote_password';" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 63 | auth=$(echo "select value from settings2 where key='ngrok_auth';" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 64 | enabled=$(echo "select value from settings2 where key='remote_access';" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 65 | domain=$(echo "select value from settings2 where key='remote_subdomain';" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 66 | 67 | if [[ $enabled != 'true' ]]; then 68 | abort "Remote access is disabled." 69 | fi 70 | 71 | if [[ ${#auth} -lt 5 ]]; then 72 | abort "ERROR: ngrok Authtoken is missing." 73 | fi 74 | 75 | if [[ ${#user} -lt 1 ]]; then 76 | abort "ERROR: Username is missing." 77 | fi 78 | 79 | if [[ ${#pass} -lt 1 ]]; then 80 | abort "ERROR: Password is missing." 81 | fi 82 | 83 | # If a custom subdomain is used, bind the main tunnel to https only. 84 | # The secondary tunnel will then be bound to http using the same 85 | # domain name. If a custom subdomain is not used, bind the main tunnel 86 | # to both http and https, since the secondary tunnel will have a 87 | # completely different URL which won't be usable by the user. 88 | bind_tls=true 89 | if [[ ${#domain} -lt 1 ]]; then 90 | bind_tls=both 91 | fi 92 | 93 | # Rebuild config file 94 | log "$script update ngrok.conf with latest user settings" 95 | sed "${conf}-TEMPLATE" -e "s/AUTHTOKEN/$auth/" -e "s/USERNAME/$user/" -e "s/PASSWORD/$pass/" -e "s/BIND_TLS/$bind_tls/" -e "s/SUBDOMAIN/$domain/" > $conf 96 | 97 | # Restart tunnel 98 | log "$script restarting ngrok tunnel." 99 | send_message "Restarting remote access system. See above for URL." 100 | sudo killall ngrok 101 | sleep 2 102 | /usr/local/bin/ngrok start -config /coachproxy/etc/ngrok.conf --all & 103 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/coachproxy_watchdog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # coachproxy_watchdog.sh 19 | # Should be run from cron every minute to ensure that things are 20 | # working properly. 21 | 22 | LOG=/coachproxy/bin/cplog.sh 23 | SCRIPT=$(basename $0 .sh) 24 | 25 | # If canbus is down, keep trying to bring it up. 26 | WASDOWN=0 27 | while [ $(ip a | grep can0 | grep -c DOWN) -gt 0 ]; do 28 | $LOG "$SCRIPT can0 is DOWN. Attempting to restart..." 29 | WASDOWN=1 30 | sudo /sbin/ip link set can0 up type can bitrate 250000 31 | sleep 5 32 | done 33 | 34 | if [ $WASDOWN -eq 1 ]; then 35 | $LOG "$SCRIPT can0 interface is now up." 36 | fi 37 | 38 | # If rvc2mqtt.pl is down, start it in background. 39 | if [ -z $(/bin/pidof -x rvc2mqtt.pl) ]; then 40 | $LOG "$SCRIPT rvc2mqtt.pl is down. Starting it..." 41 | /coachproxy/bin/rvc2mqtt.pl &> /dev/null & 42 | fi 43 | 44 | # Start WiFi MQTT publisher if it's not running. 45 | if [ $(/bin/pidof -x wifi_mqtt.pl | wc -w) -eq 0 ]; then 46 | $LOG "$SCRIPT wifi_mqtt.pl is down. Starting it..." 47 | /coachproxy/bin/wifi_mqtt.pl &> /dev/null & 48 | fi 49 | 50 | # If ngrok tunnel is down but should be up, try starting it. 51 | enabled=$(echo "select value from settings2 where key='remote_access';" | sqlite3 /coachproxy/node-red/coachproxy.sqlite) 52 | if [[ $enabled == 'true' ]]; then 53 | if [ -z $(/bin/pidof -x ngrok) ]; then 54 | $LOG "$SCRIPT ngrok tunnel is down. Attempting restart..." 55 | /coachproxy/bin/apply_remote_access_settings.sh --silent &> /dev/null & 56 | fi 57 | fi 58 | 59 | # Delete old lock files in case scripts failed and didn't clean up 60 | sudo find /tmp -name wifi_watchdog.lock -mmin +30 -delete 61 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/cplog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # cplog.sh 19 | # Write messages to /var/log/coachproxy and occasionally clean things up. 20 | 21 | # TODO: Add entry to /var/logrotate.d/coachproxy 22 | 23 | TIMESTAMP=$(date --rfc-3339=seconds) 24 | MESSAGE=$1 25 | LOGFILE=/coachproxy/log/cp.log 26 | 27 | sudo chown pi:pi $LOGFILE 28 | echo $TIMESTAMP $MESSAGE >> $LOGFILE 29 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/reset_firewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | iptables -F 19 | iptables -t nat -F 20 | 21 | iptables -A INPUT -i tun0 -s 10.255.0.0/22 -j ACCEPT 22 | 23 | for m in wlan0 eth0; do 24 | for i in 22 53 67 68 80 443 1880 1883 1900 8080 50000; do 25 | for j in tcp udp; do 26 | iptables -A INPUT -i $m -p $j --dport $i -j ACCEPT 27 | done 28 | done 29 | iptables -A INPUT -i $m -m state --state ESTABLISHED,RELATED -j ACCEPT 30 | iptables -A INPUT -i $m -j DROP 31 | done 32 | 33 | echo 1 > /proc/sys/net/ipv4/ip_forward 34 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/safe_reboot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | LOG=/coachproxy/bin/cplog.sh 19 | SCRIPT=$(basename $0 .sh) 20 | 21 | send_message() { 22 | echo $(date +%F\ %T) "$1" 23 | timeout 2 /usr/local/bin/mqtt-simple -h localhost -p "GLOBAL/SHUTDOWN" -r -m "$1" 24 | } 25 | 26 | $LOG "$SCRIPT rebooting..." 27 | send_message "Reboot: Rebooting now..." 28 | sleep 1 29 | /sbin/fake-hwclock save 30 | shutdown -r now 31 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/safe_shutdown: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | LOG=/coachproxy/bin/cplog.sh 19 | SCRIPT=$(basename $0 .sh) 20 | 21 | send_message() { 22 | echo $(date +%F\ %T) "$1" 23 | timeout 2 /usr/local/bin/mqtt-simple -h localhost -p "GLOBAL/SHUTDOWN" -r -m "$1" 24 | } 25 | 26 | $LOG "$SCRIPT shutting down." 27 | send_message "Shutdown: Shutting down now..." 28 | sleep 1 29 | /sbin/fake-hwclock save 30 | shutdown -h now 31 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/serialnumber.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # serialnumber.sh 19 | # Determine the CoachProxy serial number based on the ethernet hardware 20 | # address. Return the serial as text and publish it to MQTT. 21 | 22 | SERIAL=$(ip a | grep -A1 eth0 | awk '/link\/ether/ {print $2}' | sed -e 's/://g') 23 | 24 | # Use a default serial number if the serial is invalid 25 | if [[ ${#SERIAL} -lt 12 ]]; then 26 | SERIAL='000000000000'; 27 | fi 28 | 29 | /usr/local/bin/mqtt-simple --host localhost --retain --publish "GLOBAL/SN" --message "$SERIAL" 30 | echo $SERIAL 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # version.sh 19 | # Look up the current CoachProxy software version. Return it as text and 20 | # Publish it to MQTT. 21 | 22 | VERSION=$(cat /coachproxy/VERSION) 23 | 24 | /usr/local/bin/mqtt-simple --host localhost --retain --publish "GLOBAL/VER" --message "$VERSION" 25 | echo $VERSION 26 | 27 | exit 0 28 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/wifi_link_quality.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # link_quality.sh 19 | # Look up the WiFi link quality information and print the results. 20 | # 21 | # example iwconfig output: Link Quality=63/70 Signal level=-47 dBm 22 | 23 | INFO=$(/sbin/iwconfig wlan0 | grep 'Link Quality') 24 | QUALITY=0 25 | if [[ "$INFO" == *"/"* ]]; then 26 | QUALITY=$(echo $INFO | awk '{ split($0, a, "=| "); split(a[3], b, "/"); printf("%d", b[1]/b[2]*100); }') 27 | fi 28 | 29 | echo "$QUALITY" 30 | -------------------------------------------------------------------------------- /roles/coachproxy/files/bin/wifi_mqtt.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # wifi_mqtt.pl 19 | # Look up information about the current WiFi state and publish it to MQTT 20 | # every second. This runs as a forever loop, and should be monitored and 21 | # restarted if it exits. 22 | 23 | use strict; 24 | use warnings; 25 | use Switch; 26 | use Wifi::WpaCtrl; 27 | use Net::MQTT::Simple "localhost"; 28 | use JSON; 29 | no strict 'refs'; 30 | 31 | while (1) { 32 | my $tstamp = time; 33 | 34 | # wlan0 ("Your WiFi Network") 35 | my %wlan0_status = map { split /=|\n+/; } `/sbin/wpa_cli -i wlan0 status`; 36 | my $wlan0_ssid = $wlan0_status{ssid} || ''; 37 | my $wlan0_ip = $wlan0_status{ip_address} || ''; 38 | my $wlan0_gw = `ip route | grep default | cut -f3 -d' '`; 39 | chomp($wlan0_gw); 40 | retain "NETWORK/WPA/SSID" => "$wlan0_ssid"; 41 | retain "NETWORK/WPA/IP" => "$wlan0_ip"; 42 | retain "NETWORK/WPA/STATE" => "$wlan0_status{wpa_state}" if ($wlan0_status{wpa_state}); 43 | retain "NETWORK/WPA/MAC" => "$wlan0_status{address}" if ($wlan0_status{address}); 44 | retain "NETWORK/WPA/GW" => "$wlan0_gw"; 45 | 46 | # WiFi Link Quality 47 | my $wpa_linkinfo = `/coachproxy/bin/wifi_link_quality.sh`; 48 | if ( $wpa_linkinfo =~ /(\d+)/ ) { 49 | retain "NETWORK/WPA/QUALITY" => "$1"; 50 | } 51 | 52 | sleep 1; 53 | } 54 | -------------------------------------------------------------------------------- /roles/coachproxy/files/configurator/node_remover.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # Removes all nodes whose names contain the provided tags. If a selected 19 | # node also contains the /+pred/ tag, all preceeding nodes will also be 20 | # removed. 21 | 22 | use strict; 23 | 24 | if ( scalar(@ARGV) < 2 ) { 25 | print "Usage: $0 filename substr [substr...]\n\n"; 26 | print "Example: $0 flows_coachproxy.json /elecheat2018/ /light_1/\n\n"; 27 | print "If a node's name contains the tag /+pred/ in the json source, all preceeding\n"; 28 | print "nodes will also be removed.\n"; 29 | exit; 30 | } 31 | 32 | my $filename = shift; 33 | 34 | # Pass 1 35 | # 36 | # Get a list of the IDs of each object matching the provided list of names. 37 | # The jq code to accomplish this will look something like this: 38 | # 39 | # .[] | select( (.name != null) and (.name | contains("/heatelec-g8/") or contains("/heatfuel-g8/"))) | .id 40 | # 41 | # ".[]" takes the input array and passes each element to the next code block. 42 | # "select(logic)" only returns objects that match the provided logic. 43 | # ".name != null" is required to prevent errors, since some objects (e.g. dashboard tabs) don't have .name 44 | # ".name |" passes the .name field of an object to the next code block. 45 | # "contains(string)" returns true if string is found inside the input to the code block. 46 | # "| .id" returns the values of the .id parameters in the output of "select()". 47 | # The entire code is wrapped in [] to turn the results into an array. 48 | # 49 | # Two lists are obtained: One for nodes that should be deleted individually (node name does not contain 50 | # the /+pred/ tag), and one for ndoes that should also have all their preceeding nodes removed. 51 | 52 | sub do_filter { 53 | my $filter = shift; 54 | 55 | my $first = 1; 56 | foreach my $name (@_) { 57 | if ($first == 0) { $filter .= " or"; } 58 | $filter .= " contains(\"$name\")"; 59 | $first = 0; 60 | } 61 | 62 | $filter .= " )) | .id"; 63 | 64 | my $ids = `jq '$filter' < $filename`; 65 | 66 | return $ids; 67 | } 68 | 69 | my ($filter_setup, $ids_to_delete, $ids_with_preds, $all_ids); 70 | 71 | $filter_setup = '.[] | select( (.name != null) and (.name | contains("/+pred/") == false) and (.name | '; 72 | $ids_to_delete = do_filter("$filter_setup", @ARGV); 73 | 74 | $filter_setup = '.[] | select( (.name != null) and (.name | contains("/+pred/") == true) and (.name | '; 75 | $ids_with_preds = do_filter("$filter_setup", @ARGV); 76 | 77 | $all_ids = "$ids_to_delete$ids_with_preds"; 78 | 79 | # Pass 2 80 | # 81 | # Get a list of the IDs of each object containing a wire to an object on the 82 | # previously matched "with_predecessors" list. 83 | # 84 | # .[] | select( (.wires != null) and (.wires | contains([["fe6cb3e6.b5088"]]))) | .id 85 | 86 | my $remaining = 8; # Don't loop back more than 8 levels, to prevent infinite loops 87 | while ($ids_with_preds && $remaining > 0) { 88 | 89 | my $filter = ".[] | select( (.wires != null) and (.wires | "; 90 | 91 | my $first = 1; 92 | foreach my $id (split ' ', $ids_with_preds) { 93 | if ($first == 0) { $filter .= " or "; } 94 | $filter .= "contains([[$id]])"; 95 | $first = 0; 96 | } 97 | 98 | $filter .= " )) | .id"; 99 | 100 | my $ids = `jq '$filter' < $filename`; 101 | 102 | $all_ids .= "$ids"; 103 | $ids_with_preds = $ids; # Next loop, check the latest batch of nodes for predecessors 104 | 105 | $remaining--; 106 | if ($ids eq '') { last; } # If nothing was found, abort 107 | } 108 | 109 | my $uniq_ids = `echo "$all_ids" | sort -u`; 110 | 111 | # Pass 3 112 | # 113 | # Filter the source file removing all nodes previously identified. 114 | # 115 | # [ .[] | select( (.name == null) or (.name | contains("/heatelec-g8/") == false and contains("/heatfuel-g8/") == false and contains("/waterpump-g8/") == false )) ] 116 | 117 | my @uniq_ids = split ' ', $uniq_ids; 118 | if (scalar(@uniq_ids) > 0) { 119 | my $filter = "[ .[] | select( .id | "; 120 | 121 | my $first = 1; 122 | foreach my $id (@uniq_ids) { 123 | if ($first == 0) { $filter .= " and "; } 124 | $filter .= "contains(\"$id\") == false"; 125 | $first = 0; 126 | } 127 | 128 | $filter .= " ) ]"; 129 | 130 | system("jq '$filter' < $filename > /tmp/node_remover_temp"); 131 | system("mv /tmp/node_remover_temp $filename"); 132 | } 133 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/cron.d/coachproxy: -------------------------------------------------------------------------------- 1 | # 2 | # CoachProxy specific crontab entries 3 | # 4 | #.--------------- minute (0 - 59) 5 | # .------------- hour (0 - 23) 6 | # | .---------- day of month (1 - 31) 7 | # | | .------- month (1 - 12) OR jan,feb,mar,apr ... 8 | # | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat 9 | # | | | | 10 | # * * * * user command 11 | */5 * * * * root /coachproxy/bin/wifi_watchdog.sh &> /dev/null 12 | * * * * * pi /coachproxy/bin/coachproxy_watchdog.sh &> /dev/null 13 | * * * * * root /coachproxy/ha-bridge/re-ip.sh &> /dev/null 14 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/dpkg/dpkg.cfg.d/01_nodoc: -------------------------------------------------------------------------------- 1 | # /etc/dpkg/dpkg.conf.d/01_nodoc 2 | 3 | # Delete locales 4 | path-exclude=/usr/share/locale/* 5 | 6 | # Delete man pages 7 | path-exclude=/usr/share/man/* 8 | 9 | # Delete docs 10 | path-exclude=/usr/share/doc/* 11 | path-include=/usr/share/doc/*/copyright 12 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 localhost 2 | ::1 localhost ip6-localhost ip6-loopback 3 | ff02::1 ip6-allnodes 4 | ff02::2 ip6-allrouters 5 | 6 | 192.168.41.1 coachproxyos 7 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/mosquitto/mosquitto.conf: -------------------------------------------------------------------------------- 1 | # A full description of the configuration file is at 2 | # /usr/share/doc/mosquitto/examples/mosquitto.conf.example 3 | 4 | pid_file /var/run/mosquitto.pid 5 | 6 | persistence false 7 | 8 | log_dest none 9 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/nginx/sites-available/10-CoachProxy.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | root /var/www/html; 4 | server_name _; 5 | 6 | access_log off; 7 | error_log stderr crit; 8 | 9 | # If / is requested, redirect the user to /ui/. A temporary redirect is 10 | # used so that re-authentication requests can be made when passwords or 11 | # auth tokens have changed. If a 301 permanent redirect is used, the 12 | # browser will never check here again, and websocket connections can 13 | # get stuck in a permanently unauthorized state. 14 | location = / { 15 | return 302 /ui/; 16 | } 17 | 18 | # Enable habridge requests 19 | location /api { 20 | proxy_pass http://127.0.0.1:8080/api; 21 | } 22 | 23 | # Forward all /ui requests to the node-red web server on port 1880. 24 | # Ensure websocket connections are handled correctly. 25 | location /ui { 26 | proxy_pass http://127.0.0.1:1880/ui; 27 | proxy_http_version 1.1; 28 | proxy_set_header Upgrade $http_upgrade; 29 | proxy_set_header Connection "upgrade"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/nginx/sites-available/20-SSLRedirect.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8443 default_server; 3 | server_name _; 4 | 5 | access_log off; 6 | error_log stderr crit; 7 | 8 | return 301 https://$host$request_uri; 9 | } 10 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/ngrok.conf-TEMPLATE: -------------------------------------------------------------------------------- 1 | authtoken: AUTHTOKEN 2 | region: us 3 | console_ui: false 4 | update: false 5 | tunnels: 6 | coachproxy: 7 | addr: 80 8 | proto: http 9 | auth: USERNAME:PASSWORD 10 | subdomain: SUBDOMAIN 11 | bind_tls: BIND_TLS 12 | inspect: false 13 | 14 | # Allow unauthenticated http requests through the tunnel, but 15 | # forward them to port 8443 which will redirect them to https. 16 | # This ensures that no basic-auth usernames/passwords will be 17 | # sent over non-SSL. 18 | ssl_redirect: 19 | addr: 8443 20 | proto: http 21 | subdomain: SUBDOMAIN 22 | bind_tls: false 23 | inspect: false 24 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/profile.d/coachproxy.sh: -------------------------------------------------------------------------------- 1 | alias c=clear 2 | alias h=history 3 | 4 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/rc.local.coachproxy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # rc.local.coachproxy 19 | # Commands to be run at the end of system startup. 20 | 21 | LOG=/coachproxy/bin/cplog.sh 22 | 23 | # Load last saved date/time into fakehwclock 24 | /sbin/fake-hwclock load 25 | 26 | $LOG "rc.local.coachproxy -- BOOTED --" 27 | 28 | # Ensure crontab file is owned by root. Sometimes it gets accidentally set back to pi. 29 | chown root:root /coachproxy/etc/cron.d/coachproxy 30 | 31 | /coachproxy/bin/serialnumber.sh &> /dev/null 32 | /coachproxy/bin/version.sh &> /dev/null 33 | /coachproxy/bin/coachproxy_watchdog.sh &> /dev/null 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/resolv.conf: -------------------------------------------------------------------------------- 1 | # Generated by resolvconf 2 | nameserver 127.0.0.1 3 | nameserver 8.8.8.8 4 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/resolvconf.conf: -------------------------------------------------------------------------------- 1 | # Configuration for resolvconf(8) 2 | # See resolvconf.conf(5) for details 3 | 4 | resolv_conf=/etc/resolv.conf 5 | # If you run a local name server, you should uncomment the below line and 6 | # configure your subscribers configuration files below. 7 | name_servers=127.0.0.1 8 | 9 | # Mirror the Debian package defaults for the below resolvers 10 | # so that resolvconf integrates seemlessly. 11 | dnsmasq_resolv=/var/run/dnsmasq/resolv.conf 12 | pdnsd_conf=/etc/pdnsd.conf 13 | unbound_conf=/var/cache/unbound/resolvconf_resolvers.conf 14 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/systemd/network/04-eth0.network: -------------------------------------------------------------------------------- 1 | [Match] 2 | Name=eth0 3 | 4 | [Network] 5 | DHCP=ipv4 6 | 7 | [DHCP] 8 | RouteMetric=10 9 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/systemd/network/12-wlan0.network: -------------------------------------------------------------------------------- 1 | [Match] 2 | Name=wlan0 3 | 4 | [Network] 5 | DHCP=ipv4 6 | 7 | [DHCP] 8 | RouteMetric=20 9 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/systemd/system/habridge.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=HA Bridge 3 | Wants=network.target 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/bin/java -jar -Dconfig.file=/coachproxy/ha-bridge/habridge.config /coachproxy/ha-bridge/ha-bridge.jar 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /roles/coachproxy/files/etc/vim/vimrc.local: -------------------------------------------------------------------------------- 1 | " Map jj to ESC since it's easier to reach while in insert mode 2 | imap jj 3 | 4 | " Show $ at end of word change (cw) 5 | set cpoptions+=$ 6 | 7 | " Don't hide the double-quotes in JSON symbols. 8 | let g:vim_json_syntax_conceal=0 9 | 10 | " Use spaces instead of tabs 11 | set tabstop =2 12 | set softtabstop =2 13 | set shiftwidth =2 14 | set expandtab 15 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/device.db: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/device.db-blank: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/disable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo systemctl stop habridge 4 | sudo mount -o remount,rw / 5 | sudo systemctl disable habridge 6 | sudo mount -o remount,ro / 7 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/enable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo mount -o remount,rw / 4 | sudo systemctl enable habridge 5 | sudo mount -o remount,ro / 6 | sudo systemctl restart habridge 7 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/ha-bridge-5.2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/coachproxy/files/ha-bridge/ha-bridge-5.2.1.jar -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/ha-bridge.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/coachproxy/files/ha-bridge/ha-bridge.jar -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/habridge.config.template: -------------------------------------------------------------------------------- 1 | { 2 | "configfile" : "/coachproxy/ha-bridge/habridge.config", 3 | "upnpdevicedb" : "/coachproxy/ha-bridge/device.db", 4 | "upnpgroupdb" : "", 5 | "upnpconfigaddress" : "MYIPADDR", 6 | "upnpresponseport" : 50000, 7 | "upnpsenddelay" : 500, 8 | "upnpstrict" : true, 9 | "serverport" : 8080, 10 | "numberoflogmessages" : 512, 11 | "buttonsleep" : 100, 12 | "farenheit" : true, 13 | "settingsChanged" : false, 14 | "lifxconfigured" : false, 15 | "traceupnp" : false, 16 | "halconfigured" : false, 17 | "somfyconfigured" : false, 18 | "fhemconfigured" : false, 19 | "fibaroconfigured" : false, 20 | "useupnpiface" : false, 21 | "hassconfigured" : false, 22 | "harmonyconfigured" : false, 23 | "hueconfigured" : false, 24 | "openhabconfigured" : false, 25 | "broadlinkconfigured" : false, 26 | "homewizardconfigured" : false, 27 | "veraconfigured" : false, 28 | "nestconfigured" : false, 29 | "domoticzconfigured" : false, 30 | "mqttconfigured" : false, 31 | "myechourl" : "alexa.amazon.com/spa/index.html#cards", 32 | "hubversion" : "9999999999", 33 | "userooms" : false, 34 | "webaddress" : "0.0.0.0" 35 | } 36 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/re-init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo systemctl restart habridge 4 | -------------------------------------------------------------------------------- /roles/coachproxy/files/ha-bridge/re-ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2018 Wandertech LLC 4 | # 5 | # re-ip.sh 6 | # Detects if the system's routable IP address doesn't match the IP 7 | # configured in the habridge.config file (for example, if the CoachProxy 8 | # has been moved to a different network),updates the config file, and 9 | # restarts ha-bridge. 10 | 11 | LOG=/coachproxy/bin/cplog.sh 12 | SCRIPT=$(basename $0 .sh) 13 | 14 | # Get the network interface (e.g. wlan0, eth0) of the locally routable network 15 | routeIF=$(netstat -rn | awk '/^0\.0\.0\.0/ {print $NF}' | head -n 1) 16 | 17 | # If a network interface was found, get the local IP address of CoachProxy 18 | # and check if the ha-bridge service is enabled. 19 | if [ ! -z $routeIF ]; then 20 | currentIP=$(ip a | grep "inet .*$routeIF" | grep -v lo$ | awk '{print $2}' | sed -e 's/\/.*$//') 21 | isenabled=$(systemctl status habridge | grep -c '^ *Loaded.*service; enabled') 22 | fi 23 | 24 | # If CoachProxy's current IP address is not in the running habridge.config, 25 | # rebuild the config from the template using the current IP address. 26 | if [ ! -z $currentIP ]; then 27 | grepPhrase=$(echo $currentIP | sed -e 's/\./\\./g') 28 | grepPhrase="${grepPhrase}\"" 29 | 30 | if [ $(grep -c $grepPhrase /coachproxy/ha-bridge/habridge.config) -eq 0 ]; then 31 | $LOG "$SCRIPT updating IP address in habridge.config with $currentIP" 32 | cat /coachproxy/ha-bridge/habridge.config.template | sed -e "s/MYIPADDR/$currentIP/g" > /coachproxy/ha-bridge/habridge.config 33 | 34 | if [ $isenabled -eq 1 ]; then 35 | sleep 1 36 | sudo systemctl restart habridge 37 | $LOG "$SCRIPT restarted habridge.service" 38 | fi 39 | fi 40 | fi 41 | -------------------------------------------------------------------------------- /roles/coachproxy/files/log/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/coachproxy/files/log/.gitignore -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/coachproxy.sqlite-base: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/coachproxy/files/node-red/coachproxy.sqlite-base -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/ats_source_decode.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Output 1: name of current ATS power source 18 | 19 | const genset_status = global.get('status').power.generator_state || 'stopped'; 20 | 21 | // If the rmsc value is not numeric (e.g. 'n/a'), then 22 | // the ATS is not receiving any external power. 23 | if (isNaN(parseFloat(msg.payload['rms current']))) { 24 | msg.payload = 'None'; 25 | } else if (genset_status == 'running') { 26 | msg.payload = 'Generator'; 27 | } else { 28 | msg.payload = 'Shore'; 29 | } 30 | 31 | msg.type = 'acsource'; 32 | return msg; 33 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/ceiling_fan_decode.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Input: Payload is the JSON output of DC_DIMMER_STATUS_3/#, and 18 | // msg.instance is either "low" or "high" depending on which dimmer 19 | // is reporting. 20 | // 21 | // Output 1: Text version of the current fan speed. 22 | // Output 2: Numeric version of the current fan speed (0, 1, 2) 23 | 24 | var instance = msg.instance; 25 | var brightness = msg.payload['operating status (brightness)']; 26 | 27 | // Determine which command was sent. 28 | var level = 0; 29 | if (instance == 'low' && brightness == 100) { 30 | level = 1; 31 | } 32 | if (instance == 'high' && brightness == 100) { 33 | level = 2; 34 | } 35 | 36 | // Values to display on the text label 37 | var level_names = ['Off', 'Low', 'High']; 38 | var msg1 = { 39 | 'payload': level_names[level] 40 | }; 41 | 42 | // Set the switch position 43 | var msg2 = { 44 | 'payload': level, 45 | }; 46 | 47 | return [ msg1, msg2 ]; 48 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/command_dc_dimmer.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Input: the on/off or brightness payload from a light switch or slider. 18 | // Output: command line parameters for the dc_dimmer.pl command to actuate the light. 19 | 20 | var instance = msg.topic.split('_')[0]; 21 | var msgtype = msg.topic.split('_')[1]; 22 | var commands = { 'dim': 0, 'on': 2, 'off': 3 }; 23 | 24 | var command = commands['on']; 25 | var brightness = 100; 26 | 27 | if (msgtype == 'state') { 28 | command = commands[msg.payload]; 29 | } else if (msgtype == 'brightness') { 30 | brightness = msg.payload; 31 | if (brightness > 0) { 32 | command = commands['dim']; 33 | } 34 | } 35 | 36 | var newMsg={ 37 | 'payload': instance + ' ' + command + ' ' + brightness 38 | }; 39 | 40 | return newMsg; 41 | 42 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_chassis_mobility.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Newer coaches (2018+) report two different sets of 18 | // chassis statuses, identified by F1 or F2 in Byte 0. 19 | // The F1 message includes the ignition status, and the 20 | // F2 message includes the parking brake status. 21 | // 22 | // Older coaches always have 00 in Byte 0 and only report 23 | // ignition status. 24 | 25 | const instance = msg.payload.data.substring(0, 2); 26 | msg.topic = null; // Required to block pass-through topic 27 | let newmsg = {}; 28 | 29 | if (instance == 'F1' || instance == '00') { 30 | newmsg.topic = 'CP/IGNITION'; 31 | newmsg.payload = msg.payload['ignition switch status'] == '00' ? 'off' : 'on'; 32 | global.set('status.chassis.ignition', newmsg.payload); 33 | } else if (instance == 'F2') { 34 | newmsg.topic = 'CP/PARKBRAKE'; 35 | newmsg.payload = msg.payload['park brake status'] == '00' ? 'off' : 'on'; 36 | global.set('status.chassis.parkbrake', newmsg.payload); 37 | } 38 | 39 | return newmsg; 40 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_dc_dimmer_light.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Input: the JSON output of DC_DIMMER_STATUS_3/# for a light. 18 | // Output 1: "on" or "off" payload to set toggle switch indicator. 19 | // Output 2: brightness level (0-100) to set optional slider value. 20 | // 21 | // Also saves the on/off status and brightness into global 'status' 22 | // context for use by other nodes. 23 | // 24 | // Built-in RBE functionality only outputs messages if the value has 25 | // changed from the previously recorded value. 26 | 27 | var instance = msg.payload.instance; 28 | var brightness = msg.payload['operating status (brightness)']; 29 | 30 | var previous = global.get('status').lights[instance]; 31 | var command = (brightness > 0) ? 'on' : 'off'; 32 | 33 | var msg1 = null; 34 | var msg2 = null; 35 | 36 | // Only send a message if the value has changed. 37 | if (previous.state != command) { 38 | if (previous) { 39 | global.set('status.lights[' + instance + '].state', command); 40 | } 41 | msg1 = { 'payload': command }; 42 | } 43 | 44 | if (previous.brightness != brightness && brightness <= 100) { 45 | global.set('status.lights[' + instance + '].brightness', brightness); 46 | msg2 = { 'payload': brightness }; 47 | } 48 | 49 | return [ msg1, msg2 ]; 50 | 51 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_dc_source.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | var topic_parts=msg.topic.split('/'); 18 | var payload_parts=msg.payload.split(','); 19 | 20 | var msg1={ 21 | 'type': 'battery', 22 | 'topic': topic_parts[1], 23 | 'payload': payload_parts[0], 24 | 'external': 1, 25 | 'pkttime': payload_parts[1], 26 | }; 27 | 28 | return msg1; 29 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_fan.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Input: the JSON output of DC_DIMMER_STATUS_3/# for a roof fan. 18 | // Output 1: "on" or "off" payload to set toggle switch indicator. 19 | // Output 2: text for display next to the vent/fan name/status. 20 | // 21 | // Also saves the on/off status and brightness into global 'status' 22 | // context for use by other nodes. 23 | 24 | var commands = { 'on': 2, 'off': 3 }; 25 | 26 | var instance = msg.instance; 27 | var brightness = msg.payload['operating status (brightness)']; 28 | 29 | var status = brightness > 0 ? 'on' : 'off'; 30 | var command = commands[status]; 31 | 32 | global.set('status.fans[' + instance + ']', status); 33 | var msg1 = { 'payload': command }; 34 | 35 | return msg1; 36 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_floor_ambient_status.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Decodes THERMOSTAT_AMBIENT_STATUS messages for floor heat zones. 18 | // Saves the current floor temperature into global context and returns 19 | // it as the payload to display in the UI 20 | 21 | var instance = msg.payload.instance; 22 | var zones = { 0: 'Front', 1: 'Rear' }; 23 | 24 | if (instance in zones) { 25 | var temp = msg.payload['ambient temp F']; 26 | global.set('status.floors[' + instance + '].measured', temp); 27 | return { payload: temp, zone: zones[instance] }; 28 | } 29 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_floor_status.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Decodes THERMOSTAT_STATUS_1 messages for floor heat zones. 18 | // Saves the current floor temperature into global context and returns 19 | // it as the payload to display in the UI 20 | 21 | var methodyear = global.get('settings2').floor_heat_method; 22 | var method = methodyear.substring(0, 6); 23 | var year = methodyear.substring(6, 10); 24 | var instance = msg.payload.instance; 25 | var setpoint = parseInt(msg.payload['setpoint temp heat F']); 26 | 27 | global.set('status.floors[' + instance + '].setpoint', setpoint); 28 | 29 | var setpoint_cp = global.get('status').floors[instance].setpoint_cp; 30 | var last_setpoint_cp = global.get('status').floors[instance].last_cp_setpoint; 31 | 32 | // If this is a pre-2017 coach, or if no custom CoachProxy setpoint has been 33 | // set, set it to the current Spyder setpoint. This is only used in 2017+ 34 | // coaches where CoachProxy manages the floor heat manually. 35 | if (method == 'direct' || setpoint_cp === 0) { 36 | setpoint_cp = setpoint; 37 | global.set('status.floors[' + instance + '].setpoint_cp', setpoint_cp); 38 | } 39 | 40 | var power = msg.payload['operating mode definition'] == 'off' ? 0 : 1; 41 | global.set('status.floors[' + instance + '].power', power); 42 | 43 | var power_msg = { 44 | topic: instance, 45 | payload: power 46 | } 47 | 48 | var setpoint_msg = { 49 | topic: instance, 50 | payload: setpoint_cp 51 | }; 52 | 53 | return [power_msg, setpoint_msg]; 54 | 55 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_tank.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Calculate the tank percent full and republish it to the "CP/" topic. 18 | 19 | let newmsg = {}; 20 | msg.topic = null; // Required to block pass-through topic 21 | 22 | let pct_full = Math.round(msg.payload['relative level'] / msg.payload.resolution * 100); 23 | 24 | const labels = ['Fresh', 'Black', 'Grey', 'LPG']; 25 | 26 | newmsg.topic = 'CP/TANK_STATUS/' + labels[msg.payload.instance]; 27 | newmsg.payload = pct_full; 28 | 29 | return newmsg; 30 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/decode_vent.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Input: the JSON output of DC_DIMMER_COMMAND_2/# for a roof vent. 18 | // Output 1: dimmer command payload to set toggle switch indicator. 19 | // 20 | // Also saves the on/off status and brightness into global 'status' 21 | // context for use by other nodes. 22 | 23 | var commands = { 'open': 69, 'closed': 133 }; 24 | 25 | var instance = msg.instance; 26 | var brightness = msg.payload['desired level']; 27 | 28 | var status = brightness > 99 ? 'open' : 'closed'; 29 | var command = commands[status]; 30 | 31 | global.set('status.vents[' + instance + ']', status); 32 | var msg1 = { 'payload': command }; 33 | 34 | return msg1; 35 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/emails_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | global.set('emails', []); 18 | global.set('emails[0]', msg.payload[0].email0 || ""); 19 | global.set('emails[1]', msg.payload[0].email1 || ""); 20 | 21 | return [ 22 | { external: 1, topic:'email0', payload: global.get('emails')[0] }, 23 | { external: 1, topic:'email1', payload: global.get('emails')[1] } 24 | ]; 25 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/emails_store.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | var external=0; 18 | if(msg.hasOwnProperty('external')) 19 | external=msg.external; 20 | 21 | global.set('emailsb', global.get('emails') || []); 22 | 23 | if(external===0) { 24 | var emails=global.get('emails'); 25 | if(msg.topic=='commit') { 26 | if(msg.payload=='save') { 27 | msg.topic = ''; 28 | return [ { 29 | topic: 'update notifications set `email0`=?, `email1`=?', 30 | payload:global.get('emailsb') 31 | }, {payload:"Saved"}, null]; 32 | } else if (msg.payload=='undo') { 33 | return [ null, {payload:"Reverted to Saved Values"}, {}]; 34 | } 35 | } else { 36 | if(msg.topic=='email0') 37 | global.set('emailsb[0]', msg.payload); 38 | if(msg.topic=='email1') 39 | global.set('emailsb[1]', msg.payload); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/floor_heat_manager.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Manually manages the floor heat level in 2017+ coaches that only have 1-5 18 | // levels provided by Tiffin/Spyder. If a user selects a temperature between 19 | // the standard levels, CoachProxy will raise and/or lower the level as needed 20 | // to maintain the desired intermediate temperature. 21 | 22 | var method = global.get('settings2').floor_heat_method; 23 | var instance = msg.payload.instance; 24 | var floor_status = global.get('status').floors[instance]; 25 | var ambient_temp = floor_status.measured; 26 | var target_cp_setpoint = parseInt(floor_status.setpoint_cp); 27 | var target_cp_level = (target_cp_setpoint - 68) / 9 + 1; 28 | var current_spyder_setpoint = parseInt(msg.payload['setpoint temp heat F']); 29 | var current_spyder_level = (current_spyder_setpoint - 68) / 9 + 1; 30 | var target_is_standard_level = Number.isInteger(target_cp_level); 31 | 32 | // If the user adjusted the floor setting manually via a Spyder keypad, 33 | // use the new value. 34 | if (floor_status.last_cp_setpoint != current_spyder_setpoint) { 35 | global.set('status.floors[' + instance + '].setpoint', current_spyder_setpoint); 36 | global.set('status.floors[' + instance + '].setpoint_cp', current_spyder_setpoint); 37 | global.set('status.floors[' + instance + '].last_cp_setpoint', current_spyder_setpoint); 38 | // return { payload: 'Manual change detected. Overriding CP setting.' } 39 | } 40 | 41 | // Only manage the floor heat if the floor power is turned on and 42 | // a custom (non-Spyder) temperature setpoint has been selected. 43 | else if (floor_status.power === 1 && !target_is_standard_level) { 44 | var target_spyder_level; 45 | 46 | // Determine the desired Spyder floor heat level based on the current 47 | // ambient temperature and the desired temperature. Heat the floor to 48 | // 1 degree C (1.7 degrees F) above the desired setpoint before turning 49 | // power off. This replicates the Spyder methodology. 50 | if (ambient_temp < target_cp_setpoint) { 51 | target_spyder_level = Math.ceil(target_cp_level); 52 | } else if (ambient_temp > target_cp_setpoint + 1.7) { 53 | target_spyder_level = Math.floor(target_cp_level); 54 | } else { 55 | target_spyder_level = current_spyder_level; 56 | } 57 | 58 | if (target_spyder_level != current_spyder_level) { 59 | var count = Math.abs(current_spyder_level - target_spyder_level); 60 | var direction = (target_spyder_level < current_spyder_level) ? 'down' : 'up'; 61 | 62 | target_spyder_setpoint = (target_spyder_level - 1) * 9 + 68; 63 | global.set('status.floors[' + instance + '].setpoint', target_spyder_setpoint); 64 | global.set('status.floors[' + instance + '].last_cp_setpoint', target_spyder_setpoint); 65 | msg = { payload: method + ' ' + instance + ' ' + direction + ' ' + count }; 66 | 67 | return msg; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/form_field_save.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Save text input values to a global context structure as the user 18 | // types into the field. This makes it easy to retrieve later when 19 | // the user selects the "Save" or other action button. 20 | 21 | global.set('form.' + msg.topic, msg.payload); 22 | 23 | return null; 24 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/function_floor_power.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Create command line arguments to turn floor heat on or off based 18 | // on the user's switch selection. 19 | 20 | var method = global.get('settings2').floor_heat_method; 21 | var instance = msg.topic; 22 | var command = msg.payload === 1 ? 'on' : 'off'; 23 | 24 | return { payload: method + ' ' + instance + ' ' + command }; 25 | 26 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/function_floor_setpoint.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Create command line arguments to set the floor heat temperature or 18 | // level. 19 | 20 | var methodyear = global.get('settings2').floor_heat_method; 21 | var method = methodyear.substring(0, 6); 22 | var year = methodyear.substring(6, 10); 23 | var instance = msg.topic; 24 | 25 | if (method == 'direct') { 26 | // Pre-2017 coaches can just set the temperature directly to any level. 27 | var newMsg = { payload: methodyear + ' ' + instance + ' ' + msg.payload }; 28 | return newMsg; 29 | 30 | } else { 31 | 32 | // 2017+ coaches can only set the level to one of five values, via 33 | // up/down commands. 34 | // 35 | // Set the global context variables for the new setpoint and wait for 36 | // the floor heat manager function to take care of the rest. If the user 37 | // selected a standard Spyder temperature, set the floors to that level 38 | // immediately. 39 | 40 | var floor_status = global.get('status').floors[instance]; 41 | var target_cp_setpoint = parseInt(msg.payload); 42 | var target_cp_level = (target_cp_setpoint - 68) / 9 + 1; 43 | var current_spyder_level = (floor_status.setpoint - 68) / 9 + 1; 44 | var target_is_standard_level = Number.isInteger(target_cp_level); 45 | 46 | global.set('status.floors[' + instance + '].setpoint_cp', target_cp_setpoint); 47 | 48 | if (target_is_standard_level && target_cp_level != current_spyder_level) { 49 | global.set('status.floors[' + instance + '].last_cp_setpoint', target_cp_setpoint); 50 | var count = Math.abs(current_spyder_level - target_cp_level); 51 | var direction = target_cp_level > current_spyder_level ? 'up' : 'down'; 52 | var newMsg = { payload: methodyear + ' ' + instance + ' ' + direction + ' ' + count }; 53 | return newMsg; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/furnace_status_decode.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Sometimes zone 0 is the front, sometimes it's zone 2 (if heated floors are 0 and 1) 18 | const firstzone = parseInt(global.get('settings2').tstat_first_zone) || 0; 19 | 20 | const zone = msg.payload.instance; 21 | const heatset = parseInt(msg.payload['setpoint temp heat F']); 22 | const coolset = parseInt(msg.payload['setpoint temp cool F']); 23 | const mode = msg.payload['operating mode definition']; 24 | 25 | const fanspeed = parseInt(msg.payload['fan speed']); 26 | var fanmode = msg.payload['fan mode definition']; 27 | 28 | const roofzone = ([0, 2][zone - firstzone - 3]) + firstzone; 29 | const roofstatus = global.get('status').tstat[roofzone] || {}; 30 | const roofmode = roofstatus.mode; 31 | 32 | var status = { 33 | mode: mode, 34 | heatset: heatset, 35 | coolset: coolset, 36 | fanmode: fanmode, 37 | fanspeed: fanspeed 38 | }; 39 | global.set('status.tstat[' + zone + ']', status); 40 | 41 | var on = { foreground: 'white', background: '#3D89BE' }; 42 | var off = { foreground: 'black', background: '#dddddd' }; 43 | 44 | var heatstatus = mode == 'heat' ? on : off; 45 | // Off button highlighted if both furnace and heat pump are 'off' 46 | var offstatus = (mode == 'off' && roofmode == 'off') ? on : off; 47 | 48 | // Create an array of objects to return from the node's various outputs. 49 | // Most of the parameters are for coloring and updating the button, but 50 | // a payload is also always returned so the RBE node can determine when 51 | // the status has changed. 52 | return [ 53 | Object.assign({ payload: heatstatus.foreground }, heatstatus), 54 | Object.assign({ payload: offstatus.foreground }, offstatus), 55 | ] 56 | 57 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/genset_clear_buffer.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | global.set('genStatTime', global.get('genStatTime') || 0); 18 | if (msg.hasOwnProperty('countdown')) { 19 | if (global.get('genStatTime')===0 || (new Date().getTime()/1000)-global.get('genStatTime')>5) 20 | return msg; 21 | else 22 | return null; 23 | } else { 24 | global.set('genStatTime', new Date().getTime()/1000); 25 | return msg; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/genset_countdown.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | if(global.get('genstartenable')===1) { 18 | if(global.get('genstartenabletimer')<1) { 19 | global.set('genstartenabletimer', 5); 20 | global.set('genstartenable', 0); 21 | return [{ 22 | 'topic': 'genstartenable', 23 | 'payload': 0, 24 | 'timer': 1, 25 | },{countdown: 1, payload:"Controls disabled"}]; 26 | } 27 | countdown=global.get('genstartenabletimer'); 28 | global.set('genstartenabletimer', global.get('genstartenabletimer') - 1); 29 | return [null,{countdown: 1, payload:"Controls enabled: "+countdown+" seconds"}]; 30 | } else { 31 | global.set('genstartenabletimer', 5); 32 | return [{ 33 | 'topic': 'genstartenable', 34 | 'payload': 0, 35 | 'timer': 1, 36 | },{countdown: 1, payload:"Controls disabled"}]; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/genset_safety.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | if(!msg.hasOwnProperty('timer')) { 18 | if(msg.topic=='genstartenable') { 19 | if(msg.payload=="1") { 20 | global.set('genstartenabletimer', 5); 21 | global.set('genstartenable', 1); 22 | } else { 23 | global.set('genstartenabletimer', 5); 24 | global.set('genstartenable', 0); 25 | } 26 | } else { 27 | if(msg.topic=='generator' && global.get('genstartenable')===1 ) { 28 | const version = global.get('settings2').generator_version; 29 | msg.payload = version + ' ' + msg.payload; 30 | return msg; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/initialize_global_context.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Create global context objects that are used elsewhere 18 | // in flows/functions. This prevents many Javascript warnings 19 | // along the lines of: "TypeError: Cannot read property 'foo' of null" 20 | 21 | // To track form fields filled in by users 22 | global.set('form', {}); 23 | global.set('form.preset', {}); 24 | 25 | // To track the current status of various systems 26 | global.set('status', {}); 27 | global.set('status.power', {}); 28 | global.set('status.fans', {}); 29 | global.set('status.vents', {}); 30 | global.set('status.lights', {}); 31 | global.set('status.chassis', {}); 32 | global.set('status.network', { WPA: {}, AP: {} } ); 33 | 34 | global.set('status.floors', { 35 | 0: { power: 0, setpoint: 68, setpoint_cp: 0, measured: 0 }, 36 | 1: { power: 0, setpoint: 68, setpoint_cp: 0, measured: 0 } 37 | }); 38 | 39 | // Set every light to "off" by default 40 | for (let i = 0; i < msg.lights.length; i++) { 41 | var light = { state: 'off', brightness: 0 }; 42 | global.set('status.lights[' + msg.lights[i] + ']', light); 43 | } 44 | 45 | for (let i = 0; i <= 6; i++) { 46 | global.set('status.tstat[' + i + ']', {}); 47 | } 48 | 49 | // Settings 50 | 51 | global.set('settings2', {}); 52 | global.set('settings2.notify_email1', ''); 53 | global.set('settings2.notify_email2', ''); 54 | global.set('settings2.pushover_key', ''); 55 | global.set('settings2.remote_username', ''); 56 | global.set('settings2.remote_subdomain', ''); 57 | global.set('settings2.ngrok_auth', ''); 58 | 59 | // Legacy objects 60 | global.set(['ap', 'ign'], [{}, {}]); 61 | 62 | return {}; 63 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/levels_display.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | outmsg=[]; 18 | for (var i=0;key=global.get('levels').fields[i];i++) { 19 | var tmpmsg={ 20 | 'topic':key, 21 | 'payload':global.get('levelsb')[key], 22 | 'external':1 23 | }; 24 | outmsg.push(tmpmsg); 25 | } 26 | return outmsg; 27 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/levels_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | if (global.get('levels') === undefined) 18 | global.set('levels', {}); 19 | global.set('levels.fields', ['Fresh_low', 'Fresh_high', 'Grey_high', 'Black_high', 'LPG_low']); 20 | global.set('levels.fullTanks', ['Grey_full', 'Black_full']); 21 | global.set('levels.subItems', ['clear', 'sent', 'time']); 22 | global.set('levelsb', msg.payload[0]); 23 | 24 | outmsg = []; 25 | for (var i = 0; key = global.get('levels').fields[i]; i++) { 26 | global.set('levels.' + key, msg.payload[0][key]); 27 | for (var j = 0; subItem = global.get('levels').subItems[j]; j++) { 28 | if (global.get('levels')[key + '_' + subItem] === undefined) { 29 | global.set('levels.' + key + '_' + subItem, 0); 30 | } 31 | } 32 | var tmpmsg = { 33 | 'topic': key, 34 | 'payload': msg.payload[0][key], 35 | 'external': 1 36 | }; 37 | outmsg.push(tmpmsg); 38 | } 39 | 40 | for (var i = 0; key = global.get('levels').fullTanks[i]; i++) { 41 | for (var j = 0; subItem = global.get('levels').subItems[j]; j++) { 42 | if (global.get('levels')[key + '_' + subItem] === undefined) { 43 | global.set('levels.' + key + '_' + subItem, 0); 44 | } 45 | } 46 | global.set('levels.' + key, 99); 47 | } 48 | 49 | return outmsg; 50 | 51 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/levels_store.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | global.set('levelsb', global.get('levelsb') || {}); 18 | 19 | var external=0; 20 | if(msg.hasOwnProperty('external')) 21 | external=msg.external; 22 | /* 23 | return payload: 24 | 0 - SQL 25 | 1 - Update for filtered input 26 | 2 - Message 27 | 3 - Revert 28 | */ 29 | 30 | tank_max=99; 31 | tank_min=0; 32 | if(external===0) { 33 | if(msg.topic=='commit') { 34 | if(msg.payload=='save') { 35 | sqltopic='update notifications set `'+global.get('levels').fields.join('`=?, `')+'`=?'; 36 | sqlvalues=[]; 37 | for(i=0;key=global.get('levels').fields[i];i++) 38 | sqlvalues.push(global.get('levelsb')[key]); 39 | msg.topic = ''; 40 | return [{ 41 | topic: sqltopic, 42 | payload: sqlvalues 43 | },null,{payload:"Saved"},null]; 44 | } else if (msg.payload=='undo') { 45 | return [ null, null, {payload:"Reverted to Saved Values"}, {}]; 46 | } 47 | } else { 48 | payload=msg.payload.replace(/[^0-9\.\-]/g, ''); 49 | if(payload !== '' && payload>tank_max) 50 | payload=tank_max; 51 | if(payload !== '' && payload. 16 | 17 | return { 18 | 'topic':'battery_low', 19 | 'payload':global.get('miscb').battery_low, 20 | 'external':1 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/misc_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | misc = ['ignition', 'generator', 'acpower', 'battery_low']; 18 | global.set('misc', msg.payload[0]); 19 | 20 | flags = ['1_battery_low', '2_battery_low']; 21 | 22 | for (i = 0; flag = flags[i]; i++) { 23 | global.set('misc.' + flag + '_sent', global.get('misc')[flag + '_sent'] || 0); 24 | global.set('misc.' + flag + '_time', global.get('misc')[flag + '_time'] || 0); 25 | } 26 | 27 | global.set('ign', global.get('ign') || {}); 28 | global.set('ign.lastState', global.get('ign').lastState || 'off'); 29 | 30 | global.set('gen', global.get('gen') || {}); 31 | global.set('gen.lastState', global.get('gen').lastState || 'stopped'); 32 | global.set('gen.startEnable', global.get('gen').startEnable || false); 33 | global.set('gen.startEnableTimer', global.get('gen').startEnableTimer || 5); 34 | 35 | global.set('acsource', global.get('acsource') || {}); 36 | global.set('acsource.lastState', global.get('acsource').lastState || 'Shore'); 37 | 38 | outmsg = []; 39 | for (var i = 0; key = misc[i]; i++) { 40 | var tmpmsg = { 41 | 'topic': key, 42 | 'payload': msg.payload[0][key], 43 | 'external': 1 44 | }; 45 | outmsg.push(tmpmsg); 46 | } 47 | 48 | return outmsg; 49 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/misc_store.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | misc=['ignition','generator','acpower','battery_low']; 18 | global.set('miscb', global.get('misc') || {}); 19 | 20 | var external=0; 21 | if(msg.hasOwnProperty('external')) 22 | external=msg.external; 23 | 24 | /* 25 | return payload: 26 | 0 - SQL 27 | 1 - Update for filtered input 28 | 2 - Message 29 | 3 - Revert 30 | */ 31 | 32 | batt_min=1; 33 | batt_max=14; 34 | if(external===0) { 35 | if(msg.topic=='commit') { 36 | if(msg.payload=='save') { 37 | sqltopic='update notifications set `'+misc.join('`=?, `')+'`=?'; 38 | sqlvalues=[]; 39 | for(i=0;key=misc[i];i++) 40 | sqlvalues.push(global.get('miscb')[key]); 41 | msg.topic = ''; 42 | return [ { topic: sqltopic, payload: sqlvalues }, null, {payload:"Saved"}, null ]; 43 | } else if (msg.payload=='undo') { 44 | return [ null, null, {payload:"Reverted to Saved Values"}, {}]; 45 | } 46 | } else { 47 | payload=msg.payload; 48 | if(msg.topic=='battery_low') { 49 | payload=payload.replace(/[^0-9\.\-]/g, ''); 50 | if(payload !== '' && payload>batt_max) 51 | payload=batt_max; 52 | if(payload !== '' && payload. 16 | 17 | // Monitor NETWORK/*/* messages. Save the resulting data to 18 | // global context, and pass the full context to the dashboard. 19 | 20 | const topics = msg.topic.split('/'); 21 | if (topics.length < 3) { 22 | return null; 23 | } 24 | 25 | const topic = topics[1] + '/' + topics[2]; 26 | 27 | // Save the new value to global status 28 | global.set('status.network.' + topics[1] + '.' + topics[2], msg.payload); 29 | const network = global.get('status').network; 30 | 31 | // Create a package of information for the dashboard template 32 | var message = ''; 33 | 34 | if (!network || !network.WPA || network.WPA.IP === '') { 35 | message = "CoachProxy is not connected via WiFi."; 36 | } else { 37 | message = "CoachProxyOS is connected to the WiFi network listed below."; 38 | } 39 | 40 | // The values that will be displayed are in the `data` and other parameters, 41 | // but a payload is also always returned so the RBE node can determine when 42 | // the status has changed. 43 | const rbe = message + network.WPA.SSID + network.WPA.IP + network.WPA.MAC + network.WPA.STATE + network.WPA.QUALITY + 44 | network.AP.SSID + network.AP.IP 45 | 46 | return { payload: rbe, data: network, message: message }; 47 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/notification_deliver.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Take a notification message as input, and deliver it via a variety of 18 | // methods based on the user's settings. 19 | 20 | let generic_msg = msg; 21 | let push_msg = {}; 22 | let email_msg = {}; 23 | 24 | msg.topic = null; // Block existing topic from passing through 25 | 26 | // Pushover message 27 | 28 | let pushover_key = global.get('settings2')['pushover_key']; 29 | if (pushover_key) { 30 | push_msg.headers = { "content-type": "application/x-www-form-urlencoded" }; 31 | push_msg.payload = { 32 | user: pushover_key, 33 | token: 'afvtrcguexvtutk7orsc41m1fcw42i', 34 | title: msg.subject, 35 | message: msg.payload, 36 | }; 37 | } 38 | 39 | // Email message 40 | 41 | let smtp_host = global.get('settings2')['smtp_host']; 42 | let smtp_port = global.get('settings2')['smtp_port']; 43 | let smtp_user = global.get('settings2')['smtp_user']; 44 | let smtp_pass = global.get('settings2')['smtp_password']; 45 | 46 | let transport = {}; 47 | transport.host = smtp_host; 48 | transport.port = smtp_port; 49 | transport.auth = {}; 50 | transport.auth.user = smtp_user; 51 | transport.auth.pass = smtp_pass; 52 | 53 | let email1 = global.get('settings2')['notify_email1']; 54 | let email2 = global.get('settings2')['notify_email2']; 55 | 56 | let options = {}; 57 | options.from = smtp_user; 58 | options.subject = msg.subject; 59 | options.text = msg.payload; 60 | 61 | // If the user only added email2, change it to email1 62 | if (email2 && !email1) { 63 | email1 = email2; 64 | email2 = ''; 65 | } 66 | 67 | if (email1) { 68 | options.to = email1; 69 | if (email2) { 70 | options.to += ',' + email2; 71 | } 72 | 73 | email_msg = { 'mail': { 'transport': transport, 'options': options } }; 74 | } 75 | 76 | // Deliver 77 | 78 | return [msg, push_msg, email_msg]; 79 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/notification_menu_options.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Populate the "Notification Type" menu with appropriate options 18 | // based on the user's choice of coach and options. 19 | 20 | const configs = global.get('configs'); 21 | const features = global.get('features')[configs.year][configs.model]['Default']; 22 | const ac_source = global.get('status').power.ac_source; 23 | 24 | let options = []; 25 | 26 | // Tanks 27 | options.push('Tank: Fresh'); 28 | options.push('Tank: Grey'); 29 | options.push('Tank: Black'); 30 | if (configs.lpg === 'true') { 31 | options.push('Tank: LPG'); 32 | } 33 | 34 | // Batteries 35 | options.push('Batt: House'); 36 | if (configs.batt_chassis === 'true') { 37 | options.push('Batt: Chassis'); 38 | } 39 | 40 | // Temps 41 | if (configs.tstats === 'true') { 42 | options.push('Temp: Front'); 43 | if (configs.thirdtstat === 'true') { 44 | options.push('Temp: Mid'); 45 | } 46 | options.push('Temp: Rear'); 47 | if (configs.exttemp === 'true') { 48 | options.push('Temp: Wet Bay'); 49 | } 50 | } 51 | 52 | // Toggles 53 | 54 | if (ac_source) { 55 | options.push('AC Power'); 56 | } 57 | if (features.Ignition == 1) { 58 | options.push('Ignition'); 59 | } 60 | if (features.GeneratorStatus == 1) { 61 | options.push('Generator'); 62 | } 63 | 64 | msg = { options: options, payload: null, topic: null }; 65 | return msg; 66 | 67 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/notification_save.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // After the user has defined a new notification rule, save it to the 18 | // database. 19 | 20 | var notifs = global.get('settings2')['notify_rules'] || []; 21 | var notif = {}; 22 | 23 | const topics = [ 24 | 'notif_type', 25 | 'notif_test', 26 | 'notif_value', 27 | 'notif_unit' 28 | ]; 29 | 30 | topics.forEach(function(topic) { 31 | const value = global.get('form')[topic] || ''; 32 | notif[topic] = value; 33 | }); 34 | 35 | // If a value is required but not present, give up. 36 | if (notif['notif_test'] != 'changes' && (notif['notif_value'] === '' || isNaN(notif['notif_value']))) { 37 | msg = null; 38 | } else { 39 | notifs.push(notif); 40 | notifs.sort((a,b) => a.notif_type.localeCompare(b.notif_type)); 41 | 42 | // Delete "notif_state" for existing toggle rules (e.g. "generator") so 43 | // their current state doesn't get saved to the database along with the rule. 44 | notifs.forEach(function(n) { 45 | delete n.notif_state; 46 | }); 47 | 48 | msg = { topic: 'notify_rules', payload: JSON.stringify(notifs) }; 49 | } 50 | 51 | return msg; 52 | 53 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/notification_state_monitor.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Monitor certain MQTT topics for a change in state (e.g. "ignition" 18 | // changing from "off" to "on") and send a notification. If too many 19 | // notifications are sent within a short time, enter a long "backoff" 20 | // phase where no notifications for the topic will be sent. 21 | 22 | const topics = { 23 | 'Ignition': 'CP/IGNITION', 24 | 'Park Brake': 'CP/PARKBRAKE', 25 | 'Generator': 'CP/GENERATOR', 26 | 'AC Power': 'CP/AC_SOURCE', 27 | }; 28 | 29 | // Pause for if are sent in for a topic. 30 | // Defaults: Pause for 60 minutes if 8 messages are sent within 5 minutes 31 | const pause_time = 60 * 60 * 1000; 32 | const max_messages = 8; 33 | const max_interval = 5 * 60 * 1000; 34 | 35 | let notifs = global.get('settings2')['notify_rules'] || []; 36 | let newmsg = null; 37 | 38 | // Check each notification rule 39 | for (let notif in notifs) { 40 | let n = notifs[notif]; 41 | 42 | // If the current message topic matches a notification rule, process the rule 43 | if (topics[n.notif_type] == msg.topic) { 44 | 45 | // Look up the previous state of this topic. If this is the first time 46 | // receiving a message for the topic, set it to the current state to 47 | // prevent sending an alarm each time CoachProxy starts up. 48 | let prev_state = n.notif_state || msg.payload; 49 | 50 | // Look up the count of alarms sent recently for this topic 51 | let count = n.notif_count || 0; 52 | 53 | // Look up the start of the current backoff interval 54 | let interval_start = n.notif_interval || Date.now(); 55 | 56 | // If notifications are paused, see if it's time to release them 57 | if (n.notif_pause && Date.now() - n.notif_pause > pause_time) { 58 | n.notif_pause = null; 59 | } 60 | 61 | // If the current interval is over, clear it out 62 | if (Date.now() - interval_start > max_interval) { 63 | count = 0; 64 | n.notif_count = 0; 65 | n.notif_interval = null; 66 | } 67 | 68 | // Alarm if the current value is different from the previous value. 69 | if (msg.payload != prev_state && !n.notif_pause) { 70 | count++; 71 | 72 | // If it's over its threshold, start the pause clock, otherwise send alarm. 73 | if (count > max_messages) { 74 | newmsg = {}; 75 | newmsg.payload = 'Too many notifications being sent for ' + n.notif_type + '. '; 76 | newmsg.payload += 'Pausing for ' + (pause_time / 60 / 1000) + " minutes.\n"; 77 | newmsg.subject = 'CoachProxy ALERT for ' + n.notif_type; 78 | 79 | n.notif_pause = Date.now(); 80 | n.notif_count = 0; 81 | n.notif_interval = null; 82 | } else { 83 | newmsg = {}; 84 | newmsg.payload = n.notif_type + ' has changed from ' + prev_state + ' to ' + msg.payload + ".\n"; 85 | newmsg.subject = 'CoachProxy ALERT for ' + n.notif_type; 86 | 87 | if (count == 1) { 88 | n.notif_interval = Date.now(); 89 | } 90 | n.notif_count = count; 91 | } 92 | } 93 | 94 | // Update the latest state 95 | n.notif_state = msg.payload; 96 | } 97 | } 98 | 99 | return newmsg; 100 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/notification_submenu_options.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Once a user chooses a notification type when creating a new rule, populate 18 | // the submenus (test case, test value) with sensible options and default values. 19 | 20 | const values = { 'tank': '67', 'batt': '11.8', 'temp': '85', 'wet': '40' }; 21 | const units = { 'tank': '%', 'batt': 'V', 'temp': 'ºF', 'wet': 'ºF' }; 22 | const tests = { 'tank': 'is above', 'batt': 'is below', 'temp': 'is above', 'wet': 'is below' }; 23 | 24 | var msg_value = { topic: 'notif_value' }; 25 | var msg_unit = { topic: 'notif_unit' }; 26 | var msg_test = { topic: 'notif_test', payload: "is above" }; 27 | 28 | msg.topic = null; // Required to block existing topic from passing through 29 | 30 | // Update default value, unit, and test for numeric rules 31 | for (let key in values) { 32 | if (msg.payload && msg.payload.toLowerCase().includes(key)) { 33 | msg_test.options = ['is above', 'is below']; 34 | msg_test.payload = tests[key]; 35 | msg_value.payload = values[key]; 36 | msg_value.enabled = true; 37 | msg_unit.payload = units[key]; 38 | } 39 | } 40 | 41 | // Update menus for non-numeric rules (e.g. Ignition) 42 | const toggles = ['AC Power', 'Ignition', 'Generator']; 43 | if (toggles.includes(msg.payload)) { 44 | msg_test.options = ['changes']; 45 | msg_test.payload = 'changes'; 46 | msg_value.payload = ''; 47 | msg_value.enabled = false; 48 | msg_unit.payload = ''; 49 | } 50 | 51 | return [msg_value, msg_unit, msg_test]; 52 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/notification_value_monitor.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Monitor MQTT topics for values that are outside the desired range (e.g. 18 | // battery voltage below 11.8 V). To prevent frequent notifications around 19 | // alarm thresholds (e.g. voltage may fluctuate from 11.7 to 11.8 for a while), 20 | // a value must be out of range (faulted) for a certain time before an alarm 21 | // is sent (triggered), and then must be within the valid range (recovering) 22 | // for a certain time before the alarm is considered cleared. 23 | // 24 | // good value bad value 25 | // +------------------+ +------------------------+ 26 | // | | >30 sec | | 27 | // v bad | bad v good | 28 | // +---------+ value +---------+ value +-----------+ value +------------+ 29 | // | cleared |------>| faulted |------->| triggered |--------->| recovering | 30 | // +---------+ +---------+ +-----------+ +------------+ 31 | // ^ ^ | good value| 32 | // | | >24 hours | >5 min| 33 | // | +----------------------------------+ | 34 | // +---------------------------------------------------------------+ 35 | 36 | const topics = { 37 | 'Tank: Fresh': 'CP/TANK_STATUS/Fresh', 38 | 'Tank: Grey': 'CP/TANK_STATUS/Grey', 39 | 'Tank: Black': 'CP/TANK_STATUS/Black', 40 | 'Temp: Front': 'CP/THERMISTOR_TEMP/Front', 41 | 'Temp: Mid': 'CP/THERMISTOR_TEMP/Mid', 42 | 'Temp: Rear': 'CP/THERMISTOR_TEMP/Rear', 43 | 'Temp: Wet Bay': 'CP/THERMISTOR_TEMP/Wet Bay', 44 | 'Batt: House': 'CP/BATTERY_VOLTS/House', 45 | 'Batt: Chassis': 'CP/BATTERY_VOLTS/Chassis', 46 | }; 47 | 48 | const fault_timeout = 30 * 1000; // receive bad messages for 30 seconds before alarming 49 | const recover_timeout = 5 * 60 * 1000; // receive good messages for 5 minutes before clearing 50 | const trigger_timeout = 24 * 60 * 60 * 1000; // reset alarm after 24 hours even with bad messages 51 | 52 | let notifs = global.get('settings2')['notify_rules'] || []; 53 | let ignition = global.get('status').chassis.ignition || 'off'; 54 | let messages = []; 55 | 56 | // Check each notification rule 57 | for (let notif in notifs) { 58 | let n = notifs[notif]; 59 | let status = n.notif_status || 'cleared'; 60 | 61 | // If the current message topic matches a notification rule, process the rule. 62 | // Exception: ignore tanks when the ignition is on. 63 | if (topics[n.notif_type] == msg.topic && (ignition == 'off' || (msg.topic).includes('TANK') == false)) { 64 | 65 | let oldstatus = status; 66 | 67 | // Check whether the current value is outside the desired range 68 | let outofrange = false; 69 | if ((n.notif_test == 'is below' && parseFloat(msg.payload) < parseFloat(n.notif_value)) || 70 | (n.notif_test == 'is above' && parseFloat(msg.payload) > parseFloat(n.notif_value))) { 71 | outofrange = true; 72 | } 73 | 74 | // Perform actions based on which state the rule is currently in 75 | switch (status) { 76 | case 'cleared': 77 | if (outofrange) { 78 | status = 'faulted'; 79 | n.fault_time = Date.now(); 80 | } 81 | break; 82 | case 'faulted': 83 | if (!outofrange) { 84 | status = 'cleared'; 85 | } else if (Date.now() - n.fault_time > fault_timeout) { 86 | status = 'triggered'; 87 | n.trigger_time = Date.now(); 88 | let message = n.notif_type + ' is currently ' + msg.payload + n.notif_unit; 89 | message += ' which ' + n.notif_test; 90 | message += ' your threshold of ' + n.notif_value + n.notif_unit + ".\n"; 91 | let newmsg = { 92 | subject: 'CoachProxy ALERT for ' + n.notif_type, 93 | payload: message, 94 | } 95 | messages.push(newmsg); 96 | } 97 | break; 98 | case 'triggered': 99 | if (!outofrange) { 100 | status = 'recovering'; 101 | n.recover_time = Date.now(); 102 | } else if (Date.now() - n.trigger_time > trigger_timeout) { 103 | status = 'cleared'; 104 | } 105 | break; 106 | case 'recovering': 107 | if (outofrange) { 108 | status = 'triggered'; 109 | n.trigger_time - Date.now(); 110 | } else if (Date.now() - n.recover_time > recover_timeout) { 111 | status = 'cleared'; 112 | } 113 | break; 114 | default: 115 | break; 116 | } 117 | 118 | n.notif_status = status; 119 | } 120 | } 121 | 122 | return messages; 123 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/power_flow_function.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Set variables used in the advanced power flow diagram and update the 18 | // diagram template node. Variables updated include the battery voltages, 19 | // battery icons, power flow direction icons and colors, and charger amps. 20 | // 21 | // Inputs to this function are discarded, and are only used to trigger an 22 | // update of the diagram template node. Actual values are read from 23 | // global context. 24 | var payload = {}; 25 | 26 | payload.charger_state = global.get('status').power.charger_state; 27 | payload.inverter_state = global.get('status').power.inverter_status; 28 | payload.house_volts = global.get('status').power.house_battery_volts; 29 | payload.chassis_volts = global.get('status').power.chassis_battery_volts; 30 | 31 | // Choose battery icons and colors 32 | var colors = { 0: '#9F292C', 1: '#9F292C', 2:'#EAA626', 3: '#5187BA', 4: '#5187BA'}; 33 | var battery_level; 34 | 35 | // Convert voltage to a 0-4 value. 36 | battery_level = Math.max(0, Math.min(4, Math.trunc(payload.house_volts / 0.3 - 38))); 37 | payload.house_icon = 'fa-battery-' + battery_level; 38 | payload.house_color = colors[battery_level]; 39 | 40 | battery_level = Math.max(0, Math.min(4, Math.trunc(payload.chassis_volts / 0.3 - 38))); 41 | payload.chassis_icon = 'fa-battery-' + battery_level; 42 | payload.chassis_color = colors[battery_level]; 43 | 44 | // Tweak some RV-C labels 45 | if (payload.charger_state == 'undefined' || payload.charger_state == 'do not charge') { 46 | payload.charger_state = 'charger off'; 47 | } else { 48 | payload.charger_state += ' charge'; // e.g. "Float charge" 49 | } 50 | 51 | // Set the charger <-> house variables 52 | var dc_amps = global.get('status').power.inverter_dc_amps; 53 | if (dc_amps < 0) { 54 | // Charging 55 | payload.inverter_amps = "+" + -dc_amps; 56 | payload.house_flow = "fa-lg fa-angle-double-down faa-falling"; 57 | payload.house_flow_color = colors[4]; 58 | } else if (dc_amps > 0) { 59 | // Inverting 60 | payload.inverter_amps = "-" + dc_amps; 61 | payload.house_flow = "fa-lg fa-angle-double-up faa-falling-reverse"; 62 | payload.house_flow_color = colors[2]; 63 | } else { 64 | // Idle 65 | payload.inverter_amps = "0"; 66 | payload.house_flow = 'fa-bars'; 67 | payload.house_flow_color = "#888"; 68 | } 69 | 70 | // Set the house <-> chassis variables 71 | payload.chassis_flow = 'fa-bars'; 72 | payload.chassis_flow_color = "#888"; 73 | 74 | if (global.get('status').power.battery_merge === true) { 75 | payload.chassis_flow_color = colors[4]; 76 | payload.chassis_flow = "fa-lg fa-angle-double-down faa-falling"; 77 | if (payload.charger_state == 'charger off' && payload.chassis_volts > 13 && payload.house_volts > 12.8) { 78 | payload.chassis_flow = "fa-lg fa-angle-double-up faa-falling-reverse"; 79 | } 80 | } 81 | 82 | return { payload: payload }; 83 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/preset_delete.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | var form = global.get('form.preset'); 18 | var number = form.item; 19 | var preset = { name: '' }; 20 | 21 | if (number > 0) { 22 | // Save preset to database 23 | var preset_data = JSON.stringify(preset); 24 | var newmsg = {}; 25 | newmsg.topic = 'INSERT OR REPLACE INTO presets (number, data) VALUES(?, ?)'; 26 | newmsg.payload = [number, preset_data]; 27 | 28 | // Update global context. Converting to JSON and 29 | // back works around saving a reference to the 30 | // current values (e.g. 'preset.lights') instead 31 | // of the desired values. 32 | var data = JSON.parse(preset_data); 33 | global.set('presets[' + number + ']', data); 34 | 35 | return [ newmsg, {} ]; 36 | } 37 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/preset_restore.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Replay a saved preset 18 | 19 | var number = msg.topic; 20 | var preset = global.get('presets')[number]; 21 | 22 | if (preset) { 23 | 24 | // Lights 25 | var light_msg = []; 26 | if (preset.lights) { 27 | let commands = { 'dim': 0, 'on': 2, 'off': 3 }; 28 | for (let id in preset.lights) { 29 | let light = preset.lights[id]; 30 | let brightness = light.brightness; 31 | let command; 32 | command = brightness > 95 ? commands['on'] : commands['dim']; 33 | command = light.state == 'off' ? commands['off'] : command; 34 | let msg = { payload: id + ' ' + command + ' ' + brightness }; 35 | light_msg.push(msg); 36 | } 37 | } 38 | 39 | // Thermostats 40 | var tstat_msg = []; 41 | var tstat_furnace_zone = parseInt(global.get('settings2').tstat_first_zone) + 3; 42 | if (preset.tstat) { 43 | for (let id in preset.tstat) { 44 | let tstat = preset.tstat[id]; 45 | if (tstat) { 46 | // Set the thermostat mode. If it was saved as 'fan only', then set 47 | // the mode to 'off' and deal with the fan later. 48 | if (tstat.mode) { 49 | let msg = ''; 50 | if (tstat.mode == 'fan only') { 51 | msg = { payload: id + ' off' }; 52 | } else { 53 | msg = { payload: id + ' ' + tstat.mode }; 54 | } 55 | tstat_msg.push(msg); 56 | } 57 | 58 | // Some settings are only for the roof units 59 | if (id < tstat_furnace_zone) { 60 | 61 | // If there's a temperature setpoint, restore it. 62 | if (tstat.heatset && tstat.heatset > 0) { 63 | let msg = { payload: id + ' set ' + tstat.heatset }; 64 | tstat_msg.push(msg); 65 | } 66 | 67 | // If there's fan mode information, resore it. 68 | if (tstat.fanmode) { 69 | let cmd = ''; 70 | if (tstat.fanmode == 'auto') { 71 | cmd = 'auto'; 72 | } else { 73 | let commands = { 0: 'auto', 50: 'low', 100: 'high' }; 74 | cmd = commands[tstat.fanspeed]; 75 | } 76 | 77 | let msg = { payload: id + ' ' + cmd + ' ' + tstat.mode }; 78 | tstat_msg.push(msg); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | //var floor_msg = []; 86 | //if (preset.floors) { 87 | //let method = global.get('floor_heat_method'); 88 | //for (let id in preset.floors) { 89 | 90 | //} 91 | //} 92 | 93 | return [light_msg, tstat_msg, null, null]; 94 | } 95 | 96 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/preset_save.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Save a preset to the database 18 | 19 | var preset = {}; 20 | var form = global.get('form.preset'); 21 | var number = form.item; 22 | preset.name = form.name; 23 | 24 | if (number > 0) { 25 | if (form.opt_lights === true) { 26 | preset.lights = global.get('status').lights; 27 | } 28 | if (form.opt_vents === true) { 29 | preset.vents = global.get('status').vents; 30 | preset.fans = global.get('status').fans; 31 | } 32 | if (form.opt_floors === true) { 33 | preset.floors = global.get('status').floors; 34 | } 35 | if (form.opt_tstats === true) { 36 | preset.tstat = global.get('status').tstat; 37 | } 38 | 39 | // Save preset to database 40 | var preset_data = JSON.stringify(preset); 41 | var newmsg = {}; 42 | newmsg.topic = 'INSERT OR REPLACE INTO presets (number, data) VALUES(?, ?)'; 43 | newmsg.payload = [number, preset_data]; 44 | 45 | // Update global context. Converting to JSON and back works around saving a 46 | // reference to the current values (e.g. 'preset.lights') instead of the 47 | // desired values. 48 | var data = JSON.parse(preset_data); 49 | global.set('presets[' + number + ']', data); 50 | 51 | return [ newmsg, {} ]; 52 | } 53 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/thermostat_ambient_decode.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | var zone = msg.payload.instance; 18 | var tstat_first_zone = parseInt(global.get('settings2').tstat_first_zone) || 0; 19 | var tstat_zone_names = ['Front', 'Mid', 'Rear']; 20 | 21 | if (zone >= tstat_first_zone && zone <= tstat_first_zone + 2) { 22 | data = {}; 23 | data.zone_name = tstat_zone_names[zone - tstat_first_zone]; 24 | data.ambient_temp = msg.payload['ambient temp F']; 25 | data.ambient_temp = Math.round(data.ambient_temp*10)/10; 26 | data.unit = 'F'; 27 | msg.payload = data; // Get rid of things that might confuse RBE 28 | 29 | messages = [null, null, null]; 30 | messages[zone - tstat_first_zone] = msg; 31 | return messages; 32 | } 33 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/thermostat_ambient_decode_ext.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | var zone = msg.payload.instance; 18 | var zone_names = { 19 | 6: 'Wet Bay', 20 | 7: 'Generator Bay' 21 | }; 22 | var round = true; 23 | 24 | if (zone in zone_names) { 25 | data = {}; 26 | data.zone_name = zone_names[zone]; 27 | data.ambient_temp = msg.payload['ambient temp F']; 28 | if (round) { 29 | data.ambient_temp = Math.round(data.ambient_temp*2)/2; 30 | } 31 | data.unit = 'F'; 32 | msg.payload = data; // Get rid of things that might confuse RBE 33 | 34 | messages = [null, null]; 35 | messages[zone - 6] = msg; 36 | 37 | return messages; 38 | } 39 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/thermostat_command.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Sometimes zone 0 is the front, sometimes it's zone 2 (if heated floors are 0 and 1) 18 | const firstzone = parseInt(global.get('settings2').tstat_first_zone) || 0; 19 | 20 | // The zone name (e.g. front) that sent this command 21 | const thiszone = msg.topic; 22 | 23 | // Figure out the actual instance ID (0-2) 24 | const instances = ['tstat_front', 'tstat_mid', 'tstat_rear', 'tstat_front_furn', 'tstat_rear_furn']; 25 | const instance = instances.indexOf(thiszone) + firstzone; 26 | 27 | const status = global.get('status').tstat[instance]; 28 | const current_mode = status.mode; 29 | 30 | const fan_buttons = ['off', 'low', 'high']; 31 | const current_fanmode = status.fanmode; 32 | const current_speed = status.fanspeed; 33 | const current_fan_button = fan_buttons[current_speed / 50]; 34 | 35 | const furnzone = ([3, -99, 4][instance - firstzone]) + firstzone; 36 | 37 | var command = msg.payload; 38 | var msgs = []; 39 | 40 | // If payload contains a command (not a number)... 41 | if (isNaN(msg.payload)) { 42 | // Is this a fan command? 43 | if (fan_buttons.includes(msg.payload)) { 44 | // Turn off current mode, e.g. "low" pressed while fan already on low. 45 | if (current_fanmode == 'on' && msg.payload == current_fan_button) { 46 | command = 'auto'; 47 | } 48 | } else { 49 | if (msg.payload == current_mode) { 50 | // Turn off current mode, e.g. "cool" pressed while already cooling. 51 | command = 'off'; 52 | } else if (command == 'cool' || command == 'heat') { 53 | // If heat or cool is being turned on, ensure opposing mode is off for 54 | // all thermostats. Spyder takes care of this automatically for 2018+ 55 | // models, but in the 2017 VanLeigh Vilano, it's possible to end up with 56 | // both the air conditioner and furnace on at the same time without this 57 | // check. 58 | const disable_mode = (command == 'cool' ? 'heat' : 'cool'); 59 | for (let i = firstzone; i < firstzone + 5; i++) { 60 | let zonestatus = global.get('status').tstat[i]; 61 | if (zonestatus && zonestatus.mode && zonestatus.mode == disable_mode) { 62 | msgs.push( { payload: i + ' off' } ); 63 | } 64 | } 65 | } 66 | } 67 | msgs.push( { payload: [instance, command, current_mode].join(' ') } ); 68 | 69 | // If the "off" button was pressed, also turn off the associated 70 | // furnace for front and rear instances. 71 | if (msg.payload == 'off' && furnzone >= 0) { 72 | msgs.push( { payload: [furnzone, command].join(' ') } ); 73 | } 74 | } 75 | 76 | // Payload contains a temperature setpoint (a number)... 77 | else { 78 | msgs.push( { payload: [instance, 'set', msg.payload].join(' ') } ); 79 | } 80 | 81 | return [msgs]; 82 | 83 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/thermostat_status_decode.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Sometimes zone 0 is the front, sometimes it's zone 2 (if heated floors are 0 and 1) 18 | const firstzone = parseInt(global.get('settings2').tstat_first_zone) || 0; 19 | 20 | const zone = msg.payload.instance; 21 | const heatset = parseInt(msg.payload['setpoint temp heat F']); 22 | const coolset = parseInt(msg.payload['setpoint temp cool F']); 23 | const mode = msg.payload['operating mode definition']; 24 | 25 | const fanspeed = parseInt(msg.payload['fan speed']); 26 | var fanmode = msg.payload['fan mode definition']; 27 | 28 | const furnzone = ([3, -99, 4][zone - firstzone]) + firstzone; 29 | const furnstatus = global.get('status').tstat[furnzone] || {}; 30 | const furnmode = 'mode' in furnstatus ? furnstatus.mode : 'off'; 31 | 32 | const ac_on = global.get('status').tstat[zone].ac_on || false; 33 | const hp_on = global.get('status').tstat[zone].hp_on || false; 34 | 35 | var newstatus = { 36 | mode: mode, 37 | heatset: heatset, 38 | coolset: coolset, 39 | fanmode: fanmode, 40 | fanspeed: fanspeed, 41 | ac_on: ac_on, 42 | hp_on: hp_on 43 | }; 44 | global.set('status.tstat[' + zone + ']', newstatus); 45 | 46 | if (fanmode == 'on') { 47 | if (fanspeed == 50) { 48 | fanmode = 'low'; 49 | } else { 50 | fanmode = 'high'; 51 | } 52 | } 53 | 54 | // Add a symbol to the air conditioner/heat pump label when it's running 55 | var ac_label = ac_on ? '❄︎ A/C' : 'A/C'; 56 | var hp_label = hp_on ? '☀︎︎ H.PUMP' : 'H.PUMP'; 57 | 58 | // Colors for on/off status of buttons 59 | var on = { foreground: 'white', background: '#3D89BE' }; 60 | var off = { foreground: 'black', background: '#dddddd' }; 61 | 62 | // Off button highlighted if both furnace and heat pump are 'off' 63 | var offstatus = (mode == 'off' && furnmode == 'off') ? on : off; 64 | var coolstatus = mode == 'cool' ? on : off; 65 | var heatstatus = mode == 'heat' ? on : off; 66 | var highstatus = fanmode == 'high' ? on : off; 67 | var lowstatus = fanmode == 'low' ? on : off; 68 | var autostatus = fanmode == 'auto' ? on : off; 69 | 70 | // Create an array of objects to return from the node's various outputs. 71 | // Most of the parameters are for coloring and updating the button, but 72 | // a payload is also always returned so the RBE node can determine when 73 | // the status has changed. 74 | return [ 75 | Object.assign({ payload: coolstatus.foreground + ac_label }, coolstatus, { label: ac_label }), 76 | Object.assign({ payload: heatstatus.foreground + hp_label }, heatstatus, { label: hp_label }), 77 | Object.assign({ payload: offstatus.foreground }, offstatus), 78 | { payload: coolset }, 79 | Object.assign({ payload: highstatus.foreground }, highstatus), 80 | Object.assign({ payload: lowstatus.foreground }, lowstatus), 81 | Object.assign({ payload: autostatus.foreground }, autostatus) 82 | ] 83 | 84 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/uptime_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // format check value 18 | // msg.payload=1431579097; 19 | now=new Date().getTime() / 1000; 20 | diff=Math.floor(now-msg.payload); 21 | 22 | outmsg=""; 23 | if(diff>86400) { 24 | days=Math.floor(diff/86400); 25 | diff-=days*86400; 26 | if(days>365) { 27 | years=Math.floor(days/365); 28 | days-=(years*365); 29 | outmsg=years+"y "; 30 | } 31 | outmsg+=days+"d "; 32 | } 33 | if(diff>3600) { 34 | hours=Math.floor(diff/3600); 35 | diff-=hours*3600; 36 | outmsg+=hours+"h "; 37 | } 38 | 39 | if(diff>60) { 40 | minutes=Math.floor(diff/60); 41 | diff=0; 42 | outmsg+=minutes+"m"; 43 | } else 44 | outmsg+="0m"; 45 | 46 | msg.payload=outmsg; 47 | 48 | return msg; 49 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_direct_cancel.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Reset the saved form data 18 | global.set('form.apname', ''); 19 | global.set('form.appass', ''); 20 | 21 | return { payload: '' }; 22 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_direct_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | fields = ['apname', 'appass']; 18 | msg.topic = null; // Required to block pass-through topic 19 | 20 | outmsg = []; 21 | var payload; 22 | for (var i = 0; key = fields[i]; i++) { 23 | if (key == 'appass') { 24 | payload = ''; 25 | } else { 26 | payload = msg.payload[0][key]; 27 | } 28 | 29 | var tmpmsg = { 30 | topic: key, 31 | payload: payload 32 | }; 33 | outmsg.push(tmpmsg); 34 | } 35 | 36 | return outmsg; 37 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_direct_save.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Get the saved form data 18 | const apname = global.get('form').apname || ''; 19 | const appass = global.get('form').appass || ''; 20 | 21 | // Save settings to database 22 | var newmsg = {}; 23 | 24 | if (appass.length < 8) { 25 | return [ null, { payload: 'Password too short. Aborted.' } ] 26 | } else { 27 | // Reset the saved form data 28 | global.set('form.appass', ''); 29 | 30 | newmsg.topic = 'UPDATE SETTINGS SET `apname`=?, `appass`=?'; 31 | newmsg.payload = [apname, appass]; 32 | return [ newmsg, { payload: 'Settings saved...'} ]; 33 | } 34 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_password_blank.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | msg.payload/=1000; 18 | if(global.get('wifi').passDTStamp!==0 && msg.payload-global.get('wifi').passDTStamp>120) { 19 | global.set('wifi.passDTStamp', 0); 20 | return {payload:""}; 21 | } 22 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_scanner_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | networks=[{"-Select Network Here-":"Fill in Name"}]; 18 | seen={}; 19 | 20 | msg.payload.sort(function(a, b) { 21 | return parseFloat(b.signal_level) - parseFloat(a.signal_level); 22 | }); 23 | 24 | const apssid = global.get('status').network.AP.SSID || ''; 25 | seen[apssid] = 1; 26 | 27 | for (var i=0;key=msg.payload[i];i++) { 28 | if (msg.payload[i].ssid !== "" && !seen[msg.payload[i].ssid]) { 29 | networks.push(msg.payload[i].ssid); 30 | seen[msg.payload[i].ssid] = 1; 31 | } 32 | } 33 | 34 | return [{'options':networks,'payload':"Fill in Name"},{'payload':"Scan Complete"}]; 35 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_static_cancel.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Reset the saved form data 18 | global.set('form.dhcp', ''); 19 | global.set('form.ipaddr', ''); 20 | global.set('form.netmask', ''); 21 | global.set('form.gateway', ''); 22 | 23 | return { payload: '' }; 24 | 25 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_static_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | fields = ['dhcp', 'ipaddr', 'netmask', 'gateway']; 18 | msg.topic = null; // Required to block pass-through topic 19 | 20 | outmsg = []; 21 | for (var i = 0; key = fields[i]; i++) { 22 | var tmpmsg = { 23 | topic: key, 24 | payload: msg.payload[0][key] 25 | }; 26 | outmsg.push(tmpmsg); 27 | } 28 | 29 | return outmsg; 30 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_static_save.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Get the saved form data 18 | const staticenabled = global.get('form').dhcp || 0; 19 | const staticip = global.get('form').ipaddr || ''; 20 | const staticmask = global.get('form').netmask || ''; 21 | const staticgw = global.get('form').gateway || ''; 22 | 23 | function validateIPaddress(ipaddress) { 24 | var ipformat = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 25 | return ipaddress.match(ipformat) 26 | } 27 | 28 | var error = ''; 29 | if (staticenabled == 1) { 30 | if (! validateIPaddress(staticip)) { 31 | error = error + 'IP address not valid. '; 32 | } 33 | if (! validateIPaddress(staticgw)) { 34 | error = error + 'Gateway address not valid. '; 35 | } 36 | if (! validateIPaddress(staticmask)) { 37 | error = error + 'Netmask not valid. '; 38 | } 39 | 40 | if (error.length > 1) { 41 | return [ null, { payload: error } ]; 42 | } 43 | } 44 | 45 | // Save settings to database 46 | var newmsg = {}; 47 | 48 | newmsg.topic = 'UPDATE SETTINGS SET `dhcp`=?, `ipaddr`=?, `netmask`=?, `gateway`=?'; 49 | newmsg.payload = [staticenabled, staticip, staticmask, staticgw]; 50 | 51 | return [ newmsg, { payload: 'Settings saved...'} ]; 52 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_wpa_cancel.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Reset the saved form data 18 | global.set('form.wifiname', ''); 19 | global.set('form.wifipass', ''); 20 | 21 | return { payload: '' }; 22 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_wpa_parse.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | fields = ['wifiname', 'wifipass']; 18 | msg.topic = null; // Required to block pass-through topic 19 | 20 | outmsg = []; 21 | var payload; 22 | for (var i = 0; key = fields[i]; i++) { 23 | if (key == 'wifipass') { 24 | payload = ''; 25 | } else { 26 | payload = msg.payload[0][key]; 27 | } 28 | 29 | var tmpmsg = { 30 | topic: key, 31 | payload: payload 32 | }; 33 | outmsg.push(tmpmsg); 34 | } 35 | 36 | return outmsg; 37 | -------------------------------------------------------------------------------- /roles/coachproxy/files/node-red/js/wifi_wpa_save.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Wandertech LLC 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 | // Get the saved form data 18 | const wifiname = global.get('form').wifiname || ''; 19 | const wifipass = global.get('form').wifipass || ''; 20 | 21 | // Reset the saved form data 22 | global.set('form.wifipass', ''); 23 | 24 | // Save settings to database 25 | var newmsg = {}; 26 | 27 | newmsg.topic = 'UPDATE SETTINGS SET `wifiname`=?, `wifipass`=?'; 28 | newmsg.payload = [wifiname, wifipass]; 29 | 30 | return [ newmsg, { payload: 'Settings saved...'} ]; 31 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/ac_load.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # For the 2018 Phaeton (and presumably other models), the electric water heater 19 | # toggle requires both a DC_DIMMER_COMMAND and AC_LOAD_COMMAND. 20 | 21 | use strict; 22 | no strict 'refs'; 23 | 24 | our $debug=0; 25 | 26 | our %deccommands=(1=>'On (Duration)',2=>'On (Delay)',3=>'Off (Delay)'); 27 | 28 | if ( scalar(@ARGV) < 2 ) { 29 | print "ERR: Insufficient command line data provided.\n"; 30 | usage(); 31 | } 32 | 33 | if(!exists($deccommands{$ARGV[1]})) { 34 | print "ERR: Command not allowed. Please see command list below.\n"; 35 | usage(); 36 | } 37 | 38 | our ($prio,$dgnhi,$dgnlo,$srcAD,$instance,$command,$brightness)=(6,'1FF','BE',99,$ARGV[0],$ARGV[1],200); 39 | $brightness=0 if ($command == 3); 40 | 41 | our $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 42 | our $hexCanId=sprintf("%08X",oct("0b$binCanId")); 43 | our $hexData=sprintf("%02XFF%02X8000000000",$instance,$brightness); 44 | 45 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 46 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if ($debug); 47 | 48 | sub usage { 49 | print "Usage: \n"; 50 | print "\tac_load.pl \n"; 51 | print "\n\t is required and one of:\n"; 52 | foreach my $key ( sort {$a <=> $b} keys %deccommands ) { 53 | print "\t\t".$key." = ".$deccommands{$key} . "\n"; 54 | } 55 | print "\n"; 56 | exit(1); 57 | } 58 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/ceiling_fan.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug = 0; 22 | 23 | our %loads = ( 24 | 1 => "Bedroom", 25 | 2 => "Bedroom (2018+ Open Road)", 26 | ); 27 | 28 | our %deccommands = ( 29 | 0 => 'Off', 1 => 'Low', 2 => 'High', 30 | ); 31 | 32 | if ( scalar(@ARGV) < 2 ) { 33 | print "ERR: Insufficient command line data provided.\n"; 34 | usage(); 35 | } 36 | 37 | if (!exists($loads{$ARGV[0]})) { 38 | print "ERR: Load does not exist. Please see load list below.\n"; 39 | usage(); 40 | } 41 | 42 | if (!exists($deccommands{$ARGV[1]})) { 43 | print "ERR: Command not allowed. Please see command list below.\n"; 44 | usage(); 45 | } 46 | 47 | 48 | our ($prio,$dgnhi,$dgnlo,$srcAD,$instance,$command) = (6,'1FE','DB',96,$ARGV[0],$ARGV[1]); 49 | our ($hexData,$binCanId,$hexCanId) = (0,0,0); 50 | 51 | our %specials = ( 52 | 1 => { 0 => [ 35, 36 ], 1=> [ 35, 36 ], 2 => [ 36, 35 ] }, 53 | 2 => { 0 => [ 33, 34 ], 1=> [ 33, 34 ], 2 => [ 34, 33 ] }, 54 | ); 55 | 56 | if (exists($specials{$instance})) { 57 | $binCanId = sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 58 | $hexCanId = sprintf("%08X",oct("0b$binCanId")); 59 | 60 | if ($command > 0) { 61 | $hexData = sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[1],3,0); 62 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 63 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 64 | 65 | $hexData = sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[0],5,255); 66 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 67 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 68 | } else { 69 | $hexData = sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[0],3,0); 70 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 71 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 72 | 73 | $hexData = sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[1],3,0); 74 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 75 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 76 | } 77 | } 78 | 79 | sub usage { 80 | print "Usage: \n"; 81 | print "\t$0 \n"; 82 | print "\n\t is one of:\n"; 83 | foreach my $key ( sort {$a <=> $b} keys %loads ) { 84 | print "\t\t".$key." = ".$loads{$key} . "\n"; 85 | } 86 | print "\n\t is one of:\n"; 87 | foreach my $key ( sort {$a <=> $b} keys %deccommands ) { 88 | print "\t\t".$key." = ".$deccommands{$key} . "\n"; 89 | } 90 | print "\n"; 91 | exit(1); 92 | } 93 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/dc_ac_combo.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # dc_ac_combo.pl 19 | # 20 | # Send an on or off command (2 or 3) to both a DC and an AC system at the same 21 | # time. This is used 2018+ Tiffins which use both a DC_DIMMER_COMMAND_2 and an 22 | # AC_LOAD_COMMAND to control the electric side of the AquaHot. This script 23 | # simply calls the corresponding dc_dimmer.pl and ac_load.pl scripts with the 24 | # provided load IDs and command. 25 | 26 | use strict; 27 | no strict 'refs'; 28 | 29 | our %commands = (2 => 'On (Delay)' , 3 => 'Off (Delay)'); 30 | 31 | if ( scalar(@ARGV) < 3 ) { 32 | print "ERR: Insufficient command line data provided.\n"; 33 | usage(); 34 | } 35 | 36 | our ($dc_instance, $ac_instance, $command) = ($ARGV[0], $ARGV[1], $ARGV[2]); 37 | 38 | system("/coachproxy/rv-c/dc_dimmer.pl $dc_instance $command"); 39 | system("/coachproxy/rv-c/ac_load.pl $ac_instance $command"); 40 | 41 | 42 | sub usage { 43 | print "Usage: \n"; 44 | print "\tdc_ac_combo.pl \n"; 45 | print "\n\t is required and one of:\n"; 46 | foreach my $key ( sort {$a <=> $b} keys %commands ) { 47 | print "\t\t".$key." = ".$commands{$key} . "\n"; 48 | } 49 | print "\n"; 50 | exit(1); 51 | } 52 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/dc_dimmer.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug=0; 22 | 23 | our %deccommands=(0=>'Set Level(delay)',1=>'On (Duration)',2=>'On (Delay)',3=>'Off (Delay)', 24 | 5=>'Toggle',6=>'Memory Off',17=>'Ramp Brightness',18=>'Ramp Toggle',19=>'Ramp Up', 25 | 20=>'Ramp Down',21=>'Ramp Down/Up'); 26 | 27 | if ( scalar(@ARGV) < 2 ) { 28 | print "ERR: Insufficient command line data provided.\n"; 29 | usage(); 30 | } 31 | 32 | if(!exists($deccommands{$ARGV[1]})) { 33 | print "ERR: Command not allowed. Please see command list below.\n"; 34 | usage(); 35 | } 36 | 37 | our ($prio,$dgnhi,$dgnlo,$srcAD,$instance,$command,$brightness,$duration,$bypass)=(6,'1FE','DB',99,$ARGV[0],$ARGV[1],($ARGV[2]||100)*2,$ARGV[3]||255,$ARGV[4]||0); 38 | our $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 39 | 40 | our $hexData=sprintf("%02XFF%02X%02X%02X00FFFF",$instance,$brightness,$command,$duration); 41 | our $hexCanId=sprintf("%08X",oct("0b$binCanId")); 42 | 43 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 44 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if ($debug); 45 | if($command==0 || $command==17) { 46 | sleep 5 if($command==17 && $bypass==0); 47 | $brightness=0; 48 | $command=21; 49 | $duration=0; 50 | $hexData=sprintf("%02XFF%02X%02X%02X00FFFF",$instance,$brightness,$command,$duration); 51 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 52 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if ($debug); 53 | $command=4; 54 | $hexData=sprintf("%02XFF%02X%02X%02X00FFFF",$instance,$brightness,$command,$duration); 55 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 56 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if ($debug); 57 | } 58 | 59 | 60 | sub usage { 61 | print "Usage: \n"; 62 | print "\tdimmer_RV-C.pl {brightness} {time}\n"; 63 | print "\n\t is required and one of:\n"; 64 | print "\t\t {1..99} (check the *.dc_loads.txt files for a list)\n"; 65 | print "\n\t is required and one of:\n"; 66 | foreach my $key ( sort {$a <=> $b} keys %deccommands ) { 67 | print "\t\t".$key." = ".$deccommands{$key} . "\n"; 68 | } 69 | print "\n"; 70 | print "\t{brightness} - 0 to 100 (percentage) - Optional\n"; 71 | print "\t{time} - 0 to 240 (seconds) - Optional\n"; 72 | print "\n"; 73 | exit(1); 74 | } 75 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/dc_dimmer_pair.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | use warnings; 20 | use Getopt::Long qw(GetOptions); 21 | 22 | my $debug; 23 | my @id; 24 | my @reverse; 25 | my $command = 1; 26 | my $duration = 30; 27 | 28 | GetOptions( 29 | 'debug' => \$debug, 30 | 'id=i' => \@id, 31 | 'reverse=i' => \@reverse, 32 | 'command=i' => \$command, 33 | 'duration=i' => \$duration, 34 | ) or usage(); 35 | 36 | usage() if (!@id or !@reverse); 37 | 38 | our ($prio, $dgnhi, $dgnlo, $srcAD) = (6, '1FE', 'DB', 96); 39 | our $binCanId = sprintf("%b0%b%b%b", hex($prio), hex($dgnhi), hex($dgnlo), hex($srcAD)); 40 | our $hexCanId = sprintf("%08X", oct("0b$binCanId")); 41 | 42 | my $hexData = ''; 43 | 44 | # In case multiple IDs were provided, loop through each pair of IDs 45 | for (my $i = 0; $i < scalar(@id); $i++) { 46 | # Stop the opposing instance 47 | $hexData = sprintf("%02XFF%02X%02X%02X00FFFF", $reverse[$i], 0, 3, 0); 48 | system("cansend can0 ${hexCanId}#$hexData") if (!$debug); 49 | print "cansend can0 ${hexCanId}#$hexData\n" if ($debug); 50 | 51 | # Engage the main instance 52 | $hexData = sprintf("%02XFF%02X%02X%02X00FFFF", $id[$i], 100*2, $command, $duration); 53 | system("cansend can0 ${hexCanId}#$hexData") if (!$debug); 54 | print "cansend can0 ${hexCanId}#$hexData\n" if ($debug); 55 | } 56 | 57 | exit; 58 | 59 | sub usage { 60 | print qq{ 61 | Arguments: 62 | 63 | --id the ID of the device to activate [required] 64 | --reverse the opposing ID of the device to deactivate [required] 65 | --command the DC_DIMMER_COMMAND_2 "on" command to use [default = 1] 66 | --duration the DC_DIMMER_COMMAND_2 duration to use [default = 30] 67 | --debug print cansend command instead of executing 68 | 69 | Multiple --id and --reverse arguments may be provided to actuate multiple devices. 70 | }; 71 | print "\n"; 72 | 73 | exit(1); 74 | } 75 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/elec_aquahot.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug=0; 22 | 23 | our %deccommands=( 24 | 0=>'Off',1=>'Low',2=>'High', 25 | ); 26 | 27 | if ( scalar(@ARGV) < 1 ) { 28 | print "ERR: Insufficient command line data provided.\n"; 29 | usage(); 30 | } 31 | 32 | if(!exists($deccommands{$ARGV[0]})) { 33 | print "ERR: Command not allowed. Please see command list below.\n"; 34 | usage(); 35 | } 36 | 37 | 38 | our ($prio,$dgnhi,$dgnlo,$srcAD,$instance,$command)=(6,'1FE','DB',96,1,$ARGV[0]); 39 | our ($hexData,$binCanId,$hexCanId)=(0,0,0); 40 | 41 | our %specials=( 42 | 1 => { 0 => [ 105, 106 ], 1=> [ 106, 105 ], 2 => [ 105, 106 ] }, 43 | ); 44 | 45 | if (exists($specials{$instance})) { 46 | $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 47 | $hexCanId=sprintf("%08X",oct("0b$binCanId")); 48 | 49 | if($command > 0) { 50 | # Turn on master Elect On/Off 51 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",107,1,255); 52 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 53 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 54 | 55 | # Turn off Anti-mode 56 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[1],3,0); 57 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 58 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 59 | 60 | # Turn on Desired Mode 61 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[0],1,255); 62 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 63 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 64 | } else { 65 | # Turn on master Elect On/Off 66 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",107,3,0); 67 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 68 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 69 | 70 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[0],3,0); 71 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 72 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 73 | 74 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[1],3,0); 75 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 76 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 77 | } 78 | } 79 | 80 | sub usage { 81 | print "Usage: \n"; 82 | print "\t$0 \n"; 83 | print "\n\t is one of:\n"; 84 | foreach my $key ( sort {$a <=> $b} keys %deccommands ) { 85 | print "\t\t".$key." = ".$deccommands{$key} . "\n"; 86 | } 87 | print "\n"; 88 | exit(1); 89 | } 90 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/lift.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug=0; 22 | 23 | our %ls=( 24 | 0 => "TV Lift", 25 | 1 => "Bed Lift", 26 | 2 => "Rear TV Lift", 27 | 3 => "2019 RED TV Lift", 28 | 4 => "Beacon Vilano TV Lift" 29 | ); 30 | 31 | our %mappings=( 32 | 0=>{ 'u'=>[43,44], 'd'=>[44,43] }, 33 | 1=>{ 'u'=>[41,42], 'd'=>[42,41] }, 34 | 2=>{ 'u'=>[45,46], 'd'=>[46,45] }, 35 | 3=>{ 'u'=>[17,21], 'd'=>[21,17] }, 36 | 4=>{ 'u'=>[34,35], 'd'=>[35,34] } 37 | ); 38 | 39 | our %ds=('u'=>1, 'd'=>1); 40 | 41 | if ( scalar(@ARGV) < 2 ) { 42 | print "ERR: Insufficient command line data provided.\n"; 43 | usage(); 44 | } 45 | 46 | if(!exists($ls{$ARGV[0]})) { 47 | print "ERR: Lift Location does not exist. Please see Location list below.\n"; 48 | usage(); 49 | } 50 | 51 | if(!exists($ds{$ARGV[1]})) { 52 | print "ERR: Direction not allowed. Please see Direction list below.\n"; 53 | usage(); 54 | } 55 | 56 | 57 | my ($loc,$dir)=($ARGV[0],$ARGV[1]); 58 | 59 | shade($loc,$dir); 60 | 61 | sub shade { 62 | my ($loc,$dir) = @_; 63 | our %ds; 64 | our %mappings; 65 | my ($prio,$dgnhi,$dgnlo,$srcAD)=(6,'1FE','DF',96); 66 | 67 | if (ref($mappings{$loc}) eq 'HASH') { 68 | $dgnlo='DB'; 69 | my $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 70 | my $hexCanId=sprintf("%08X",oct("0b$binCanId")); 71 | 72 | # Stop the 'Anti' Location 73 | my $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$mappings{$loc}{$dir}[1],3,0); 74 | system('cansend can0 '.$hexCanId."#".$hexData) if(!$debug); 75 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if($debug); 76 | 77 | # Engage the Location 78 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$mappings{$loc}{$dir}[0],5,30); 79 | system('cansend can0 '.$hexCanId."#".$hexData) if(!$debug); 80 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if($debug); 81 | } else { 82 | my $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 83 | my $hexCanId=sprintf("%08X",oct("0b$binCanId")); 84 | my $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$mappings{$loc},$ds{$dir},30); 85 | 86 | system('cansend can0 '.$hexCanId."#".$hexData) if(!$debug); 87 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if($debug); 88 | } 89 | } 90 | 91 | sub usage { 92 | print "Usage: \n"; 93 | print "\t$0 \n"; 94 | print "\n\t is required and one of:\n"; 95 | foreach my $key ( sort {$a <=> $b} keys %ls ) { 96 | print "\t\t".$key." = ".$ls{$key} . "\n"; 97 | } 98 | print "\n\t is required and one of:\n"; 99 | print "\t\td = Toggle Lift Down\n"; 100 | print "\t\tu = Toggle Lift Up\n"; 101 | print "\n"; 102 | exit(1); 103 | } 104 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/locks.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug=0; 22 | 23 | our %loads=( 24 | 0=>'All Doors', 25 | 1=>'Entry Door', 26 | 2=>'DS Cargo', 27 | 3=>'PS Cargo', 28 | # 4=>'Unknown', 29 | # 5=>'Unknown', 30 | 6=>'Cargo', 31 | ); 32 | 33 | our %loops=( 34 | 6=>[2,3], 35 | ); 36 | 37 | our %deccommands=(0=>'Unlock',1=>'Lock'); 38 | 39 | if ( scalar(@ARGV) < 2 ) { 40 | print "ERR: Insufficient command line data provided.\n"; 41 | usage(); 42 | } 43 | 44 | if(!exists($loads{$ARGV[0]})) { 45 | print "ERR: Lock does not exist. Please see lock list below.\n"; 46 | usage(); 47 | } 48 | 49 | if(!exists($deccommands{$ARGV[1]})) { 50 | print "ERR: Command not allowed. Please see command list below.\n"; 51 | usage(); 52 | } 53 | 54 | our ($prio,$dgnhi,$dgnlo,$srcAD,$instance,$command)=(6,'1FE','E4',99,$ARGV[0],$ARGV[1]); 55 | 56 | our $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 57 | our $hexCanId=sprintf("%08X",oct("0b$binCanId")); 58 | our $hexData=sprintf("%02X%02XFFFFFFFFFFFF",$instance,$command); 59 | 60 | if(exists($loops{$instance})) { 61 | for(our $i=0;our $inst=$loops{$instance}[$i];$i++) { 62 | $hexData=sprintf("%02X%02XFFFFFFFFFFFF",$inst,$command); 63 | system('cansend can0 '.$hexCanId."#".$hexData) if(!$debug); 64 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if($debug); 65 | sleep 2 if($i==0); 66 | } 67 | } else { 68 | system('cansend can0 '.$hexCanId."#".$hexData) if(!$debug); 69 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if($debug); 70 | } 71 | 72 | sub usage { 73 | print "Usage: \n"; 74 | print "\t$0 \n"; 75 | print "\n\t is one of:\n"; 76 | foreach my $key ( sort {$a <=> $b} keys %loads ) { 77 | print "\t\t".$key." = ".$loads{$key} . "\n"; 78 | } 79 | print "\n\t is one of:\n"; 80 | foreach my $key ( sort {$a <=> $b} keys %deccommands ) { 81 | print "\t\t".$key." = ".$deccommands{$key} . "\n"; 82 | } 83 | print "\n"; 84 | exit(1); 85 | } 86 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/master.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | $debug = 0; 19 | 20 | if ( scalar(@ARGV) < 2 ) { 21 | print "ERR: Insufficient command line data provided.\n"; 22 | usage(); 23 | } 24 | 25 | my $command = shift; 26 | 27 | $prio = 6; 28 | $dgnhi = '1FE'; 29 | $dgnlo = 'DB'; 30 | $srcAD = '99'; 31 | 32 | $binCanId = sprintf("%b0%b%b%b", hex($prio), hex($dgnhi), hex($dgnlo), hex($srcAD)); 33 | 34 | $duration = 0; 35 | if ($command == 0) { 36 | $cmd = 6; # Turn off and remember previous value 37 | $level = 0; # 0% 38 | } elsif ($command == 1) { 39 | $cmd = 0; # Set to level 40 | $level = 251; # Master memory value 41 | } elsif ($command == 2) { 42 | $cmd = 1; # Set to level 43 | $level = 200; # 100% 44 | $duration = 255; # Continuous 45 | } else { 46 | die('Unknown command'); 47 | } 48 | 49 | foreach my $id (@ARGV) { 50 | $hexData = sprintf("%02XFF%02X%02X%02X00FFFF", $id, $level, $cmd, $duration); 51 | $hexCanId = sprintf("%08X",oct("0b$binCanId")); 52 | 53 | system('cansend can0 '.$hexCanId."#".$hexData); 54 | print 'cansend can0 '.$hexCanId."#".$hexData."\n" if ($debug); 55 | } 56 | 57 | sub usage { 58 | print "Usage: \n"; 59 | print "\t$0 ...\n"; 60 | print "\n\t is required and one of:\n"; 61 | print "\t\t0 = Off\n"; 62 | print "\t\t1 = Restore On\n"; 63 | print "\t\t2 = All On\n"; 64 | print "\n"; 65 | print "\n"; 66 | exit(1); 67 | } 68 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/panel_lights.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug=0; 22 | 23 | our %loads=( 24 | 126 => "All", 25 | 129 => "Entry", 26 | 131 => "Passenger Slide", 27 | 132 => "Galley", 28 | 136 => "Mid Bath", 29 | 138 => "Hall", 30 | 139 => "Bedroom", 31 | 140 => "Rear Bath", 32 | ); 33 | 34 | our %loads_2018=( 35 | 126 => "All", 36 | 129 => "Entry", 37 | 131 => "Passenger Slide", 38 | 132 => "Galley", 39 | 133 => "Mid Bath", 40 | 134 => "Rear Bath", 41 | 135 => "Bedroom", 42 | 144 => "Hall Screen", 43 | 148 => "Bedroom Screen" 44 | ); 45 | 46 | if ( scalar(@ARGV) < 2 ) { 47 | print "ERR: Insufficient command line data provided.\n"; 48 | usage(); 49 | } 50 | 51 | if($ARGV[1]<0 || $ARGV[1]>100 || $ARGV[1]=~/^[^0-9]$/) { 52 | print "ERR: Command not allowed. Please see command list below.\n"; 53 | usage(); 54 | } 55 | 56 | our ($prio,$dgnhi,$dgnlo,$srcAD,$instance,$brightness)=(6,'1FE','D9',99,$ARGV[0],$ARGV[1]*2); 57 | 58 | our $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 59 | our $hexCanId=sprintf("%08X",oct("0b$binCanId")); 60 | 61 | # Set Level 62 | our $hexData=sprintf("FF%02X%02XFFFFFF00FF",$instance,$brightness); 63 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 64 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 65 | 66 | sub usage { 67 | print "Usage: \n"; 68 | print "\t$0 \n"; 69 | print "\n\t is one of:\n"; 70 | foreach my $key ( sort {$a <=> $b} keys %loads ) { 71 | print "\t\t".$key." = ".$loads{$key} . "\n"; 72 | } 73 | print "\n\tFor 2018+, is one of:\n"; 74 | foreach my $key ( sort {$a <=> $b} keys %loads_2018 ) { 75 | print "\t\t".$key." = ".$loads_2018{$key} . "\n"; 76 | } 77 | print "\n\t is brightness from 0 to 100 percent\n"; 78 | print "\n"; 79 | exit(1); 80 | } 81 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/thermostats.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # Thermostat control was added in the 2018 Phaeton and above. 19 | 20 | use strict; 21 | no strict 'refs'; 22 | use Switch 23 | 24 | our $debug = 0; 25 | 26 | our %commands = ( 27 | 'off' => 'Off', 28 | 'cool' => 'A/C On', 29 | 'heat' => 'Heat On', 30 | 'low' => 'Fan Low', 31 | 'high' => 'Fan High', 32 | 'auto' => 'Fan Auto', 33 | 'up' => 'Temp Up', 34 | 'down' => 'Temp Down', 35 | 'set' => 'Set Temp To...', 36 | ); 37 | 38 | our %thermostat_commands = ( 39 | 'off' => 'C0FFFFFFFFFFFF', 40 | 'cool' => 'C1FFFFFFFFFFFF', 41 | 'heat' => 'C2FFFFFFFFFFFF', 42 | 'low' => 'DF64FFFFFFFFFF', 43 | 'high' => 'DFC8FFFFFFFFFF', 44 | 'auto' => 'CFFFFFFFFFFFFF', 45 | 'low_fanonly' => 'D464FFFFFFFFFF', 46 | 'high_fanonly' => 'D4C8FFFFFFFFFF', 47 | 'auto_fanonly' => 'C0FFFFFFFFFFFF', 48 | 'up' => 'FFFFFFFFFAFFFF', 49 | 'down' => 'FFFFFFFFF9FFFF', 50 | ); 51 | 52 | if (scalar(@ARGV) < 2) { 53 | print "ERROR: Too few command line arguments provided.\n"; 54 | usage(); 55 | } 56 | 57 | our $instance = $ARGV[0]; 58 | if ($instance < 0 or $instance > 6) { 59 | print "ERROR: Invalid zone specified.\n"; 60 | usage(); 61 | } 62 | 63 | our $command = $ARGV[1]; 64 | if (!exists($commands{$command})) { 65 | print "ERROR: Invalid command specified.\n"; 66 | usage(); 67 | } 68 | 69 | # When controlling the fans, slightly different commands need to be sent 70 | # depending on whether the "mode" is already set to something like A/C or 71 | # not. 72 | if (scalar(@ARGV) >= 3) { 73 | our $current_mode = $ARGV[2]; 74 | if (grep(/^$command$/, ('low', 'high', 'auto')) and grep(/^$current_mode$/, ('off', 'fan'))) { 75 | $command .= '_fanonly'; 76 | } 77 | } 78 | 79 | our ($prio, $dgnhi, $dgnlo, $srcAD) = (6, '1FE', 'F9', 99); 80 | 81 | our $binCanId = sprintf("%b0%b%b%b", hex($prio), hex($dgnhi), hex($dgnlo), hex($srcAD)); 82 | our $hexCanId = sprintf("%08X", oct("0b$binCanId")); 83 | 84 | our $hexData; 85 | 86 | # Send THERMOSTAT_COMMAND. 87 | if ($thermostat_commands{$command}) { 88 | $hexData = sprintf("%02X%s", $instance, $thermostat_commands{$command}); 89 | cansend($hexCanId, $hexData); 90 | } 91 | 92 | # Send setpoint commands 93 | if ($command eq 'set') { 94 | if (exists($ARGV[2])) { 95 | # Spyder uses 75.5, 76.5, 77.5, etc. so we need to add 0.5 96 | my $tempRVC = tempF2hex($ARGV[2] + 0.5); 97 | $hexData = sprintf("%02XFFFF%s%sFF", $instance, $tempRVC, $tempRVC); 98 | cansend($hexCanId, $hexData); 99 | 100 | # Also set the furnace setpoints, if available. So far, only zones 0 and 2 (RED) 101 | # or 2 and 4 (Phaeton+) have furnaces available. 102 | if ($instance % 2 == 0) { 103 | $hexData = sprintf("%02XFFFF%s%sFF", $instance + 3, $tempRVC, $tempRVC); 104 | cansend($hexCanId, $hexData); 105 | } 106 | } 107 | } 108 | 109 | exit; 110 | 111 | # Add 0.999 to perform a ceil() function on the resulting value to prevent 112 | # rounding errors. E.g. 71 F normally translates to 9429.33333 or 24D5 hex. 113 | # However, 24D5 translates back to 70.98125 F, causing the Spyder screen to 114 | # display 70 instead of 71. 115 | sub tempF2hex { 116 | my ($data)=@_; 117 | my $hexchars=sprintf("%04X",(((($data-32)*5/9)+273)/0.03125)+0.999); 118 | my @binarray= $hexchars =~ m/(..?)/sg; 119 | return $binarray[1].$binarray[0]; 120 | } 121 | 122 | 123 | sub cansend { 124 | our $debug; 125 | my ($id, $data) = @_; 126 | system('cansend can0 ' . $id . "#" . $data) if (!$debug); 127 | print 'cansend can0 '. $id . "#" . $data . "\n" if ($debug); 128 | } 129 | 130 | 131 | sub usage { 132 | print "Usage: \n"; 133 | print "\t$0 \n"; 134 | print "\n\tZones:\n"; 135 | print "\t\tHighline coaches: 2=Front 3=Mid 4=Rear 5=Front Furnace 6=Rear Furnace\n"; 136 | print "\t\tLowline coaches: 0=Front 1=Mid 2=Rear 3=Front Furnace 4=Rear Furnace\n"; 137 | print "\n\tCommands:\n"; 138 | foreach my $key ( keys %commands ) { 139 | print "\t\t" . $key . " = " . $commands{$key} . "\n"; 140 | } 141 | print "\n"; 142 | exit(1); 143 | } 144 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/vent_fan.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | use strict; 19 | no strict 'refs'; 20 | 21 | our $debug=0; 22 | 23 | our @versions=('2014-hi','2014-lo','2018-hi', '2018-lo'); 24 | 25 | our %loads=( 26 | 1 => "Galley",2 => "Mid Bath",3 => "Rear Bath", 27 | ); 28 | 29 | our %deccommands=( 30 | 2=>'On',3=>'Off',69=>'Up',133=>'Down' 31 | ); 32 | 33 | if ( scalar(@ARGV) < 3 ) { 34 | print "ERR: Insufficient command line data provided.\n"; 35 | usage(); 36 | } 37 | 38 | if(!exists($versions[$ARGV[0]])) { 39 | print "ERR: Version not present. Please see Version list below.\n"; 40 | usage(); 41 | } 42 | 43 | if(!exists($loads{$ARGV[1]})) { 44 | print "ERR: Load does not exist. Please see load list below.\n"; 45 | usage(); 46 | } 47 | 48 | if(!exists($deccommands{$ARGV[2]})) { 49 | print "ERR: Command not allowed. Please see command list below.\n"; 50 | usage(); 51 | } 52 | 53 | our ($prio,$dgnhi,$dgnlo,$srcAD,$ver,$instance,$command)=(6,'1FE','DB',96,$ARGV[0],$ARGV[1],$ARGV[2]); 54 | 55 | our %specials = (); 56 | our @indicator = (); 57 | 58 | if ($ver == 1) { 59 | # Low Line 60 | %specials=( 61 | 1=>{2 => 27, 3 => 27, 69 => [ 25, 26], 133=>[ 26, 25]}, 62 | 2=>{2 => 30, 3 => 30, 69 => [ 28, 29], 133=>[ 29, 28]}, 63 | 3=>{2 => 32, 3 => 32, 69 => [ 33, 34], 133=>[ 34, 33]}, 64 | ); 65 | @indicator = ( 0, 8, 44, 114 ); 66 | } elsif ($ver == 3) { 67 | # Low Line 2018+ 68 | %specials=( 69 | 1=>{2 => 23, 3 => 23, 69 => [ 21, 22], 133=>[ 22, 21]}, 70 | 2=>{2 => 19, 3 => 19, 69 => [ 17, 18], 133=>[ 18, 17]}, 71 | 3=>{2 => 15, 3 => 15, 69 => [ 13, 14], 133=>[ 14, 13]}, 72 | ); 73 | } else { 74 | # High Line all years 75 | %specials=( 76 | 1=>{2 => 25, 3 => 25, 69 => [ 26, 27], 133=>[ 27, 26]}, 77 | 2=>{2 => 29, 3 => 29, 69 => [ 30, 31], 133=>[ 31, 30]}, 78 | 3=>{2 => 32, 3 => 32, 69 => [ 33, 34], 133=>[ 34, 33]}, 79 | ); 80 | @indicator = ( 0, 39, 54, 114 ); 81 | } 82 | 83 | our %status = ( 69=>3, 133=>2 ); 84 | our $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 85 | our $hexCanId=sprintf("%08X",oct("0b$binCanId")); 86 | our $hexData; 87 | 88 | if (exists($specials{$instance})) { 89 | 90 | if($command > 3) { 91 | # Stop the 'Anti' instance 92 | $hexData=sprintf("%02XFF00%02X%02X00FFFF",$specials{$instance}{$command}[1],3,0); 93 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 94 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 95 | 96 | # Engage the instance 97 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command}[0],1,20); 98 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 99 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 100 | 101 | # Set the indicator on pre-2018 coaches 102 | if ($ver < 2) { 103 | $dgnlo='D9'; 104 | $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 105 | $hexCanId=sprintf("%08X",oct("0b$binCanId")); 106 | $hexData=sprintf("%02Xff00ffffff%02Xff",$indicator[$instance],$status{$command}); 107 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 108 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 109 | } 110 | 111 | } else { 112 | $hexData=sprintf("%02XFFC8%02X%02X00FFFF",$specials{$instance}{$command},$command,255); 113 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 114 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 115 | } 116 | } 117 | 118 | sub usage { 119 | print "Usage: \n"; 120 | 121 | print "\t$0 \n"; 122 | print "\n\t is required and one of the following based on model year:\n"; 123 | for(my $i=0;my $ver=$versions[$i];$i++) { 124 | print "\t\t".$i." = ".$ver . "\n"; 125 | } 126 | 127 | print "\n\t is required and one of:\n"; 128 | foreach my $key ( sort {$a <=> $b} keys %loads ) { 129 | print "\t\t".$key." = ".$loads{$key} . "\n"; 130 | } 131 | 132 | print "\n\t is required and one of:\n"; 133 | foreach my $key ( sort {$a <=> $b} keys %deccommands ) { 134 | print "\t\t".$key." = ".$deccommands{$key} . "\n"; 135 | } 136 | 137 | print "\n"; 138 | exit(1); 139 | } 140 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/vent_fan_new.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # Control vent fan on/off status 19 | 20 | use strict; 21 | no strict 'refs'; 22 | 23 | our $debug = 0; 24 | 25 | our %commands = (2 => 'on', 3 => 'off'); 26 | 27 | if (scalar(@ARGV) < 2) { 28 | print "ERR: Insufficient command line data provided.\n"; 29 | usage(); 30 | } 31 | 32 | our $command = $ARGV[0]; 33 | if (!exists($commands{$command})) { 34 | print "ERR: Command not allowed. Please see command list below.\n"; 35 | usage(); 36 | } 37 | 38 | our $load = $ARGV[1]; 39 | 40 | our ($prio, $dgnhi, $dgnlo, $srcAD) = (6, '1FE', 'DB', 96); 41 | our $binCanId = sprintf("%b0%b%b%b", hex($prio), hex($dgnhi), hex($dgnlo), hex($srcAD)); 42 | our $hexCanId = sprintf("%08X", oct("0b$binCanId")); 43 | our $hexData; 44 | 45 | $hexData = sprintf("%02XFFC8%02X%02X00FFFF", $load, $command, 255); 46 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 47 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 48 | 49 | sub usage { 50 | print "Usage: \n"; 51 | 52 | print "\t$0 \n"; 53 | 54 | print "\n\t is required and one of:\n"; 55 | foreach my $key ( keys %commands ) { 56 | print "\t\t".$key." = ".$commands{$key}."\n"; 57 | } 58 | 59 | print "\n\t is required and is a valid Spyder load ID\n"; 60 | 61 | print "\n"; 62 | exit(1); 63 | } 64 | -------------------------------------------------------------------------------- /roles/coachproxy/files/rv-c/vent_lid.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Copyright (C) 2019 Wandertech LLC 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # Control vent lid up/down status 19 | 20 | use strict; 21 | no strict 'refs'; 22 | 23 | our $debug = 0; 24 | 25 | our %commands = (69 => 'up', 133 => 'down'); 26 | 27 | if (scalar(@ARGV) < 3) { 28 | print "ERR: Insufficient command line data provided.\n"; 29 | usage(); 30 | } 31 | 32 | our $command = $ARGV[0]; 33 | if (!exists($commands{$command})) { 34 | print "ERR: Command not allowed. Please see command list below.\n"; 35 | usage(); 36 | } 37 | 38 | our $load_up = $ARGV[1]; 39 | our $load_down = $ARGV[2]; 40 | our $indicator = $ARGV[3]; 41 | 42 | our @instances = ($load_up, $load_down); 43 | @instances = reverse @instances if ($command == 133); 44 | 45 | our ($prio, $dgnhi, $dgnlo, $srcAD) = (6, '1FE', 'DB', 96); 46 | our $binCanId=sprintf("%b0%b%b%b",hex($prio),hex($dgnhi),hex($dgnlo),hex($srcAD)); 47 | our $hexCanId=sprintf("%08X",oct("0b$binCanId")); 48 | our $hexData; 49 | 50 | # Stop the 'Anti' instance 51 | $hexData = sprintf("%02XFF00%02X%02X00FFFF", $instances[1], 3, 0); 52 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 53 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 54 | 55 | # Engage the instance 56 | $hexData = sprintf("%02XFFC8%02X%02X00FFFF", $instances[0], 1, 20); 57 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 58 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 59 | 60 | # Set the indicator if present (pre-2018 coaches) 61 | if ($indicator) { 62 | our %status = (69 => 3, 133 => 2); 63 | $dgnlo = 'D9'; 64 | $binCanId = sprintf("%b0%b%b%b", hex($prio), hex($dgnhi), hex($dgnlo), hex($srcAD)); 65 | $hexCanId = sprintf("%08X", oct("0b$binCanId")); 66 | $hexData = sprintf("%02Xff00ffffff%02Xff", $indicator, $status{$command}); 67 | system('cansend can0 '.$hexCanId."#".$hexData) if (!$debug); 68 | print 'cansend can0 '.$hexCanId."#".$hexData . "\n" if($debug); 69 | } 70 | 71 | sub usage { 72 | print "Usage: \n"; 73 | 74 | print "\t$0 \n"; 75 | 76 | print "\n\t is required and one of:\n"; 77 | foreach my $key ( keys %commands ) { 78 | print "\t\t".$key." = ".$commands{$key}."\n"; 79 | } 80 | 81 | print "\n\t is required and is a valid Spyder load ID\n"; 82 | print "\n\t is required and is a valid Spyder load ID\n"; 83 | print "\n\t is optional and is a valid Spyder load ID\n"; 84 | 85 | print "\n"; 86 | exit(1); 87 | } 88 | -------------------------------------------------------------------------------- /roles/coachproxy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Install all required files into /coachproxy 2 | 3 | --- 4 | 5 | - name: Create /coachproxy directory 6 | file: 7 | path: /coachproxy 8 | state: directory 9 | become: true 10 | 11 | - name: Ensure /coachproxy is owned by pi:pi 12 | file: 13 | path: /coachproxy 14 | owner: pi 15 | group: pi 16 | mode: 0755 17 | become: true 18 | 19 | # 'delete: false' is used because other files (e.g. nodered modules) 20 | # will be installed later and shouldn't be clobbered on subsequent 21 | # Ansible runs. 22 | 23 | - name: Install /coachproxy files 24 | synchronize: 25 | src: "files/" 26 | dest: /coachproxy/ 27 | recursive: true 28 | delete: false 29 | links: true 30 | perms: true 31 | times: true 32 | owner: false 33 | group: false 34 | 35 | # node-red configs 36 | 37 | - name: Copy initial node-red flows file to main flows file 38 | copy: 39 | src: /coachproxy/node-red/flows_coachproxy-base.json 40 | dest: /coachproxy/node-red/flows_coachproxy.json 41 | remote_src: true 42 | 43 | - name: Copy initial database file to main database file 44 | copy: 45 | src: /coachproxy/node-red/coachproxy.sqlite-base 46 | dest: /coachproxy/node-red/coachproxy.sqlite 47 | remote_src: true 48 | 49 | # crontab 50 | 51 | - name: Stop cron (but leave it enabled) while Ansible is setting things up. 52 | systemd: 53 | name: cron 54 | state: stopped 55 | become: true 56 | 57 | - name: Ensure crontab is owned by root, not pi 58 | file: 59 | path: /coachproxy/etc/cron.d/coachproxy 60 | owner: root 61 | become: true 62 | 63 | - name: Link custom crontab 64 | file: 65 | src: /coachproxy/etc/cron.d/coachproxy 66 | dest: /etc/cron.d/coachproxy 67 | state: link 68 | become: true 69 | 70 | # /etc/hosts 71 | 72 | - name: Install custom /etc/hosts 73 | copy: 74 | src: /coachproxy/etc/hosts 75 | dest: /etc/hosts 76 | remote_src: true 77 | become: true 78 | 79 | # rc.local 80 | 81 | - name: Call the coachproxy rc.local file from /etc/rc.local 82 | lineinfile: 83 | path: /etc/rc.local 84 | line: /coachproxy/etc/rc.local.coachproxy 85 | insertbefore: '^exit 0' 86 | become: true 87 | 88 | # mosquitto 89 | 90 | - name: Link mosquitto configuration 91 | file: 92 | src: /coachproxy/etc/mosquitto/mosquitto.conf 93 | dest: /etc/mosquitto/mosquitto.conf 94 | state: link 95 | force: yes 96 | become: true 97 | 98 | # habridge / Alexa integration 99 | 100 | - name: Install ha-bridge systemd file 101 | copy: 102 | src: /coachproxy/etc/systemd/system/habridge.service 103 | dest: /etc/systemd/system/habridge.service 104 | remote_src: true 105 | become: true 106 | 107 | - name: Install default habridge.config file 108 | copy: 109 | src: /coachproxy/ha-bridge/habridge.config.template 110 | dest: /coachproxy/ha-bridge/habridge.config 111 | remote_src: true 112 | 113 | - name: Enable habridge Alexa service 114 | systemd: 115 | name: habridge 116 | enabled: true 117 | become: true 118 | 119 | # nginx 120 | 121 | - name: Disable default nginx config 122 | file: 123 | path: /etc/nginx/sites-enabled/default 124 | state: absent 125 | become: true 126 | 127 | - name: Link custom nginx config 128 | file: 129 | src: /coachproxy/etc/nginx/sites-available/10-CoachProxy.conf 130 | dest: /etc/nginx/sites-enabled/10-CoachProxy.conf 131 | state: link 132 | become: true 133 | 134 | # fake-hwclock 135 | 136 | - name: Link fake-hwclock to /coachproxy/etc 137 | file: 138 | src: /coachproxy/etc/fake-hwclock.data 139 | dest: /etc/fake-hwclock.data 140 | state: link 141 | force: yes 142 | follow: no 143 | become: true 144 | 145 | - name: Restart fake-hwclock service 146 | systemd: 147 | name: fake-hwclock 148 | state: restarted 149 | become: true 150 | 151 | # default vimrc and bash aliases 152 | 153 | - name: Link custom vimrc 154 | file: 155 | src: /coachproxy/etc/vim/vimrc.local 156 | dest: /etc/vim/vimrc.local 157 | state: link 158 | become: true 159 | 160 | - name: Link custom bash profile 161 | file: 162 | src: /coachproxy/etc/profile.d/coachproxy.sh 163 | dest: /etc/profile.d/coachproxy.sh 164 | state: link 165 | become: true 166 | 167 | # pi user home directory 168 | 169 | - name: Check if pi home directory has already been copied 170 | stat: 171 | path: /coachproxy/home/pi/.ssh 172 | register: home_pi_moved 173 | 174 | - name: Copy old /home/pi directory to /coachproxy/home/pi 175 | command: cp -r /home/pi /coachproxy/home/ 176 | when: home_pi_moved.stat.exists == false 177 | 178 | - name: Change pi user home directory in /etc/passwd 179 | lineinfile: 180 | path: /etc/passwd 181 | regexp: '^.*/home/pi:.*$' 182 | line: 'pi:x:1000:1000:,,,:/coachproxy/home/pi:/bin/bash' 183 | become: true 184 | 185 | - name: Delete /home/pi directory 186 | file: 187 | path: /home/pi 188 | state: absent 189 | become: true 190 | 191 | - name: Replace /home/pi with a symlink to the new home 192 | file: 193 | src: /coachproxy/home/pi 194 | dest: /home/pi 195 | state: link 196 | become: true 197 | 198 | # install ngrok for remote access 199 | 200 | - name: Check if ngrok has already been installed 201 | stat: 202 | path: /usr/local/bin/ngrok 203 | register: ngrok 204 | 205 | - name: Download ngrok binary 206 | unarchive: 207 | src: https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip 208 | dest: /usr/local/bin 209 | mode: 0755 210 | remote_src: yes 211 | when: ngrok.stat.exists == false 212 | become: true 213 | -------------------------------------------------------------------------------- /roles/locale-us/files/keyboard: -------------------------------------------------------------------------------- 1 | # KEYBOARD CONFIGURATION FILE 2 | 3 | # Consult the keyboard(5) manual page. 4 | 5 | XKBMODEL="pc105" 6 | XKBLAYOUT="us" 7 | XKBVARIANT="" 8 | XKBOPTIONS="" 9 | 10 | BACKSPACE="guess" 11 | -------------------------------------------------------------------------------- /roles/locale-us/files/locale: -------------------------------------------------------------------------------- 1 | LANG=en_US.UTF-8 2 | LC_ALL=en_US.UTF-8 3 | LANGUAGE=en_US.UTF-8 4 | -------------------------------------------------------------------------------- /roles/locale-us/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Change locale info from GB to US 2 | 3 | --- 4 | 5 | - name: Enable en_US locale in /etc/locale.gen 6 | lineinfile: 7 | path: /etc/locale.gen 8 | line: 'en_US.UTF-8 UTF-8' 9 | become: true 10 | 11 | - name: Disable en_GB locale in /etc/locale.gen 12 | lineinfile: 13 | path: /etc/locale.gen 14 | line: 'en_GB.UTF-8 UTF-8' 15 | state: absent 16 | become: true 17 | 18 | - name: Get a list of which locales have been generated 19 | command: locale -a 20 | register: locale_result 21 | changed_when: false 22 | 23 | - name: Generate US locale 24 | command: locale-gen 25 | become: true 26 | when: locale_result.stdout.find('en_US.utf8') == -1 27 | 28 | - name: Ensure locale is set to U.S. 29 | copy: 30 | src: locale 31 | dest: /etc/default/locale 32 | become: true 33 | 34 | - name: Ensure keyboard is set to U.S. 35 | copy: 36 | src: keyboard 37 | dest: /etc/default/keyboard 38 | become: true 39 | 40 | - name: Delete non-en locale files to save space 41 | shell: rm -rf /usr/share/locale/[a-d,f-z]* 42 | args: 43 | warn: no 44 | removes: /usr/share/locale/zu 45 | become: true 46 | -------------------------------------------------------------------------------- /roles/misc/files/CoachProxy_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/misc/files/CoachProxy_logo.jpg -------------------------------------------------------------------------------- /roles/misc/files/icon120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/misc/files/icon120x120.png -------------------------------------------------------------------------------- /roles/misc/files/icon192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/misc/files/icon192x192.png -------------------------------------------------------------------------------- /roles/misc/files/icon64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxkidd/coachproxy-os/4a78b98fa6dab6365cc16595e3c2e03e8f3ef23e/roles/misc/files/icon64x64.png -------------------------------------------------------------------------------- /roles/misc/tasks/main.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | - name: Add canbus driver info to boot config 5 | blockinfile: 6 | path: /boot/config.txt 7 | insertafter: EOF 8 | block: | 9 | dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25 10 | become: true 11 | 12 | - name: Add custom browser/home screen icons and other assets 13 | copy: 14 | src: "{{ item }}" 15 | dest: /coachproxy/node-red/node_modules/node-red-dashboard/dist/ 16 | with_items: 17 | - icon64x64.png 18 | - icon120x120.png 19 | - icon192x192.png 20 | - CoachProxy_logo.jpg 21 | 22 | - name: Delete existing SSH host keys 23 | shell: rm /etc/ssh/ssh_host_* 24 | args: 25 | removes: /etc/ssh/ssh_host_key 26 | become: true 27 | -------------------------------------------------------------------------------- /roles/networking/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Set up networking using systemd-networkd 2 | # 3 | # See: 4 | # https://raspberrypi.stackexchange.com/a/88631/79608 5 | # https://raspberrypi.stackexchange.com/a/78788/79866 6 | # https://raspberrypi.stackexchange.com/a/88234/79608 7 | 8 | --- 9 | 10 | - name: Disable classic networking services 11 | systemd: 12 | name: "{{ item }}" 13 | masked: true 14 | become: true 15 | with_items: 16 | - networking 17 | - dhcpcd 18 | 19 | - name: Enable systemd networking services 20 | systemd: 21 | name: "{{ item }}" 22 | enabled: true 23 | become: true 24 | with_items: 25 | - systemd-networkd 26 | - systemd-resolved 27 | 28 | - name: Disable IPv6 networking 29 | lineinfile: 30 | path: /etc/sysctl.conf 31 | regexp: "{{ item.regexp }}" 32 | line: "{{ item.line }}" 33 | insertafter: EOF 34 | with_items: 35 | - { regexp: '^net.ipv6.conf.all.disable_ipv6', line: 'net.ipv6.conf.all.disable_ipv6 = 1' } 36 | - { regexp: '^net.ipv6.conf.default.disable_ipv6', line: 'net.ipv6.conf.default.disable_ipv6 = 1' } 37 | - { regexp: '^net.ipv6.conf.lo.disable_ipv6', line: 'net.ipv6.conf.lo.disable_ipv6 = 1' } 38 | become: true 39 | 40 | - name: Symlink /etc/resolv.conf to systemd-resolved version 41 | file: 42 | src: /run/systemd/resolve/resolv.conf 43 | dest: /etc/resolv.conf 44 | state: link 45 | force: true 46 | follow: no 47 | become: true 48 | 49 | - name: Symlink systemd network configuration files in /coachproxy 50 | file: 51 | src: /coachproxy/etc/systemd/network/{{ item }} 52 | dest: /etc/systemd/network/{{ item }} 53 | state: link 54 | force: true 55 | become: true 56 | with_items: 57 | - 04-eth0.network 58 | - 12-wlan0.network 59 | 60 | - name: Create symlink for wlan0 WiFi configuration 61 | file: 62 | src: /etc/wpa_supplicant/wpa_supplicant.conf 63 | dest: /etc/wpa_supplicant/wpa_supplicant-wlan0.conf 64 | state: link 65 | force: true 66 | become: true 67 | 68 | - name: Enable the new WiFi supplicant 69 | systemd: 70 | name: "wpa_supplicant@wlan0.service" 71 | enabled: true 72 | become: true 73 | -------------------------------------------------------------------------------- /roles/node-red/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Install NodeJS and Node-RED for Raspberry Pi 3 2 | 3 | --- 4 | 5 | - name: Link default node-red directory to /coachproxy 6 | file: 7 | src: /coachproxy/node-red 8 | dest: /coachproxy/home/pi/.node-red 9 | state: link 10 | 11 | - name: Check if Node-RED has already been installed 12 | stat: 13 | path: /usr/bin/node-red-start 14 | register: node_red_start 15 | 16 | - name: Download update-nodejs-and-nodered script 17 | get_url: 18 | url: https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered 19 | dest: /tmp/update-nodejs-and-nodered 20 | mode: 0777 21 | when: node_red_start.stat.exists == false 22 | 23 | - name: Upgrade nodejs and nodered 24 | shell: "yes | /tmp/update-nodejs-and-nodered" 25 | when: node_red_start.stat.exists == false 26 | 27 | - name: Ensure required node-red nodes are installed 28 | npm: 29 | name: "{{ item }}" 30 | path: /coachproxy/node-red 31 | with_items: 32 | - node-red-dashboard 33 | - node-red-node-pushover 34 | - node-red-node-sqlite 35 | - node-red-contrib-file-function-ext 36 | - node-red-contrib-throttle 37 | - node-red-contrib-timeout 38 | - node-red-contrib-wifiscan 39 | - node-red-contrib-simple-gate 40 | - node-red-contrib-dsm 41 | - node-red-contrib-simpletime 42 | - node-red-contrib-schedex 43 | - node-red-contrib-simple-sendmail 44 | 45 | - name: Replace boilerplate Node-RED text in blank dashboard page 46 | replace: 47 | path: /coachproxy/node-red/node_modules/node-red-dashboard/dist/index.html 48 | regexp: 'Welcome to the Node-RED Dashboard' 49 | replace: 'Welcome to CoachProxyOS' 50 | 51 | - name: Replace more boilerplate Node-RED text in blank dashboard page 52 | replace: 53 | path: /coachproxy/node-red/node_modules/node-red-dashboard/dist/index.html 54 | regexp: 'Please add some UI nodes to your flow and redeploy.' 55 | replace: 'Please wait a moment while CoachProxyOS loads...' 56 | 57 | - name: Copy extra CSS file into node-red-dashboard html directory 58 | copy: 59 | src: font-awesome-animation.min.css 60 | dest: /coachproxy/node-red/node_modules/node-red-dashboard/dist/css/font-awesome-animation.min.css 61 | mode: 0644 62 | 63 | - name: enable node-red in systemctl 64 | systemd: 65 | name: nodered 66 | enabled: true 67 | daemon_reload: true 68 | become: true 69 | --------------------------------------------------------------------------------