smart_message 0.0.2 → 0.0.4
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 +4 -4
- data/CHANGELOG.md +31 -1
- data/Gemfile.lock +6 -1
- data/README.md +92 -14
- data/docs/README.md +1 -0
- data/docs/architecture.md +41 -8
- data/docs/dispatcher.md +52 -16
- data/docs/getting-started.md +64 -2
- data/docs/logging.md +452 -0
- data/docs/message_processing.md +423 -0
- data/docs/proc_handlers_summary.md +247 -0
- data/docs/transports.md +202 -8
- data/examples/.gitignore +2 -0
- data/examples/04_redis_smart_home_iot.rb +649 -0
- data/examples/05_proc_handlers.rb +181 -0
- data/examples/06_custom_logger_example.rb +620 -0
- data/examples/README.md +118 -3
- data/examples/smart_home_iot_dataflow.md +257 -0
- data/lib/smart_message/base.rb +94 -4
- data/lib/smart_message/dispatcher.rb +22 -6
- data/lib/smart_message/logger/default.rb +217 -0
- data/lib/smart_message/logger.rb +9 -1
- data/lib/smart_message/transport/redis_transport.rb +190 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +1 -0
- data/lib/smart_message/version.rb +1 -1
- data/smart_message.gemspec +1 -0
- metadata +25 -1
@@ -0,0 +1,649 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/04_redis_smart_home_iot.rb
|
3
|
+
#
|
4
|
+
# Redis Transport Example: Smart Home IoT Dashboard
|
5
|
+
#
|
6
|
+
# This example demonstrates Redis pub/sub messaging in a realistic IoT scenario
|
7
|
+
# where multiple smart home devices publish sensor data and receive commands
|
8
|
+
# through Redis channels. Each message type gets its own Redis channel for
|
9
|
+
# efficient routing and scaling.
|
10
|
+
|
11
|
+
require_relative '../lib/smart_message'
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'redis'
|
15
|
+
rescue LoadError
|
16
|
+
puts "⚠️ Redis gem not available. Install with: gem install redis"
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
puts "=== SmartMessage Redis Example: Smart Home IoT Dashboard ==="
|
21
|
+
puts
|
22
|
+
|
23
|
+
# Suppress thread error reporting for cleaner output
|
24
|
+
# This prevents low-level socket error messages from Redis connection pools
|
25
|
+
# in Ruby 3.4+ that don't affect functionality
|
26
|
+
Thread.report_on_exception = false
|
27
|
+
|
28
|
+
# Configuration for Redis (falls back to memory if Redis not available)
|
29
|
+
def create_transport
|
30
|
+
begin
|
31
|
+
# Try Redis first - test the connection before creating transport
|
32
|
+
test_redis = Redis.new(url: 'redis://localhost:6379', db: 1, timeout: 1)
|
33
|
+
test_redis.ping
|
34
|
+
test_redis.quit
|
35
|
+
|
36
|
+
# If we get here, Redis is available
|
37
|
+
puts "✅ Redis server detected, using Redis transport"
|
38
|
+
SmartMessage::Transport.create(:redis,
|
39
|
+
url: 'redis://localhost:6379',
|
40
|
+
db: 1, # Use database 1 for examples
|
41
|
+
auto_subscribe: true,
|
42
|
+
debug: false
|
43
|
+
)
|
44
|
+
rescue => e
|
45
|
+
# Catch all Redis-related connection errors
|
46
|
+
if e.class.name.include?('Redis') || e.class.name.include?('Connection') || e.is_a?(Errno::ECONNREFUSED)
|
47
|
+
puts "⚠️ Redis not available (#{e.class.name}), falling back to memory transport"
|
48
|
+
puts " To use Redis: ensure Redis server is running on localhost:6379"
|
49
|
+
else
|
50
|
+
puts "⚠️ Unexpected error connecting to Redis (#{e.class.name}: #{e.message})"
|
51
|
+
puts " Falling back to memory transport"
|
52
|
+
end
|
53
|
+
SmartMessage::Transport.create(:memory, auto_process: true)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
SHARED_TRANSPORT = create_transport
|
58
|
+
|
59
|
+
# Sensor Data Message - Published by IoT devices
|
60
|
+
class SensorDataMessage < SmartMessage::Base
|
61
|
+
property :device_id
|
62
|
+
property :device_type # 'thermostat', 'security_camera', 'door_lock', 'smoke_detector'
|
63
|
+
property :location # 'living_room', 'kitchen', 'bedroom', 'garage'
|
64
|
+
property :sensor_type # 'temperature', 'humidity', 'motion', 'status'
|
65
|
+
property :value
|
66
|
+
property :unit # 'celsius', 'percent', 'boolean', 'lux'
|
67
|
+
property :timestamp
|
68
|
+
property :battery_level # For battery-powered devices
|
69
|
+
|
70
|
+
config do
|
71
|
+
transport SHARED_TRANSPORT
|
72
|
+
serializer SmartMessage::Serializer::JSON.new
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.process(message_header, message_payload)
|
76
|
+
sensor_data = JSON.parse(message_payload)
|
77
|
+
icon = case sensor_data['device_type']
|
78
|
+
when 'thermostat' then '🌡️'
|
79
|
+
when 'security_camera' then '📹'
|
80
|
+
when 'door_lock' then '🚪'
|
81
|
+
when 'smoke_detector' then '🔥'
|
82
|
+
else '📊'
|
83
|
+
end
|
84
|
+
|
85
|
+
puts "#{icon} Sensor data: #{sensor_data['device_id']} (#{sensor_data['location']}) - #{sensor_data['sensor_type']}: #{sensor_data['value']} #{sensor_data['unit']}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Device Command Message - Sent to control IoT devices
|
90
|
+
class DeviceCommandMessage < SmartMessage::Base
|
91
|
+
property :device_id
|
92
|
+
property :command # 'set_temperature', 'lock_door', 'start_recording', 'test_alarm'
|
93
|
+
property :parameters # Command-specific parameters
|
94
|
+
property :requested_by # Who/what requested this command
|
95
|
+
property :timestamp
|
96
|
+
|
97
|
+
config do
|
98
|
+
transport SHARED_TRANSPORT
|
99
|
+
serializer SmartMessage::Serializer::JSON.new
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.process(message_header, message_payload)
|
103
|
+
command_data = JSON.parse(message_payload)
|
104
|
+
puts "🎛️ Command sent: #{command_data['command']} to #{command_data['device_id']} (requested by #{command_data['requested_by']})"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Alert Message - For critical notifications
|
109
|
+
class AlertMessage < SmartMessage::Base
|
110
|
+
property :alert_id
|
111
|
+
property :severity # 'low', 'medium', 'high', 'critical'
|
112
|
+
property :alert_type # 'security_breach', 'fire_detected', 'device_offline', 'battery_low'
|
113
|
+
property :device_id
|
114
|
+
property :location
|
115
|
+
property :message
|
116
|
+
property :timestamp
|
117
|
+
property :requires_action # Boolean indicating if immediate action needed
|
118
|
+
|
119
|
+
config do
|
120
|
+
transport SHARED_TRANSPORT
|
121
|
+
serializer SmartMessage::Serializer::JSON.new
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.process(message_header, message_payload)
|
125
|
+
alert_data = JSON.parse(message_payload)
|
126
|
+
severity_icon = case alert_data['severity']
|
127
|
+
when 'low' then '💙'
|
128
|
+
when 'medium' then '💛'
|
129
|
+
when 'high' then '🧡'
|
130
|
+
when 'critical' then '🚨'
|
131
|
+
else '📢'
|
132
|
+
end
|
133
|
+
|
134
|
+
puts "#{severity_icon} ALERT [#{alert_data['severity'].upcase}]: #{alert_data['message']} (#{alert_data['device_id']})"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Dashboard Status Message - System-wide status updates
|
139
|
+
class DashboardStatusMessage < SmartMessage::Base
|
140
|
+
property :system_status # 'normal', 'warning', 'alert'
|
141
|
+
property :active_devices
|
142
|
+
property :offline_devices
|
143
|
+
property :active_alerts
|
144
|
+
property :last_updated
|
145
|
+
property :summary_stats # Hash with various metrics
|
146
|
+
|
147
|
+
config do
|
148
|
+
transport SHARED_TRANSPORT
|
149
|
+
serializer SmartMessage::Serializer::JSON.new
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.process(message_header, message_payload)
|
153
|
+
status_data = JSON.parse(message_payload)
|
154
|
+
status_icon = case status_data['system_status']
|
155
|
+
when 'normal' then '✅'
|
156
|
+
when 'warning' then '⚠️'
|
157
|
+
when 'alert' then '🚨'
|
158
|
+
else '📊'
|
159
|
+
end
|
160
|
+
|
161
|
+
puts "#{status_icon} Dashboard: #{status_data['active_devices']} devices online, #{status_data['active_alerts']} active alerts"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Smart Thermostat Device
|
166
|
+
class SmartThermostat
|
167
|
+
def initialize(device_id, location)
|
168
|
+
@device_id = device_id
|
169
|
+
@location = location
|
170
|
+
@current_temp = 20.0 + rand(5.0) # Random starting temperature
|
171
|
+
@target_temp = 22.0
|
172
|
+
@battery_level = 85 + rand(15)
|
173
|
+
@running = false
|
174
|
+
|
175
|
+
puts "🌡️ Smart Thermostat #{@device_id} initialized in #{@location}"
|
176
|
+
|
177
|
+
# Subscribe to commands for this device
|
178
|
+
DeviceCommandMessage.subscribe("SmartThermostat.handle_command")
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.handle_command(message_header, message_payload)
|
182
|
+
command_data = JSON.parse(message_payload)
|
183
|
+
|
184
|
+
# Only process commands intended for thermostats with our device ID
|
185
|
+
return unless command_data['device_id']&.start_with?('THERM-')
|
186
|
+
return unless ['set_temperature', 'get_status'].include?(command_data['command'])
|
187
|
+
|
188
|
+
puts "🌡️ Thermostat #{command_data['device_id']} received command: #{command_data['command']}"
|
189
|
+
|
190
|
+
# Process the command
|
191
|
+
case command_data['command']
|
192
|
+
when 'set_temperature'
|
193
|
+
target_temp = command_data.dig('parameters', 'target')
|
194
|
+
puts " Setting target temperature to #{target_temp}°C" if target_temp
|
195
|
+
when 'get_status'
|
196
|
+
puts " Reporting current status"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def start_monitoring
|
201
|
+
@running = true
|
202
|
+
|
203
|
+
Thread.new do
|
204
|
+
while @running
|
205
|
+
# Simulate temperature readings
|
206
|
+
@current_temp += (rand - 0.5) * 0.5 # Small random changes
|
207
|
+
@battery_level -= 0.1 if rand < 0.1 # Occasional battery drain
|
208
|
+
|
209
|
+
# Publish sensor data
|
210
|
+
SensorDataMessage.new(
|
211
|
+
device_id: @device_id,
|
212
|
+
device_type: 'thermostat',
|
213
|
+
location: @location,
|
214
|
+
sensor_type: 'temperature',
|
215
|
+
value: @current_temp.round(1),
|
216
|
+
unit: 'celsius',
|
217
|
+
timestamp: Time.now.iso8601,
|
218
|
+
battery_level: @battery_level.round(1)
|
219
|
+
).publish
|
220
|
+
|
221
|
+
# Check for alerts
|
222
|
+
if @current_temp > 28.0
|
223
|
+
AlertMessage.new(
|
224
|
+
alert_id: "TEMP-#{Time.now.to_i}",
|
225
|
+
severity: 'high',
|
226
|
+
alert_type: 'high_temperature',
|
227
|
+
device_id: @device_id,
|
228
|
+
location: @location,
|
229
|
+
message: "Temperature too high: #{@current_temp.round(1)}°C",
|
230
|
+
timestamp: Time.now.iso8601,
|
231
|
+
requires_action: true
|
232
|
+
).publish
|
233
|
+
end
|
234
|
+
|
235
|
+
sleep(3 + rand(2)) # Publish every 3-5 seconds
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def stop_monitoring
|
241
|
+
@running = false
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Security Camera Device
|
246
|
+
class SecurityCamera
|
247
|
+
def initialize(device_id, location)
|
248
|
+
@device_id = device_id
|
249
|
+
@location = location
|
250
|
+
@motion_detected = false
|
251
|
+
@recording = false
|
252
|
+
@battery_level = 90 + rand(10)
|
253
|
+
@running = false
|
254
|
+
|
255
|
+
puts "📹 Security Camera #{@device_id} initialized in #{@location}"
|
256
|
+
|
257
|
+
DeviceCommandMessage.subscribe("SecurityCamera.handle_command")
|
258
|
+
end
|
259
|
+
|
260
|
+
def self.handle_command(message_header, message_payload)
|
261
|
+
command_data = JSON.parse(message_payload)
|
262
|
+
|
263
|
+
# Only process commands intended for cameras with our device ID
|
264
|
+
return unless command_data['device_id']&.start_with?('CAM-')
|
265
|
+
return unless ['start_recording', 'stop_recording', 'get_status'].include?(command_data['command'])
|
266
|
+
|
267
|
+
puts "📹 Camera #{command_data['device_id']} received command: #{command_data['command']}"
|
268
|
+
|
269
|
+
# Process the command
|
270
|
+
case command_data['command']
|
271
|
+
when 'start_recording'
|
272
|
+
duration = command_data.dig('parameters', 'duration') || 30
|
273
|
+
puts " Starting recording for #{duration} seconds"
|
274
|
+
when 'stop_recording'
|
275
|
+
puts " Stopping recording"
|
276
|
+
when 'get_status'
|
277
|
+
puts " Reporting camera status"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def start_monitoring
|
282
|
+
@running = true
|
283
|
+
|
284
|
+
Thread.new do
|
285
|
+
while @running
|
286
|
+
# Simulate motion detection
|
287
|
+
motion_detected = rand < 0.3 # 30% chance of motion
|
288
|
+
|
289
|
+
if motion_detected && !@motion_detected
|
290
|
+
@motion_detected = true
|
291
|
+
|
292
|
+
# Publish motion detection
|
293
|
+
SensorDataMessage.new(
|
294
|
+
device_id: @device_id,
|
295
|
+
device_type: 'security_camera',
|
296
|
+
location: @location,
|
297
|
+
sensor_type: 'motion',
|
298
|
+
value: true,
|
299
|
+
unit: 'boolean',
|
300
|
+
timestamp: Time.now.iso8601,
|
301
|
+
battery_level: @battery_level.round(1)
|
302
|
+
).publish
|
303
|
+
|
304
|
+
# Send security alert
|
305
|
+
AlertMessage.new(
|
306
|
+
alert_id: "MOTION-#{Time.now.to_i}",
|
307
|
+
severity: 'medium',
|
308
|
+
alert_type: 'motion_detected',
|
309
|
+
device_id: @device_id,
|
310
|
+
location: @location,
|
311
|
+
message: "Motion detected in #{@location}",
|
312
|
+
timestamp: Time.now.iso8601,
|
313
|
+
requires_action: false
|
314
|
+
).publish
|
315
|
+
|
316
|
+
# Auto-start recording
|
317
|
+
DeviceCommandMessage.new(
|
318
|
+
device_id: @device_id,
|
319
|
+
command: 'start_recording',
|
320
|
+
parameters: { duration: 30 },
|
321
|
+
requested_by: 'motion_detector',
|
322
|
+
timestamp: Time.now.iso8601
|
323
|
+
).publish
|
324
|
+
|
325
|
+
elsif !motion_detected && @motion_detected
|
326
|
+
@motion_detected = false
|
327
|
+
|
328
|
+
# Motion stopped
|
329
|
+
SensorDataMessage.new(
|
330
|
+
device_id: @device_id,
|
331
|
+
device_type: 'security_camera',
|
332
|
+
location: @location,
|
333
|
+
sensor_type: 'motion',
|
334
|
+
value: false,
|
335
|
+
unit: 'boolean',
|
336
|
+
timestamp: Time.now.iso8601,
|
337
|
+
battery_level: @battery_level.round(1)
|
338
|
+
).publish
|
339
|
+
end
|
340
|
+
|
341
|
+
@battery_level -= 0.05 if rand < 0.1
|
342
|
+
sleep(2 + rand(3)) # Check every 2-5 seconds
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def stop_monitoring
|
348
|
+
@running = false
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Smart Door Lock
|
353
|
+
class SmartDoorLock
|
354
|
+
def initialize(device_id, location)
|
355
|
+
@device_id = device_id
|
356
|
+
@location = location
|
357
|
+
@locked = true
|
358
|
+
@battery_level = 75 + rand(20)
|
359
|
+
@running = false
|
360
|
+
|
361
|
+
puts "🚪 Smart Door Lock #{@device_id} initialized at #{@location}"
|
362
|
+
|
363
|
+
DeviceCommandMessage.subscribe("SmartDoorLock.handle_command")
|
364
|
+
end
|
365
|
+
|
366
|
+
def self.handle_command(message_header, message_payload)
|
367
|
+
command_data = JSON.parse(message_payload)
|
368
|
+
|
369
|
+
# Only process commands intended for door locks with our device ID
|
370
|
+
return unless command_data['device_id']&.start_with?('LOCK-')
|
371
|
+
return unless ['lock', 'unlock', 'get_status'].include?(command_data['command'])
|
372
|
+
|
373
|
+
puts "🚪 Door Lock #{command_data['device_id']} received command: #{command_data['command']}"
|
374
|
+
|
375
|
+
# Process the command
|
376
|
+
case command_data['command']
|
377
|
+
when 'lock'
|
378
|
+
puts " Locking door"
|
379
|
+
when 'unlock'
|
380
|
+
puts " Unlocking door"
|
381
|
+
when 'get_status'
|
382
|
+
puts " Reporting lock status"
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def start_monitoring
|
387
|
+
@running = true
|
388
|
+
|
389
|
+
Thread.new do
|
390
|
+
while @running
|
391
|
+
# Periodically report status
|
392
|
+
SensorDataMessage.new(
|
393
|
+
device_id: @device_id,
|
394
|
+
device_type: 'door_lock',
|
395
|
+
location: @location,
|
396
|
+
sensor_type: 'status',
|
397
|
+
value: @locked ? 'locked' : 'unlocked',
|
398
|
+
unit: 'string',
|
399
|
+
timestamp: Time.now.iso8601,
|
400
|
+
battery_level: @battery_level.round(1)
|
401
|
+
).publish
|
402
|
+
|
403
|
+
# Check for low battery
|
404
|
+
if @battery_level < 20
|
405
|
+
AlertMessage.new(
|
406
|
+
alert_id: "BATTERY-#{Time.now.to_i}",
|
407
|
+
severity: 'medium',
|
408
|
+
alert_type: 'battery_low',
|
409
|
+
device_id: @device_id,
|
410
|
+
location: @location,
|
411
|
+
message: "Door lock battery low: #{@battery_level.round(1)}%",
|
412
|
+
timestamp: Time.now.iso8601,
|
413
|
+
requires_action: true
|
414
|
+
).publish
|
415
|
+
end
|
416
|
+
|
417
|
+
@battery_level -= 0.2 if rand < 0.1
|
418
|
+
sleep(8 + rand(4)) # Report every 8-12 seconds
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def stop_monitoring
|
424
|
+
@running = false
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# IoT Dashboard Service
|
429
|
+
class IoTDashboard
|
430
|
+
@@devices = {}
|
431
|
+
@@alerts = []
|
432
|
+
@@instance = nil
|
433
|
+
|
434
|
+
def initialize
|
435
|
+
@running = false
|
436
|
+
|
437
|
+
puts "📊 IoT Dashboard starting up..."
|
438
|
+
|
439
|
+
# Subscribe to all message types
|
440
|
+
SensorDataMessage.subscribe("IoTDashboard.handle_sensor_data")
|
441
|
+
AlertMessage.subscribe("IoTDashboard.handle_alert")
|
442
|
+
DeviceCommandMessage.subscribe("IoTDashboard.log_command")
|
443
|
+
end
|
444
|
+
|
445
|
+
def self.handle_sensor_data(message_header, message_payload)
|
446
|
+
@@instance ||= new
|
447
|
+
@@instance.process_sensor_data(message_header, message_payload)
|
448
|
+
end
|
449
|
+
|
450
|
+
def self.handle_alert(message_header, message_payload)
|
451
|
+
@@instance ||= new
|
452
|
+
@@instance.process_alert(message_header, message_payload)
|
453
|
+
end
|
454
|
+
|
455
|
+
def self.log_command(message_header, message_payload)
|
456
|
+
@@instance ||= new
|
457
|
+
@@instance.log_device_command(message_header, message_payload)
|
458
|
+
end
|
459
|
+
|
460
|
+
def process_sensor_data(message_header, message_payload)
|
461
|
+
data = JSON.parse(message_payload)
|
462
|
+
device_id = data['device_id']
|
463
|
+
|
464
|
+
@@devices[device_id] = {
|
465
|
+
device_type: data['device_type'],
|
466
|
+
location: data['location'],
|
467
|
+
last_seen: Time.now,
|
468
|
+
battery_level: data['battery_level'],
|
469
|
+
latest_reading: data
|
470
|
+
}
|
471
|
+
end
|
472
|
+
|
473
|
+
def process_alert(message_header, message_payload)
|
474
|
+
alert_data = JSON.parse(message_payload)
|
475
|
+
@@alerts << alert_data
|
476
|
+
@@alerts = @@alerts.last(10) # Keep only last 10 alerts
|
477
|
+
end
|
478
|
+
|
479
|
+
def log_device_command(message_header, message_payload)
|
480
|
+
command_data = JSON.parse(message_payload)
|
481
|
+
puts "📊 Dashboard logged command: #{command_data['command']} → #{command_data['device_id']}"
|
482
|
+
end
|
483
|
+
|
484
|
+
def start_status_updates
|
485
|
+
@running = true
|
486
|
+
|
487
|
+
Thread.new do
|
488
|
+
while @running
|
489
|
+
sleep(10) # Update every 10 seconds
|
490
|
+
|
491
|
+
active_devices = @@devices.count { |_, device| Time.now - device[:last_seen] < 30 }
|
492
|
+
offline_devices = @@devices.count { |_, device| Time.now - device[:last_seen] >= 30 }
|
493
|
+
recent_alerts = @@alerts.count { |alert| Time.parse(alert['timestamp']) > Time.now - 300 }
|
494
|
+
|
495
|
+
DashboardStatusMessage.new(
|
496
|
+
system_status: recent_alerts > 0 ? 'alert' : 'normal',
|
497
|
+
active_devices: active_devices,
|
498
|
+
offline_devices: offline_devices,
|
499
|
+
active_alerts: recent_alerts,
|
500
|
+
last_updated: Time.now.iso8601,
|
501
|
+
summary_stats: {
|
502
|
+
total_devices: @@devices.size,
|
503
|
+
avg_battery: @@devices.values.map { |d| d[:battery_level] || 100 }.sum / [@@devices.size, 1].max
|
504
|
+
}
|
505
|
+
).publish
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def stop_status_updates
|
511
|
+
@running = false
|
512
|
+
end
|
513
|
+
|
514
|
+
def print_summary
|
515
|
+
puts "\n" + "="*60
|
516
|
+
puts "📊 DASHBOARD SUMMARY"
|
517
|
+
puts "="*60
|
518
|
+
puts "Active Devices: #{@@devices.size}"
|
519
|
+
@@devices.each do |device_id, device|
|
520
|
+
status = Time.now - device[:last_seen] < 30 ? "🟢 Online" : "🔴 Offline"
|
521
|
+
puts " #{device_id} (#{device[:device_type]}) - #{status}"
|
522
|
+
end
|
523
|
+
puts "\nRecent Alerts: #{@@alerts.size}"
|
524
|
+
@@alerts.last(3).each do |alert|
|
525
|
+
puts " #{alert['severity'].upcase}: #{alert['message']}"
|
526
|
+
end
|
527
|
+
puts "="*60
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# Demo Runner for Redis Smart Home
|
532
|
+
class SmartHomeDemo
|
533
|
+
def run
|
534
|
+
puts "🚀 Starting Smart Home IoT Demo with Redis Transport\n"
|
535
|
+
|
536
|
+
# Start dashboard
|
537
|
+
dashboard = IoTDashboard.new
|
538
|
+
|
539
|
+
# Subscribe to all message types for logging
|
540
|
+
SensorDataMessage.subscribe
|
541
|
+
DeviceCommandMessage.subscribe
|
542
|
+
AlertMessage.subscribe
|
543
|
+
DashboardStatusMessage.subscribe
|
544
|
+
|
545
|
+
puts "\n" + "="*60
|
546
|
+
puts "Initializing Smart Home Devices"
|
547
|
+
puts "="*60
|
548
|
+
|
549
|
+
# Create devices
|
550
|
+
thermostat = SmartThermostat.new("THERM-001", "living_room")
|
551
|
+
camera = SecurityCamera.new("CAM-001", "front_door")
|
552
|
+
door_lock = SmartDoorLock.new("LOCK-001", "main_entrance")
|
553
|
+
|
554
|
+
puts "\n" + "="*60
|
555
|
+
puts "Starting Device Monitoring (Redis Pub/Sub Active)"
|
556
|
+
puts "="*60
|
557
|
+
|
558
|
+
# Start all monitoring
|
559
|
+
thermostat.start_monitoring
|
560
|
+
camera.start_monitoring
|
561
|
+
door_lock.start_monitoring
|
562
|
+
dashboard.start_status_updates
|
563
|
+
|
564
|
+
# Simulate some manual commands
|
565
|
+
puts "\n🎛️ Sending some manual commands..."
|
566
|
+
sleep(2)
|
567
|
+
|
568
|
+
DeviceCommandMessage.new(
|
569
|
+
device_id: "THERM-001",
|
570
|
+
command: "set_temperature",
|
571
|
+
parameters: { target: 24.0 },
|
572
|
+
requested_by: "mobile_app",
|
573
|
+
timestamp: Time.now.iso8601
|
574
|
+
).publish
|
575
|
+
|
576
|
+
sleep(3)
|
577
|
+
|
578
|
+
DeviceCommandMessage.new(
|
579
|
+
device_id: "CAM-001",
|
580
|
+
command: "start_recording",
|
581
|
+
parameters: { duration: 60 },
|
582
|
+
requested_by: "security_schedule",
|
583
|
+
timestamp: Time.now.iso8601
|
584
|
+
).publish
|
585
|
+
|
586
|
+
sleep(2)
|
587
|
+
|
588
|
+
DeviceCommandMessage.new(
|
589
|
+
device_id: "LOCK-001",
|
590
|
+
command: "get_status",
|
591
|
+
parameters: {},
|
592
|
+
requested_by: "mobile_app",
|
593
|
+
timestamp: Time.now.iso8601
|
594
|
+
).publish
|
595
|
+
|
596
|
+
puts "\n⏳ Monitoring for 30 seconds (watch the Redis channels!)..."
|
597
|
+
puts " Each message type uses its own Redis channel for efficient routing"
|
598
|
+
|
599
|
+
# Let it run for a while
|
600
|
+
sleep(30)
|
601
|
+
|
602
|
+
# Show dashboard summary
|
603
|
+
dashboard.print_summary
|
604
|
+
|
605
|
+
# Stop monitoring
|
606
|
+
puts "\n🛑 Stopping monitoring..."
|
607
|
+
thermostat.stop_monitoring
|
608
|
+
camera.stop_monitoring
|
609
|
+
door_lock.stop_monitoring
|
610
|
+
dashboard.stop_status_updates
|
611
|
+
|
612
|
+
# Final stats
|
613
|
+
puts "\n✨ Demo completed!"
|
614
|
+
|
615
|
+
transport_name = SHARED_TRANSPORT.class.name.include?('Redis') ? 'Redis' : 'Memory'
|
616
|
+
puts "\n📡 Transport used: #{transport_name}"
|
617
|
+
|
618
|
+
puts "\nThis example demonstrated:"
|
619
|
+
if SHARED_TRANSPORT.class.name.include?('Redis')
|
620
|
+
puts "• Redis pub/sub transport with automatic channel routing"
|
621
|
+
puts "• Multiple device types publishing to separate Redis channels:"
|
622
|
+
puts " - SensorDataMessage → 'SensorDataMessage' channel"
|
623
|
+
puts " - DeviceCommandMessage → 'DeviceCommandMessage' channel"
|
624
|
+
puts " - AlertMessage → 'AlertMessage' channel"
|
625
|
+
puts " - DashboardStatusMessage → 'DashboardStatusMessage' channel"
|
626
|
+
puts "• Real-time IoT device communication and monitoring"
|
627
|
+
puts "• Event-driven architecture with automatic message routing"
|
628
|
+
puts "• Scalable pub/sub pattern supporting multiple subscribers per channel"
|
629
|
+
puts "\n🔧 Redis Commands you could run to see the channels:"
|
630
|
+
puts " redis-cli -n 1 PUBSUB CHANNELS"
|
631
|
+
puts " redis-cli -n 1 MONITOR"
|
632
|
+
else
|
633
|
+
puts "• Memory transport with in-process message routing (Redis fallback)"
|
634
|
+
puts "• Multiple device types with simulated IoT communication"
|
635
|
+
puts "• Event-driven architecture and message processing"
|
636
|
+
puts "• Real-time device monitoring and alerting"
|
637
|
+
puts "\n💡 To see Redis transport in action:"
|
638
|
+
puts " 1. Install Redis: brew install redis (macOS) or apt install redis (Linux)"
|
639
|
+
puts " 2. Start Redis: redis-server"
|
640
|
+
puts " 3. Re-run this example"
|
641
|
+
end
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
# Run the demo if this file is executed directly
|
646
|
+
if __FILE__ == $0
|
647
|
+
demo = SmartHomeDemo.new
|
648
|
+
demo.run
|
649
|
+
end
|