homey-433
v1.2.2
Published
433 tools for Homey apps
Downloads
11
Readme
Homey 433 mhz app generator
This module generates driver files for Homey using a custom config file. The goal of this module is to implement generic 433 features and generate Homey drivers using minimal device specific code.
Notice
This generator generates files in the /drivers
, /drivers/lib
and /assets/433_generator
folder. All previous files in these folders will be overwritten. Also, this generator overwrites drivers
and flows
in the app.json
config. These actions cannot be undone. Use with caution!
Usage
To install this tool run
npm install -g homey-433
To use this tool you need to run
homey433 generate [configDir] [projectDir]
The configDir is relative to the projectDir and is 433_generator
by default. The projectDir is defaulted to the current directory in the command line. To generate and run the project in one command you can call the following command.
homey433 generate && athom project --run
The config file
The config file consists of 3 main options. Views, Deviceclasses and devices. These options inherit the defaultConfig settings (included in /lib/defaultConfig.js
). A sample configuration is given below.
module.exports = {
views: {
start: {
template: './pair/start.html', // Template file location
options: {
title: {
required: true, // Asserts that the option is set
},
body: {
default: 'pair.info.body', // Default value if not set by child
},
next: true, // If the next button should be visible in pair wizard
previous: false, // if previous button should be visible
},
},
},
deviceClasses: {
eurodomest: {
signal: { // The signal definition of the device
sof: [], // Start of frame
eof: [295], // End of frame
words: [
[295, 885], // 0
[885, 295], // 1
],
interval: 9565, // Time between repititions
repetitions: 20,
sensitivity: 0.7,
minimalLength: 24,
maximalLength: 24,
},
},
myRemote: {
extends: 'remote', // Extends the remote deviceclass from defaultConfig
driver: './drivers/remote.js', // Path to the driver file to use
class: 'other', // The class of the device
capabilities: [], // The capabilities of the device
pair: {
viewOrder: ['start', 'imitate', 'test_remote', 'done'],
viewOptions: {
start: {
title: 'pair.start',
next: 'imitate', // If nessecary a specific view can be given
}
}
}
},
},
devices: {
remote_1: { // A device for which a driver will be generated
extends: ['myRemote', 'eurodomest'], // Extends multiple deviceclasses
name: 'remote.1.name', // Name is localized using the locales/*lang_id*.json
icon: './assets/remote1/remote.svg', // Location of the icon of this device
pair: {
viewOptions: {
start: {
body: 'Connect remote 1!',
}
imitate: {
title: './assets/remote1/remote_pair.svg',
},
imitate: {
svg: './assets/remote1/remote_pair.svg',
},
test_remote: {
svg: './assets/remote1/remote.svg',
},
},
},
},
remote_2: {
extends: ['myRemote', 'eurodomest'],
name: 'remote.2.name',
icon: './assets/remote2/remote.svg',
pair: {
viewOptions: {
start: {
body: 'pair.start.remote1',
}
imitate: {
svg: './assets/remote2/remote_pair.svg',
// Prepend to the template, can be html, syles or script
// The value can be a path or a raw string
// Notice that when specifying a path the file has to
// be located in a frontend accessable path like /assets
prepend: [{ styles: ['../assets/AMST-606/animate.css'] }],
},
test_remote: {
svg: './assets/remote2/remote.svg',
// Append to the template, you can append multiple items
append: [{ styles: ['../assets/AMST-606/animate.css'] }],
},
},
},
},
},
};
The config file is structured as follows. Devices need configuration for driver
, signal
, name
, icon
, images
, pair
, capabilities
, triggers
, conditions
and actions
. The configuration of many devices overlaps a device can extend it's config from one or more deviceClasses
. A deviceclass itself can also extend from other deviceclasses. Because many devices require the same views in the pair wizard views
are, like deviceClasses, extendable building blocks. A view has a template
and viewOptions
configuration. In the viewOptions
you can define which options the view needs from the device that is using this view or set a default value. The next and previous options are reserved to generate the navigation
configuration in the resulting app.json
which will add previous and next buttons in the pair wizard.
Triggers, Conditions and Actions
It is also possible to automatically generate the triggers
, conditions
and actions
in the app.json. If these are defined as device options the generator will automatically append the following argument.
{
"name": "device",
"type": "device",
"filter": "driver_id=${driver_id}"
}
Notice that, because the generator overwrites the flow
config in app.json
it is not possible to include other custom flows. Therefore it is possible to define your triggers
, conditions
and actions
in the root of the config file. The generator will append them to the generated flow
config. The config options are the same as the normal flow configuration in app.json
except that the localization object will be generated automatically by the generator. The default driver implements a received
trigger and a send
action by default. If a device has these configured the driver will try to genericly implement them. For instance, a device sends a signal containing the following data.
{
address:'0101110101',
group: '0'
unit: '110'
state: '1'
}
You can configure the recieved
trigger to check all the values when a signal is received by matching the name of the argument with its value to the value in the data object. If all conditions are met the trigger is fired. For the send
, when triggered, the arguments will be merged with the default data packet (the first packet saved when pairing) and send by Homey. An example of such a config is given below.
triggers: [
{
id: 'received',
title: 'trigger.received.title.remote',
args: [
{
name: 'unit',
type: 'dropdown',
values: [
{ id: '111', label: 'remote.button_1' },
{ id: '110', label: 'remote.button_2' },
],
},
{
name: 'state',
type: 'dropdown',
values: [
{ id: '1', label: 'remote.on' },
{ id: '0', label: 'remote.off' },
],
},
],
},
],
actions: [
{
id: 'send',
title: 'action.send.title.remote',
args: [
{
name: 'unit',
type: 'dropdown',
values: [
{ id: '111', label: 'remote.button_1' },
{ id: '110', label: 'remote.button_2' },
],
},
{
name: 'state',
type: 'dropdown',
values: [
{ id: '1', label: 'remote.on' },
{ id: '0', label: 'remote.off' },
],
},
],
},
],
Drivers
A driver should rely on the default driver and only implement the features that are custom to a device. To use a driver you will have to implement 3 functions, generateData
, payloadToData
and dataToPayload
. payloadToData
is called when a payload is received. This function should convert the payload array to a usable javascript object witch at least should contain a id. The id is the unique identifier for the device. For example you can only add 1 remote per address so this will be it's id. But a doorbell is defined by its address, group, channel and unit (or whatever you separate the payload in). payloadToData
does the opposite. This function should convert the given data object to an bit array that will be send by Homey. generateData
is used to connect devices without having a transmitter. This function returns a random data object which can be used to control a device. An example of a driver is given below.
'use strict';
const DefaultDriver = require('../../../drivers/lib/driver');
const SignalManager = Homey.wireless('433').Signal;
module.exports = class MyDriver extends DefaultDriver {
generateData() {
const data = {
address: Math.random().toString(2).substr(2, 26),
group: 0,
channel: Math.random().toString(2).substr(2, 2),
unit: Math.random().toString(2).substr(2, 2),
state: 1,
};
data.id = `${data.address}:${data.group}:${data.channel}:${data.unit}`;
return data;
}
payloadToData(payload) { // Convert received data to usable variables
if (payload.length === 32) {
const data = {
address: SignalManager.bitArrayToString(payload.slice(0, 26)),
group: payload.slice(26, 27)[0],
channel: SignalManager.bitArrayToString(payload.slice(28, 30)),
unit: SignalManager.bitArrayToString(payload.slice(30, 32)),
state: payload.slice(27, 28)[0],
};
data.id = `${data.address}:${data.group}:${data.channel}:${data.unit}`;
return data;
}
return null; // returning null will discard the signal
}
dataToPayload(data) {
const address = SignalManager.bitStringToBitArray(data.address);
const channel = SignalManager.bitStringToBitArray(data.channel);
const unit = SignalManager.bitStringToBitArray(data.unit);
return address.concat(data.group, data.state, channel, unit);
}
};
If a device needs to override more functionality of the default driver it can do so. It is encouraged to make use of the events that are emitted in the DefaultDriver. You can for example listen to state changes with this.on('newState')
or incoming frames with this.on('received')
. To change the exports object of the driver you can override the getExports
function. It is good practice to, if not explicitly done, call the function on the super class if you override it. An example of a Socket driver which implements the onoff
capability is given below.
'use strict';
const Promax = require('./promax');
module.exports = class Socket extends Promax {
constructor(config) {
super(config); // Important: when using the constructor pass config to super
this.on('newFrame', this.updateState.bind(this));
this.on('newState', this.updateRealtime.bind(this));
}
updateState(device, frame) {
this.setState(device, Object.assign({}, this.getState(device), frame));
}
updateRealtime(device, state, oldState) {
if (String(state.state) !== String(oldState.state)) {
this.realtime(device, 'onoff', String(state.state) === '1');
}
}
getExports() {
const exports = super.getExports();
exports.capabilities = exports.capabilities || {};
exports.capabilities.onoff = {
get: (device, callback) => callback(null, Boolean(this.getState(device).state)),
set: (device, state, callback) => this.send(device, { state: String(state) === '1' ? 1 : 0 }, callback),
};
return exports;
}
};
Svg
Svg's included in the standard templates will be highlighted by the svghighlighter located in /assets/433_generator/js/svghighlighter.js
in your project. To configure how to highlight your svg you need to set custom xml attributes. The highlighter will match a received data frame to the custom classes set in the svg. Notice that all custom classes need to be defined in the <svg>
tag like xmlns:customAttribute="nonEmptyString"
. By default you can use the show
, hide
and pulse
class. To create custom attributes to which the svghighlighter will listen you will need to define them in the following way: animation:myClass="not-myClass"
. Where in this instance the svg element will get the class myClass
when the conditions are met, and get the class not-myClass
when the condition is not met. Conditions are checked using a regular expression. The initial class can be set by adding [class]:initial="true"
or [class]:initial="false"
to the element. Multiple conditions on the same class will act as a &&
. For example for an element like in the example below
<path myClass:state="1" myClass:group="1|2" ...></path>
with a data frame that contains { address: 11010101, group:2, state:0 }
. This will set the class of the element to not-myClass
. This is because although the group condition (1|2
) matches 2
, the state condition (1
) does not match 0
. When you want to add your own functionality you will want to add css to actually react to the class change of the svghighlighter. An example of such a custom class is given in the example below.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW X7 -->
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:animation="-"
xmlns:pulse="-"
xmlns:show="-"
xmlns:hide="-"
animation:contact="no-contact"
xml:space="preserve" width="1667px" height="2110px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="0 0 1667 2110" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
</style>
</defs>
<g id="Layer_x0020_1">
<path contact:state="1" d="..."/>
<path pulse:initial="true" pulse:state="1" pulse:group="0" d="..."/>
<path show:group="1|2" d="..."/>
<path hide:address=".{22}" d="..."/>
</g>
</svg>
with an included css file containing the animation when the contact
and no-contact
classes are set like below.
svg .contact {
-moz-animation: contact 1s ease-in-out forwards;
-webkit-animation: contact 1s ease-in-out forwards;
-ms-animation: contact 1s ease-in-out forwards;
animation: contact 1s ease-in-out forwards;
}
svg .no-contact {
-moz-animation: no-contact 1s ease-in-out forwards;
-webkit-animation: no-contact 1s ease-in-out forwards;
-ms-animation: no-contact 1 ease-in-out forwards;
animation: no-contact 1s ease-in-out forwards;
}
@-webkit-keyframes contact {
from {
transform: rotate(8deg) translate(50px, -30px);
}
to {
transform: rotate(0.5deg) translate(-64px, -5px);
}
}
@keyframes contact {
from {
transform: rotate(8deg) translate(50px, -30px);
}
to {
transform: rotate(0.5deg) translate(-64px, -5px);
}
}
@-webkit-keyframes no-contact {
from {
transform: rotate(0.5deg) translate(-64px, -5px);
}
to {
transform: rotate(8deg) translate(50px, -30px);
}
}
@keyframes no-contact {
from {
transform: rotate(0.5deg) translate(-64px, -5px);
}
to {
transform: rotate(8deg) translate(50px, -30px);
}
}
Debugging
If you are not sure that the configuration you made is parsed correctly it is possible to look at the resulting configuration in the following files: /drivers/config.js
, /drivers/my_driver/driver.js
and /drivers/my_driver/pair/view.html
. Notice that changes you make in this file will be overwritten when it is generated again.