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.
@@ -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