@bldgblocks/node-red-contrib-quietcool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/nodes/quietcool-config.html +144 -0
- package/nodes/quietcool-config.js +300 -0
- package/nodes/quietcool-control.html +106 -0
- package/nodes/quietcool-control.js +128 -0
- package/nodes/quietcool-sensor.html +85 -0
- package/nodes/quietcool-sensor.js +101 -0
- package/package.json +47 -0
- package/python/bridge.py +606 -0
- package/python/requirements.txt +1 -0
- package/python/setup_venv.sh +43 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="quietcool-control">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-fan"><i class="fa fa-bluetooth"></i> Fan</label>
|
|
4
|
+
<input type="text" id="node-input-fan">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-action"><i class="fa fa-cog"></i> Action</label>
|
|
8
|
+
<select id="node-input-action" style="width:70%;">
|
|
9
|
+
<option value="off">Turn Off</option>
|
|
10
|
+
<option value="smart">Smart Mode (TH)</option>
|
|
11
|
+
<option value="run_high">Run High</option>
|
|
12
|
+
<option value="run_low">Run Low</option>
|
|
13
|
+
<option value="timer">Timer</option>
|
|
14
|
+
<option value="preset">Apply Preset</option>
|
|
15
|
+
<option value="set_mode">Set Mode (custom)</option>
|
|
16
|
+
<option value="set_speed">Set Speed (custom)</option>
|
|
17
|
+
<option value="set_thresholds">Set Thresholds</option>
|
|
18
|
+
<option value="raw">Raw API Command</option>
|
|
19
|
+
</select>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="form-row" id="action-value-row">
|
|
22
|
+
<label for="node-input-actionValue"><i class="fa fa-pencil"></i> Value</label>
|
|
23
|
+
<input type="text" id="node-input-actionValue" placeholder="e.g., Winter, HIGH, TH">
|
|
24
|
+
</div>
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
27
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
28
|
+
</div>
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script type="text/html" data-help-name="quietcool-control">
|
|
32
|
+
<p>Controls a QuietCool whole house fan over BLE.</p>
|
|
33
|
+
|
|
34
|
+
<h3>Actions</h3>
|
|
35
|
+
<ul>
|
|
36
|
+
<li><b>Turn Off</b> — Sets fan to Idle mode</li>
|
|
37
|
+
<li><b>Smart Mode (TH)</b> — Temperature/humidity auto mode</li>
|
|
38
|
+
<li><b>Run High</b> — Continuous run at high speed</li>
|
|
39
|
+
<li><b>Run Low</b> — Continuous run at low speed</li>
|
|
40
|
+
<li><b>Timer</b> — Run for a duration. Set <code>msg.payload.hours</code>,
|
|
41
|
+
<code>msg.payload.minutes</code>, <code>msg.payload.speed</code></li>
|
|
42
|
+
<li><b>Apply Preset</b> — Apply a named profile (Summer, Winter, etc.).
|
|
43
|
+
Set preset name in Value or <code>msg.payload.name</code></li>
|
|
44
|
+
<li><b>Set Mode</b> — Custom mode: Idle, Timer, TH</li>
|
|
45
|
+
<li><b>Set Speed</b> — Custom speed: LOW, HIGH</li>
|
|
46
|
+
<li><b>Set Thresholds</b> — Set temp/humidity thresholds via <code>msg.payload.args</code></li>
|
|
47
|
+
<li><b>Raw</b> — Send raw JSON API command</li>
|
|
48
|
+
</ul>
|
|
49
|
+
|
|
50
|
+
<h3>Input</h3>
|
|
51
|
+
<p>Trigger with any <code>msg</code>. Override action via <code>msg.payload</code>:</p>
|
|
52
|
+
<pre>msg.payload = {
|
|
53
|
+
action: "preset",
|
|
54
|
+
args: { name: "Summer" }
|
|
55
|
+
}</pre>
|
|
56
|
+
|
|
57
|
+
<h3>Output</h3>
|
|
58
|
+
<p><code>msg.payload</code> contains the fan's JSON response.</p>
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<script type="text/javascript">
|
|
62
|
+
RED.nodes.registerType("quietcool-control", {
|
|
63
|
+
category: "QuietCool",
|
|
64
|
+
defaults: {
|
|
65
|
+
name: { value: "" },
|
|
66
|
+
fan: { value: "", type: "quietcool-config", required: true },
|
|
67
|
+
action: { value: "off" },
|
|
68
|
+
actionValue: { value: "" },
|
|
69
|
+
},
|
|
70
|
+
color: "#4DB6AC",
|
|
71
|
+
inputs: 1,
|
|
72
|
+
outputs: 1,
|
|
73
|
+
icon: "font-awesome/fa-fan",
|
|
74
|
+
paletteLabel: "fan control",
|
|
75
|
+
label: function () {
|
|
76
|
+
if (this.name) return this.name;
|
|
77
|
+
var labels = {
|
|
78
|
+
off: "Fan Off",
|
|
79
|
+
smart: "Smart Mode",
|
|
80
|
+
run_high: "Run High",
|
|
81
|
+
run_low: "Run Low",
|
|
82
|
+
timer: "Timer",
|
|
83
|
+
preset: "Preset" + (this.actionValue ? ": " + this.actionValue : ""),
|
|
84
|
+
set_mode: "Mode",
|
|
85
|
+
set_speed: "Speed",
|
|
86
|
+
set_thresholds: "Thresholds",
|
|
87
|
+
raw: "Raw",
|
|
88
|
+
};
|
|
89
|
+
return labels[this.action] || "Fan Control";
|
|
90
|
+
},
|
|
91
|
+
oneditprepare: function () {
|
|
92
|
+
var actionSelect = $("#node-input-action");
|
|
93
|
+
var valueRow = $("#action-value-row");
|
|
94
|
+
function toggleValue() {
|
|
95
|
+
var action = actionSelect.val();
|
|
96
|
+
if (["preset", "set_mode", "set_speed"].indexOf(action) >= 0) {
|
|
97
|
+
valueRow.show();
|
|
98
|
+
} else {
|
|
99
|
+
valueRow.hide();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
actionSelect.on("change", toggleValue);
|
|
103
|
+
toggleValue();
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuietCool BLE control node — send commands to the fan.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
module.exports = function (RED) {
|
|
6
|
+
function QuietCoolControlNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
|
|
10
|
+
node.configNodeId = config.fan;
|
|
11
|
+
node.action = config.action || "set_mode";
|
|
12
|
+
node.actionValue = config.actionValue || "";
|
|
13
|
+
|
|
14
|
+
const fanConfig = RED.nodes.getNode(node.configNodeId);
|
|
15
|
+
if (!fanConfig) {
|
|
16
|
+
node.status({ fill: "red", shape: "ring", text: "no config" });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
node.updateNodeStatus = function (connected) {
|
|
21
|
+
node.status(
|
|
22
|
+
connected
|
|
23
|
+
? { fill: "green", shape: "dot", text: "connected" }
|
|
24
|
+
: { fill: "red", shape: "ring", text: "disconnected" }
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
fanConfig.registerUser(node);
|
|
29
|
+
|
|
30
|
+
node.on("input", function (msg, send, done) {
|
|
31
|
+
send =
|
|
32
|
+
send ||
|
|
33
|
+
function () {
|
|
34
|
+
node.send.apply(node, arguments);
|
|
35
|
+
};
|
|
36
|
+
done =
|
|
37
|
+
done ||
|
|
38
|
+
function (err) {
|
|
39
|
+
if (err) node.error(err, msg);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Determine action from node config or msg.payload
|
|
43
|
+
let action = node.action;
|
|
44
|
+
let args = {};
|
|
45
|
+
|
|
46
|
+
if (msg.payload && typeof msg.payload === "object") {
|
|
47
|
+
if (msg.payload.action) action = msg.payload.action;
|
|
48
|
+
if (msg.payload.args) args = msg.payload.args;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Map user-friendly actions to bridge commands
|
|
52
|
+
switch (action) {
|
|
53
|
+
case "off":
|
|
54
|
+
args = { mode: "Idle" };
|
|
55
|
+
action = "set_mode";
|
|
56
|
+
break;
|
|
57
|
+
case "smart":
|
|
58
|
+
args = { mode: "TH" };
|
|
59
|
+
action = "set_mode";
|
|
60
|
+
break;
|
|
61
|
+
case "run_high":
|
|
62
|
+
args = { speed: "HIGH" };
|
|
63
|
+
action = "set_speed";
|
|
64
|
+
break;
|
|
65
|
+
case "run_low":
|
|
66
|
+
args = { speed: "LOW" };
|
|
67
|
+
action = "set_speed";
|
|
68
|
+
break;
|
|
69
|
+
case "timer":
|
|
70
|
+
args.hours = args.hours || msg.payload.hours || 1;
|
|
71
|
+
args.minutes = args.minutes || msg.payload.minutes || 0;
|
|
72
|
+
args.speed = args.speed || msg.payload.speed || "HIGH";
|
|
73
|
+
action = "set_timer";
|
|
74
|
+
break;
|
|
75
|
+
case "preset":
|
|
76
|
+
args.name =
|
|
77
|
+
args.name ||
|
|
78
|
+
node.actionValue ||
|
|
79
|
+
msg.payload.preset ||
|
|
80
|
+
msg.payload.name ||
|
|
81
|
+
"";
|
|
82
|
+
action = "set_preset";
|
|
83
|
+
break;
|
|
84
|
+
case "set_mode":
|
|
85
|
+
args.mode =
|
|
86
|
+
args.mode ||
|
|
87
|
+
node.actionValue ||
|
|
88
|
+
msg.payload.mode ||
|
|
89
|
+
"Idle";
|
|
90
|
+
break;
|
|
91
|
+
case "set_speed":
|
|
92
|
+
args.speed =
|
|
93
|
+
args.speed ||
|
|
94
|
+
node.actionValue ||
|
|
95
|
+
msg.payload.speed ||
|
|
96
|
+
"HIGH";
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
node.status({ fill: "blue", shape: "dot", text: action });
|
|
103
|
+
|
|
104
|
+
fanConfig.sendBridgeCommand(action, args, function (response) {
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
msg.payload = response.data;
|
|
107
|
+
node.updateNodeStatus(fanConfig.connected);
|
|
108
|
+
send(msg);
|
|
109
|
+
done();
|
|
110
|
+
} else {
|
|
111
|
+
node.status({
|
|
112
|
+
fill: "red",
|
|
113
|
+
shape: "ring",
|
|
114
|
+
text: response.error || "error",
|
|
115
|
+
});
|
|
116
|
+
done(new Error(response.error || "Command failed"));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
node.on("close", function (done) {
|
|
122
|
+
fanConfig.deregisterUser(node);
|
|
123
|
+
done();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
RED.nodes.registerType("quietcool-control", QuietCoolControlNode);
|
|
128
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="quietcool-sensor">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-fan"><i class="fa fa-bluetooth"></i> Fan</label>
|
|
4
|
+
<input type="text" id="node-input-fan">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-query"><i class="fa fa-search"></i> Query</label>
|
|
8
|
+
<select id="node-input-query" style="width:70%;">
|
|
9
|
+
<option value="get_state">State (temp, humidity, mode, speed)</option>
|
|
10
|
+
<option value="get_status">Full Status</option>
|
|
11
|
+
<option value="get_info">Fan Info</option>
|
|
12
|
+
<option value="get_version">Firmware Version</option>
|
|
13
|
+
<option value="get_params">Parameters</option>
|
|
14
|
+
<option value="get_presets">Presets</option>
|
|
15
|
+
<option value="get_remain">Timer Remaining</option>
|
|
16
|
+
</select>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-row">
|
|
19
|
+
<label for="node-input-pollInterval"><i class="fa fa-clock-o"></i> Poll (sec)</label>
|
|
20
|
+
<input type="number" id="node-input-pollInterval" min="0" step="1" placeholder="0 = on input only" style="width:100px;">
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
24
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
25
|
+
</div>
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script type="text/html" data-help-name="quietcool-sensor">
|
|
29
|
+
<p>Reads sensor data and status from a QuietCool whole house fan.</p>
|
|
30
|
+
|
|
31
|
+
<h3>Queries</h3>
|
|
32
|
+
<ul>
|
|
33
|
+
<li><b>State</b> — Current mode, speed, temperature (°F), humidity (%)</li>
|
|
34
|
+
<li><b>Full Status</b> — Complete status including fan info, firmware, presets, thresholds</li>
|
|
35
|
+
<li><b>Fan Info</b> — Name, model, serial number</li>
|
|
36
|
+
<li><b>Firmware Version</b> — Firmware and hardware version</li>
|
|
37
|
+
<li><b>Parameters</b> — Current temp/humidity thresholds and timer settings</li>
|
|
38
|
+
<li><b>Presets</b> — List of preset profiles (Summer, Winter, etc.)</li>
|
|
39
|
+
<li><b>Timer Remaining</b> — Hours, minutes, seconds remaining on timer</li>
|
|
40
|
+
</ul>
|
|
41
|
+
|
|
42
|
+
<h3>Output</h3>
|
|
43
|
+
<p><code>msg.payload</code> — Full response object from the fan</p>
|
|
44
|
+
<p>For <b>State</b> and <b>Full Status</b> queries, convenience fields are added:</p>
|
|
45
|
+
<ul>
|
|
46
|
+
<li><code>msg.temperature</code> — Temperature in °F</li>
|
|
47
|
+
<li><code>msg.humidity</code> — Humidity %</li>
|
|
48
|
+
<li><code>msg.mode</code> — Current mode (Idle, Timer, TH)</li>
|
|
49
|
+
<li><code>msg.range</code> — Current speed (LOW, MEDIUM, HIGH, CLOSE)</li>
|
|
50
|
+
</ul>
|
|
51
|
+
|
|
52
|
+
<h3>Polling</h3>
|
|
53
|
+
<p>Set <b>Poll</b> to a number of seconds to auto-query at that interval.
|
|
54
|
+
Set to 0 to only query on input.</p>
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<script type="text/javascript">
|
|
58
|
+
RED.nodes.registerType("quietcool-sensor", {
|
|
59
|
+
category: "QuietCool",
|
|
60
|
+
defaults: {
|
|
61
|
+
name: { value: "" },
|
|
62
|
+
fan: { value: "", type: "quietcool-config", required: true },
|
|
63
|
+
query: { value: "get_state" },
|
|
64
|
+
pollInterval: { value: 0 },
|
|
65
|
+
},
|
|
66
|
+
color: "#80CBC4",
|
|
67
|
+
inputs: 1,
|
|
68
|
+
outputs: 1,
|
|
69
|
+
icon: "font-awesome/fa-thermometer-half",
|
|
70
|
+
paletteLabel: "fan sensor",
|
|
71
|
+
label: function () {
|
|
72
|
+
if (this.name) return this.name;
|
|
73
|
+
var labels = {
|
|
74
|
+
get_state: "Fan State",
|
|
75
|
+
get_status: "Full Status",
|
|
76
|
+
get_info: "Fan Info",
|
|
77
|
+
get_version: "Firmware",
|
|
78
|
+
get_params: "Parameters",
|
|
79
|
+
get_presets: "Presets",
|
|
80
|
+
get_remain: "Timer Left",
|
|
81
|
+
};
|
|
82
|
+
return labels[this.query] || "Fan Sensor";
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuietCool BLE sensor node — read sensor data from the fan.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
module.exports = function (RED) {
|
|
6
|
+
function QuietCoolSensorNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
|
|
10
|
+
node.configNodeId = config.fan;
|
|
11
|
+
node.query = config.query || "get_state";
|
|
12
|
+
node.pollInterval = (parseInt(config.pollInterval) || 0) * 1000;
|
|
13
|
+
node.pollTimer = null;
|
|
14
|
+
|
|
15
|
+
const fanConfig = RED.nodes.getNode(node.configNodeId);
|
|
16
|
+
if (!fanConfig) {
|
|
17
|
+
node.status({ fill: "red", shape: "ring", text: "no config" });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
node.updateNodeStatus = function (connected) {
|
|
22
|
+
node.status(
|
|
23
|
+
connected
|
|
24
|
+
? { fill: "green", shape: "dot", text: "connected" }
|
|
25
|
+
: { fill: "red", shape: "ring", text: "disconnected" }
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
fanConfig.registerUser(node);
|
|
30
|
+
|
|
31
|
+
function doQuery(msg) {
|
|
32
|
+
msg = msg || { payload: {} };
|
|
33
|
+
const query = node.query;
|
|
34
|
+
|
|
35
|
+
node.status({ fill: "blue", shape: "dot", text: query });
|
|
36
|
+
|
|
37
|
+
fanConfig.sendBridgeCommand(query, {}, function (response) {
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const data = response.data || {};
|
|
40
|
+
msg.payload = data;
|
|
41
|
+
|
|
42
|
+
// Add convenience fields for common queries
|
|
43
|
+
if (query === "get_state") {
|
|
44
|
+
msg.temperature =
|
|
45
|
+
data.temperature_f || (data.Temp_Sample || 0) / 10.0;
|
|
46
|
+
msg.humidity =
|
|
47
|
+
data.humidity_pct || data.Humidity_Sample || 0;
|
|
48
|
+
msg.mode = data.Mode;
|
|
49
|
+
msg.range = data.Range;
|
|
50
|
+
} else if (query === "get_status") {
|
|
51
|
+
msg.temperature = data.temperature_f;
|
|
52
|
+
msg.humidity = data.humidity;
|
|
53
|
+
msg.mode = data.mode;
|
|
54
|
+
msg.range = data.range;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
node.updateNodeStatus(fanConfig.connected);
|
|
58
|
+
node.send(msg);
|
|
59
|
+
} else {
|
|
60
|
+
node.status({
|
|
61
|
+
fill: "red",
|
|
62
|
+
shape: "ring",
|
|
63
|
+
text: response.error || "error",
|
|
64
|
+
});
|
|
65
|
+
node.error(response.error || "Query failed", msg);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Input-triggered query
|
|
71
|
+
node.on("input", function (msg) {
|
|
72
|
+
doQuery(msg);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Optional polling
|
|
76
|
+
if (node.pollInterval > 0) {
|
|
77
|
+
const startPolling = () => {
|
|
78
|
+
if (fanConfig.connected) {
|
|
79
|
+
node.pollTimer = setInterval(
|
|
80
|
+
() => doQuery(),
|
|
81
|
+
node.pollInterval
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
setTimeout(startPolling, 5000);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
setTimeout(startPolling, 3000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
node.on("close", function (done) {
|
|
91
|
+
if (node.pollTimer) {
|
|
92
|
+
clearInterval(node.pollTimer);
|
|
93
|
+
node.pollTimer = null;
|
|
94
|
+
}
|
|
95
|
+
fanConfig.deregisterUser(node);
|
|
96
|
+
done();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
RED.nodes.registerType("quietcool-sensor", QuietCoolSensorNode);
|
|
101
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bldgblocks/node-red-contrib-quietcool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node-RED nodes for controlling QuietCool whole house fans over BLE",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node-red",
|
|
7
|
+
"quietcool",
|
|
8
|
+
"ble",
|
|
9
|
+
"bluetooth",
|
|
10
|
+
"fan",
|
|
11
|
+
"hvac",
|
|
12
|
+
"whole-house-fan",
|
|
13
|
+
"home-automation"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "BldgBlocks",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/BldgBlocks/node-red-contrib-quietcool"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"node-red": {
|
|
28
|
+
"version": ">=3.0.0",
|
|
29
|
+
"nodes": {
|
|
30
|
+
"quietcool-config": "nodes/quietcool-config.js",
|
|
31
|
+
"quietcool-control": "nodes/quietcool-control.js",
|
|
32
|
+
"quietcool-sensor": "nodes/quietcool-sensor.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"postinstall": "bash python/setup_venv.sh",
|
|
37
|
+
"preuninstall": "pkill -f 'python.*bridge\\.py' 2>/dev/null; rm -rf python/.venv 2>/dev/null; echo 'QuietCool BLE: Cleaned up.'; true"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"nodes/",
|
|
41
|
+
"python/bridge.py",
|
|
42
|
+
"python/requirements.txt",
|
|
43
|
+
"python/setup_venv.sh",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
]
|
|
47
|
+
}
|