woox 0.0.1
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.
- checksums.yaml +7 -0
- data/README.md +52 -0
- data/bin/woox +247 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 17c9cdba29244d47abb7332e08eeb06f001683195407f00e943035a66fbc3e60
|
|
4
|
+
data.tar.gz: 165c578c0f8883a141bf406cb43d44c67bb3509505dcb98d9e2aabd2f001a784
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 65889379e745a0732c13c621ac17c93e87a68ae7a59ddd443d703b0590ae9705171cb247d84e5fa01f69a72f05d7cbe3edc90326226a950dc0a9b9168a924cdb
|
|
7
|
+
data.tar.gz: 6b7a4b98fe2867ce99c66783a4b76915d3a56e1de585283f459a97576f21cdc855e7b08674eb8427843e6bc63a29b19d213cef95f345e89c1d4608528ade21f0
|
data/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Woox
|
|
2
|
+
|
|
3
|
+
A low-overhead, highly optimized local area network (LAN) library for communicating directly with Woox Smart Home hardware (Plugs, Bulbs, and Strips) using Tuya v3.3 socket engines. Carefully structured to enforce zero-object-allocation cipher cycles, maximizing execution performance on lower-end embedded microprocessors.
|
|
4
|
+
|
|
5
|
+
## 🤝 Project Origin & Credits
|
|
6
|
+
This library was authored by Jan Svoboda (`escpos`) in collaboration with an interactive physical hardware logging sandbox loop and a Google AI assistant. This engineering process isolated and resolved the strict 16-byte cryptographic resetting quirks of the Tuya protocol.
|
|
7
|
+
|
|
8
|
+
## 🔑 How to Retrieve Your Device ID and Local Key
|
|
9
|
+
|
|
10
|
+
### Method A: The QR Code Script Approach (Easiest / No Developer Account)
|
|
11
|
+
You can completely skip creating cloud developer projects by using public reverse-engineered client tools:
|
|
12
|
+
1. Run the official interactive reverse-extraction terminal utility:
|
|
13
|
+
```bash
|
|
14
|
+
npx @tuyapi/cli wizard
|
|
15
|
+
```
|
|
16
|
+
2. Enter the standard application User ID code from your mobile **Smart Life** or **Tuya Smart** application profile.
|
|
17
|
+
3. Scan the terminal's generated QR code with your mobile app's scanner. It will immediately dump every connected hardware `Device ID` and `Local Key` string.
|
|
18
|
+
|
|
19
|
+
### Method B: The Tuya Cloud Explorer Approach (Official Manual Route)
|
|
20
|
+
1. Register a trial workspace on the [Tuya Developer Portal](https://tuya.com).
|
|
21
|
+
2. Navigate to **Cloud -> Link Tuya App** and scan the link vector with your mobile app.
|
|
22
|
+
3. Head to **Cloud -> API Explorer -> Smart Home Device System -> Device Management -> Query Device Details**.
|
|
23
|
+
4. Provide your hardware tracking strings to retrieve your permanent, matching 16-character `local_key`.
|
|
24
|
+
|
|
25
|
+
## 💻 Installation
|
|
26
|
+
Add this line to your project's `Gemfile`:
|
|
27
|
+
```ruby
|
|
28
|
+
gem 'woox'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
And then execute:
|
|
32
|
+
```bash
|
|
33
|
+
\$ bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 🛠 Command Line Interface (CLI) Use
|
|
37
|
+
Once installed, use the global shell hook to toggle state values or parse metrics straight from your terminal terminal:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# General Syntax: woox <plug|bulb> <ip> <device_id> <local_key> <action> [value]
|
|
41
|
+
|
|
42
|
+
# Controlling a Smart Plug:
|
|
43
|
+
\$ woox plug 192.168.1.50 <device_id> <local_key> on
|
|
44
|
+
\$ woox plug 192.168.1.50 <device_id> <local_key> status
|
|
45
|
+
|
|
46
|
+
# Controlling a Smart Bulb/LED Strip:
|
|
47
|
+
\$ woox bulb 192.168.1.60 <device_id> <local_key> brightness 300
|
|
48
|
+
\$ woox bulb 192.168.1.60 <device_id> <local_key> color green
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 🔴 Comprehensive Programming Examples
|
|
52
|
+
For an exhaustive, runnable breakdown of all available capabilities (including dimming loops and sequential multi-DP unbundled tracking controls), see the `examples/all_use_cases.rb` script inside the repository directory.
|
data/bin/woox
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Robust path interception: pulls from local lib tree if testing locally
|
|
5
|
+
local_lib = File.expand_path("../lib", __dir__)
|
|
6
|
+
if File.directory?(local_lib)
|
|
7
|
+
$LOAD_PATH.unshift(local_lib)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
require "woox"
|
|
12
|
+
rescue LoadError => e
|
|
13
|
+
puts "Error: Cannot load the Woox library module framework."
|
|
14
|
+
puts e.message
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require "io/console"
|
|
19
|
+
require "json"
|
|
20
|
+
|
|
21
|
+
# FIXED: Re-engineered using string definitions to prevent markdown rendering bugs from stripping the array values
|
|
22
|
+
COLOR_PRESETS = {
|
|
23
|
+
"red" => "255,0,0".split(",").map(&:to_i),
|
|
24
|
+
"green" => "0,255,0".split(",").map(&:to_i),
|
|
25
|
+
"blue" => "0,0,255".split(",").map(&:to_i),
|
|
26
|
+
"yellow" => "255,255,0".split(",").map(&:to_i),
|
|
27
|
+
"magenta" => "255,0,255".split(",").map(&:to_i),
|
|
28
|
+
"cyan" => "0,255,255".split(",").map(&:to_i),
|
|
29
|
+
"orange" => "255,165,0".split(",").map(&:to_i),
|
|
30
|
+
"purple" => "128,0,128".split(",").map(&:to_i),
|
|
31
|
+
"pink" => "255,192,203".split(",").map(&:to_i),
|
|
32
|
+
"white" => "255,255,255".split(",").map(&:to_i)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Helper method to transform standard RGB representations into Tuya v3.3 12-character HHHHSSSSVVVV profiles
|
|
36
|
+
def convert_rgb_to_tuya_hex(r, g, b, targeted_brightness = nil)
|
|
37
|
+
r_n = r / 255.0
|
|
38
|
+
g_n = g / 255.0
|
|
39
|
+
b_n = b / 255.0
|
|
40
|
+
|
|
41
|
+
max_val = [r_n, g_n, b_n].max
|
|
42
|
+
min_val = [r_n, g_n, b_n].min
|
|
43
|
+
delta = max_val - min_val
|
|
44
|
+
|
|
45
|
+
# Calculate Hue (Scaled 0-360)
|
|
46
|
+
h = 0
|
|
47
|
+
if delta > 0
|
|
48
|
+
if max_val == r_n
|
|
49
|
+
h = (60 * ((g_n - b_n) / delta) + 360) % 360
|
|
50
|
+
elsif max_val == g_n
|
|
51
|
+
h = (60 * ((b_n - r_n) / delta) + 120) % 360
|
|
52
|
+
elsif max_val == b_n
|
|
53
|
+
h = (60 * ((r_n - g_n) / delta) + 240) % 360
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Calculate Saturation (Scaled 0-1000)
|
|
58
|
+
s = max_val == 0 ? 0 : (delta / max_val) * 1000
|
|
59
|
+
|
|
60
|
+
# If a specific brightness level is provided, use that instead of maximizing to 1000 based on RGB layout
|
|
61
|
+
v = targeted_brightness ? targeted_brightness : (max_val * 1000)
|
|
62
|
+
|
|
63
|
+
# Convert safely to hex strings padded precisely to 4 characters
|
|
64
|
+
sprintf("%04x%04x%04x", h.round, s.round, v.round)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Utility to break down flexible input formats (presets, comma structures, or hex values) into raw RGB values
|
|
68
|
+
def parse_color_input(input)
|
|
69
|
+
cleaned = input.to_s.strip.downcase
|
|
70
|
+
|
|
71
|
+
if COLOR_PRESETS.has_key?(cleaned)
|
|
72
|
+
return COLOR_PRESETS[cleaned]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if cleaned.include?(",")
|
|
76
|
+
parts = cleaned.split(",").map { |p| p.to_i }
|
|
77
|
+
return parts if parts.length == 3
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
hex_match = cleaned.gsub("#", "")
|
|
81
|
+
if hex_match.length == 6 && hex_match =~ /\A[0-9a-f]{6}\z/
|
|
82
|
+
r = hex_match[0..1].to_i(16)
|
|
83
|
+
g = hex_match[2..3].to_i(16)
|
|
84
|
+
b = hex_match[4..5].to_i(16)
|
|
85
|
+
return [r, g, b]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Enforce a strict 16-byte length boundary safety check for cryptographic keys
|
|
92
|
+
def sanitize_key(raw_key)
|
|
93
|
+
cleaned = raw_key.to_s.strip
|
|
94
|
+
if cleaned.bytesize > 16
|
|
95
|
+
cleaned[0...16]
|
|
96
|
+
elsif cleaned.bytesize < 16
|
|
97
|
+
cleaned.ljust(16, "\x00")
|
|
98
|
+
else
|
|
99
|
+
cleaned
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Executed when running the interactive CLI interface
|
|
104
|
+
def run_interactive_menu(device, type)
|
|
105
|
+
loop do
|
|
106
|
+
puts "\n--- Woox Interactive Controller (#{(type == 'bulb' ? 'Smart Bulb' : 'Smart Plug')}) ---"
|
|
107
|
+
puts " Power Relay ON"
|
|
108
|
+
puts " Power Relay OFF"
|
|
109
|
+
puts " [s] Query Telemetry Status Matrix"
|
|
110
|
+
if type == "bulb"
|
|
111
|
+
puts " [b] Set White Brightness Level (10-1000)"
|
|
112
|
+
puts " [c] Modify Chromatic RGB Color Space"
|
|
113
|
+
end
|
|
114
|
+
puts " [q] Quit CLI Interface"
|
|
115
|
+
print "Select action: "
|
|
116
|
+
choice = IO.console.gets.strip.downcase
|
|
117
|
+
|
|
118
|
+
case choice
|
|
119
|
+
when "1", "on"
|
|
120
|
+
puts device.turn_on ? "✅ Success: Device turned ON" : "❌ Error: Command failed"
|
|
121
|
+
when "2", "off"
|
|
122
|
+
puts device.turn_off ? "✅ Success: Device turned OFF" : "❌ Error: Command failed"
|
|
123
|
+
when "s", "status"
|
|
124
|
+
status = device.get_status
|
|
125
|
+
if status
|
|
126
|
+
puts "✅ Current Data Points (dps): #{status['dps']}"
|
|
127
|
+
else
|
|
128
|
+
puts "❌ Error: Failed to retrieve status payload."
|
|
129
|
+
end
|
|
130
|
+
when "b", "brightness"
|
|
131
|
+
if type == "bulb"
|
|
132
|
+
print "Enter brightness level (10-1000): "
|
|
133
|
+
val = IO.console.gets.strip.to_i
|
|
134
|
+
puts device.set_brightness(val) ? "✅ Brightness adjusted" : "❌ Command failed"
|
|
135
|
+
end
|
|
136
|
+
when "c", "color"
|
|
137
|
+
if type == "bulb"
|
|
138
|
+
print "Enter color (e.g., 'red', 'ff0000', or '0,255,0'): "
|
|
139
|
+
input = IO.console.gets.strip
|
|
140
|
+
rgb = parse_color_input(input)
|
|
141
|
+
if rgb
|
|
142
|
+
# Read the current status to preserve the active brightness level
|
|
143
|
+
status = device.get_status
|
|
144
|
+
active_brightness = nil
|
|
145
|
+
|
|
146
|
+
if status && status["dps"]
|
|
147
|
+
if status["dps"]["21"] == "colour" && status["dps"]["24"]
|
|
148
|
+
# Extract the last 4 hex characters (VVVV) from the existing color code
|
|
149
|
+
active_brightness = status["dps"]["24"][-4..-1].to_i(16)
|
|
150
|
+
elsif status["dps"]["22"]
|
|
151
|
+
# Fallback to white mode brightness if switching from white to color
|
|
152
|
+
active_brightness = status["dps"]["22"].to_i
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
tuya_hex = convert_rgb_to_tuya_hex(rgb[0], rgb[1], rgb[2], active_brightness)
|
|
157
|
+
puts device.set_dps("21" => "colour", "24" => tuya_hex) ? "✅ Color shifted to RGB #{rgb.inspect}" : "❌ Command failed"
|
|
158
|
+
else
|
|
159
|
+
puts "❌ Error: Unknown color pattern entry."
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
when "q", "quit"
|
|
163
|
+
puts "Exiting script runner."
|
|
164
|
+
break
|
|
165
|
+
else
|
|
166
|
+
puts "❌ Error: Invalid selection menu choice."
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ==============================================================================
|
|
172
|
+
# MAIN ROUTING BLOCK
|
|
173
|
+
# ==============================================================================
|
|
174
|
+
|
|
175
|
+
if ARGV.empty? || ARGV == ["-i"] || ARGV == ["--interactive"]
|
|
176
|
+
puts "Woox Local Engine Configuration Setup Wizard"
|
|
177
|
+
puts "============================================="
|
|
178
|
+
print "Device Target Type (plug/bulb): "; type = IO.console.gets.strip.downcase
|
|
179
|
+
print "Device LAN Network IP Address : "; ip = IO.console.gets.strip
|
|
180
|
+
print "Alphanumeric Hardware Device ID: "; device_id = IO.console.gets.strip
|
|
181
|
+
print "Permanent 16-Character Key : "; raw_key = IO.console.gets.strip
|
|
182
|
+
|
|
183
|
+
key = sanitize_key(raw_key)
|
|
184
|
+
device = (type == "bulb") ? Woox::LocalBulb.new(ip, device_id, key) : Woox::LocalPlug.new(ip, device_id, key)
|
|
185
|
+
run_interactive_menu(device, type)
|
|
186
|
+
exit 0
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if ARGV.length < 5
|
|
190
|
+
puts "Usage (Automation Mode): woox <plug|bulb> <ip> <device_id> <local_key> <action> [value]"
|
|
191
|
+
puts "Usage (Interactive Mode): woox --interactive"
|
|
192
|
+
exit 1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
type, ip, device_id, local_key, action, value = ARGV[0..5]
|
|
196
|
+
key = sanitize_key(local_key)
|
|
197
|
+
device = (type == "bulb") ? Woox::LocalBulb.new(ip, device_id, key) : Woox::LocalPlug.new(ip, device_id, key)
|
|
198
|
+
|
|
199
|
+
case action.to_s.downcase
|
|
200
|
+
when "1", "on"
|
|
201
|
+
exit(device.turn_on ? 0 : 1)
|
|
202
|
+
when "2", "off"
|
|
203
|
+
exit(device.turn_off ? 0 : 1)
|
|
204
|
+
when "s", "status"
|
|
205
|
+
status = device.get_status
|
|
206
|
+
if status
|
|
207
|
+
puts status["dps"].to_json
|
|
208
|
+
exit 0
|
|
209
|
+
else
|
|
210
|
+
puts "Error"
|
|
211
|
+
exit 1
|
|
212
|
+
end
|
|
213
|
+
when "brightness", "bright"
|
|
214
|
+
if type == "bulb"
|
|
215
|
+
exit(device.set_brightness(value.to_i) ? 0 : 1)
|
|
216
|
+
else
|
|
217
|
+
puts "Error: Brightness parameter adjustments are only supported on bulb hardware profiles."
|
|
218
|
+
exit 1
|
|
219
|
+
end
|
|
220
|
+
when "color", "colour"
|
|
221
|
+
if type == "bulb"
|
|
222
|
+
rgb = parse_color_input(value)
|
|
223
|
+
if rgb
|
|
224
|
+
status = device.get_status
|
|
225
|
+
active_brightness = nil
|
|
226
|
+
if status && status["dps"]
|
|
227
|
+
if status["dps"]["21"] == "colour" && status["dps"]["24"]
|
|
228
|
+
active_brightness = status["dps"]["24"][-4..-1].to_i(16)
|
|
229
|
+
elsif status["dps"]["22"]
|
|
230
|
+
active_brightness = status["dps"]["22"].to_i
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
tuya_hex = convert_rgb_to_tuya_hex(rgb[0], rgb[1], rgb[2], active_brightness)
|
|
235
|
+
exit(device.set_dps("21" => "colour", "24" => tuya_hex) ? 0 : 1)
|
|
236
|
+
else
|
|
237
|
+
puts "Error: Unknown color matrix format."
|
|
238
|
+
exit 1
|
|
239
|
+
end
|
|
240
|
+
else
|
|
241
|
+
puts "Error: Color parameters are only supported on bulb hardware profiles."
|
|
242
|
+
exit 1
|
|
243
|
+
end
|
|
244
|
+
else
|
|
245
|
+
puts "Unknown action: #{action}"
|
|
246
|
+
exit 1
|
|
247
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: woox
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jan Svoboda
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.0'
|
|
55
|
+
description: Engineered via an exhaustive human-AI hardware testing session, this
|
|
56
|
+
gem provides direct local Tuya v3.3 socket communication configurations for Woox
|
|
57
|
+
smart plugs, avoiding all external cloud routing bottlenecks.
|
|
58
|
+
email:
|
|
59
|
+
- jan@mluv.cz
|
|
60
|
+
executables:
|
|
61
|
+
- woox
|
|
62
|
+
extensions: []
|
|
63
|
+
extra_rdoc_files: []
|
|
64
|
+
files:
|
|
65
|
+
- README.md
|
|
66
|
+
- bin/woox
|
|
67
|
+
homepage: https://github.com/svoboda-jan/woox
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
homepage_uri: https://github.com/svoboda-jan/woox
|
|
72
|
+
source_code_uri: https://github.com/svoboda-jan/woox
|
|
73
|
+
cooperation_credits: Developed via interactive runtime pair-programming between
|
|
74
|
+
a human engineer running active physical test benches and a Google AI assistant.
|
|
75
|
+
post_install_message:
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 1.9.2
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 3.1.6
|
|
91
|
+
signing_key:
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: A lightweight local network Ruby driver for Woox Smart Home devices.
|
|
94
|
+
test_files: []
|