From 03b336be43c0133741718913d935d46f3628f405 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Mon, 4 Sep 2023 17:17:55 +1000 Subject: [PATCH] feat(meraki/mqtt): add line crossing counts --- drivers/cisco/meraki/mqtt.cr | 85 +++++++++++++++++++++++++++++ drivers/cisco/meraki/mqtt_models.cr | 24 ++++++++ drivers/cisco/meraki/mqtt_spec.cr | 30 ++++++++++ 3 files changed, 139 insertions(+) diff --git a/drivers/cisco/meraki/mqtt.cr b/drivers/cisco/meraki/mqtt.cr index 5cfac1ce7b..d4396d9b88 100644 --- a/drivers/cisco/meraki/mqtt.cr +++ b/drivers/cisco/meraki/mqtt.cr @@ -33,6 +33,10 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver building_id: "zone-456", }, ], + + line_crossing_combined: { + area_name: ["camera_serial1", "camera_serial2"], + }, }) SUBS = { @@ -47,6 +51,9 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver # Number of entrances in the camera’s complete field of view # {ts: unix_time, counts: {person: number, vehicle: number}} "/merakimv/+/0", + + # meraki entry and exist monitoring + "/merakimv/+/crossing/+", } @keep_alive : Int32 = 60 @@ -61,6 +68,12 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver @floor_lookup : Hash(String, FloorMapping) = {} of String => FloorMapping + # area name => array of serials + @line_crossing : Hash(String, Array(String)) = {} of String => Array(String) + + # serial => area name + @crossing_lookup : Hash(String, String) = {} of String => String + def on_load @sub_proc = Proc(String, Bytes, Nil).new { |key, payload| on_message(key, payload) } on_update @@ -93,6 +106,13 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver existing = @subs @subs = SUBS.to_a + @line_crossing = line_crossing_combined = setting?(Hash(String, Array(String)), :line_crossing_combined) || {} of String => Array(String) + line_crossing_mapping = {} of String => String + line_crossing_combined.each do |name, serials| + serials.each { |serial| line_crossing_mapping[serial] = name } + end + @crossing_lookup = line_crossing_mapping + schedule.clear schedule.every((@keep_alive // 3).seconds) { ping } @@ -154,6 +174,15 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver end end + # Serial => count + getter crossing_people : Hash(String, Tuple(Int32, Int64)) do + Hash(String, Tuple(Int32, Int64)).new { |hash, key| hash[key] = {0, 0_i64} } + end + + getter crossing_vehicle : Hash(String, Tuple(Int32, Int64)) do + Hash(String, Tuple(Int32, Int64)).new { |hash, key| hash[key] = {0, 0_i64} } + end + getter lux : Hash(String, Tuple(Float64, Int64)) = {} of String => Tuple(Float64, Int64) # this is where we do all of the MQTT message processing @@ -174,6 +203,20 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver light = LuxLevel.from_json(json_message) lux[serial_no] = {light.lux, light.timestamp} self["camera_#{serial_no}_lux"] = light.lux + when "crossing" + crossing = Crossing.from_json(json_message) + count_hash = crossing.type.person? ? crossing_people : crossing_vehicle + lookup_name = @crossing_lookup[serial_no]? || serial_no + current_count, _timestamp = count_hash[lookup_name] + case crossing.event + when .crossing_in? + current_count += 1 + when .crossing_out? + current_count -= 1 + end + current_count = 0 if current_count < 0 + count_hash[lookup_name] = {current_count, crossing.timestamp} + self["camera_mvx-#{serial_no}_#{crossing.type.to_s.downcase}"] = current_count else # Everything else is a zone count entry = Entrances.from_json json_message @@ -250,10 +293,14 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver add_lux_values(sensors, mac, serial_filter) add_people_counts(sensors, mac, serial_filter) add_vehicle_counts(sensors, mac, serial_filter) + add_people_crossing(sensors, mac, serial_filter) + add_vehicle_crossing(sensors, mac, serial_filter) when .people_count? add_people_counts(sensors, mac, serial_filter) + add_people_crossing(sensors, mac, serial_filter) when .counter? add_vehicle_counts(sensors, mac, serial_filter) + add_vehicle_crossing(sensors, mac, serial_filter) when .illuminance? add_lux_values(sensors, mac, serial_filter) else @@ -290,6 +337,44 @@ class Cisco::Meraki::MQTT < PlaceOS::Driver sensors end + protected def add_people_crossing(sensors, mac : String? = nil, serial_filter : Array(String)? = nil) + if mac + return sensors unless mac.starts_with?("mvx-") + mac = mac[4..-1] + + if data = crossing_people[mac]? + count, time = data + sensors << to_sensor(SensorType::PeopleCount, "mvx-#{mac}", "person", count, time) + end + else + crossing_people.each do |mac, (count, time)| + serial = @line_crossing[mac]?.try(&.first?) || mac + next if serial_filter && !serial_filter.includes?(serial) + sensors << to_sensor(SensorType::PeopleCount, "mvx-#{mac}", "person", count, time) + end + end + sensors + end + + protected def add_vehicle_crossing(sensors, mac : String? = nil, serial_filter : Array(String)? = nil) + if mac + return sensors unless mac.starts_with?("mvx-") + mac = mac[4..-1] + + if data = crossing_vehicle[mac]? + count, time = data + sensors << to_sensor(SensorType::Counter, "mvx-#{mac}", "vehicle", count, time) + end + else + crossing_vehicle.each do |mac, (count, time)| + serial = @line_crossing[mac]?.try(&.first?) || mac + next if serial_filter && !serial_filter.includes?(serial) + sensors << to_sensor(SensorType::Counter, "mvx-#{mac}", "vehicle", count, time) + end + end + sensors + end + protected def add_lux_values(sensors, mac : String? = nil, serial_filter : Array(String)? = nil) if mac return sensors if serial_filter && !serial_filter.includes?(mac) diff --git a/drivers/cisco/meraki/mqtt_models.cr b/drivers/cisco/meraki/mqtt_models.cr index b10de7d052..395e322f71 100644 --- a/drivers/cisco/meraki/mqtt_models.cr +++ b/drivers/cisco/meraki/mqtt_models.cr @@ -71,4 +71,28 @@ Float64 # occupancy @[JSON::Field(ignore: true)] getter count : Int32 { counts[:person] || counts[:vehicle] || 0 } end + + enum CrossingObject + Person + Vehicle + Unknown + end + + enum CrossingEvent + CrossingIn + CrossingOut + Expired + Appeared + end + + struct Crossing + include JSON::Serializable + + @[JSON::Field(key: "ts")] + getter timestamp : Int64 + # getter object_id : Int64 + getter label : String? + getter event : CrossingEvent + getter type : CrossingObject + end end diff --git a/drivers/cisco/meraki/mqtt_spec.cr b/drivers/cisco/meraki/mqtt_spec.cr index 8692f91d7b..0f8bd3d439 100644 --- a/drivers/cisco/meraki/mqtt_spec.cr +++ b/drivers/cisco/meraki/mqtt_spec.cr @@ -146,4 +146,34 @@ DriverSpecs.mock_driver "Place::MQTT" do [137, 542, 162, 573, 189, 597, 0], ], }) + + # ============================ + # Check Line Crossing + # ============================ + puts "===== REMOTE PUBLISH =====" + publish = MQTT::V3::Publish.new + publish.id = MQTT::RequestType::Publish + publish.message_id = 8_u16 + publish.topic = "/merakimv/56789/crossing/uuid" + publish.payload = %({"label":"testing","event":"crossing_in","type":"person","ts":1642564558,"object_id":2}) + publish.packet_length = publish.calculate_length + + transmit publish.to_slice + sleep 0.1 # wait a bit for processing + status["camera_mvx-56789_person"].should eq(1) + + exec(:sensors, "people_count", "mvx-56789").get.should eq([ + { + "status" => "normal", + "type" => "people_count", + "value" => 1.0, + "last_seen" => 1642564558, + "mac" => "mvx-56789", + "id" => "person", + "name" => "Meraki Camera mvx-56789: person", + "module_id" => "spec_runner", + "binding" => "camera_mvx-56789_person", + "location" => "sensor", + }, + ]) end