Current section

Files

Jump to
grizzly lib grizzly zwave notifications.ex
Raw

lib/grizzly/zwave/notifications.ex

defmodule Grizzly.ZWave.Notifications do
@moduledoc false
alias Grizzly.ZWave.Commands.{NodeLocationReport, UserCodeReport}
alias Grizzly.ZWave.CommandClasses.{NodeNaming, UserCode}
alias Grizzly.ZWave.{DecodeError, Decoder}
require Logger
@type event :: atom()
@type type :: atom()
@type zwave_alarm_status :: :deactivate | :activate
@type status :: :enabled | :disabled
table = %{
{0x01, :smoke_alarm} => %{
0x00 => :state_idle,
0x01 => :smoke_detected_location_provided,
0x02 => :smoked_detected,
0x03 => :smoke_alarm_test,
0x04 => :replacement_required,
0x05 => :replacement_required_end_of_life,
0x06 => :alarm_silenced,
0x07 => :maintenance_required_planned_periodic_inspection,
0x08 => :maintenance_required_dust_in_device,
0xFE => :unknown
},
{0x02, :co_alarm} => %{
0x00 => :state_idle,
0x01 => :co_detected_location_provided,
0x02 => :co_detected,
0x03 => :co_test,
0x04 => :replacement_required,
0x05 => :replacement_required_end_of_life,
0x06 => :alarm_silenced,
0x07 => :maintenance_required_planned_periodic_inspection,
0xFE => :unknown
},
{0x03, :co2_alarm} => %{
0x00 => :state_idle,
0x01 => :co2_detected_location_provided,
0x02 => :co2_detected,
0x03 => :co2_test,
0x04 => :replacement_required,
0x05 => :replacement_required_end_of_life,
0x06 => :alarm_silenced,
0x07 => :maintenance_required_planned_periodic_inspection,
0xFE => :unknown
},
{0x04, :heat_alarm} => %{
0x00 => :state_idle,
0x01 => :overheat_detected_location_provided,
0x02 => :overheat_detected,
0x03 => :rapid_temperature_rise_location_provided,
0x04 => :rapid_temperature_rise,
0x05 => :underheat_detected_location_provided,
0x06 => :underheat_detected,
0x07 => :heat_alarm_test,
0x08 => :replacement_required_end_of_life,
0x09 => :alarm_silenced,
0x0A => :maintenance_required_dust_in_device,
0x0B => :maintenance_required_planned_periodic_inspection,
0x0C => :rapid_temperature_fall_location_provided,
0x0D => :rapid_temperature_fall,
0xFE => :unknown
},
{0x05, :water_alarm} => %{
0x00 => :state_idle,
0x01 => :water_leak_detected_location_provided,
0x02 => :water_leak_detected,
0x03 => :water_level_dropped_location_provided,
0x04 => :water_level_dropped,
0x05 => :replace_water_filter,
0x06 => :water_flow_alarm,
0x07 => :water_pressure_alarm,
0x08 => :water_temperature_alarm,
0x09 => :water_level_alarm,
0x0A => :sump_pump_active,
0x0B => :sump_pump_failure,
0xFE => :unknown
},
{0x06, :access_control} => %{
0x00 => :state_idle,
0x01 => :manual_lock_operation,
0x02 => :manual_unlock_operation,
0x03 => :rf_lock_operation,
0x04 => :rf_unlock_operation,
0x05 => :keypad_lock_operation,
0x06 => :keypad_unlock_operation,
0x07 => :manual_not_fully_locked_operation,
0x08 => :rf_not_fully_locked_operation,
0x09 => :auto_lock_locked_operation,
0x0A => :auto_lock_not_fully_locked_operation,
0x0B => :lock_jammed,
0x0C => :all_user_codes_deleted,
0x0D => :single_user_code_deleted,
0x0E => :new_user_code_added,
0x0F => :new_user_code_not_added_duplicate,
0x10 => :keypad_temporarily_disabled,
0x11 => :keypad_busy,
0x12 => :new_program_code_entered,
0x13 => :manually_enter_user_code_exceeds_code_limit,
0x14 => :unlock_by_rf_with_invalid_user_code,
0x15 => :locked_by_rf_with_invalid_user_code,
0x16 => :window_or_door_is_open,
0x17 => :window_or_door_is_closed,
0x18 => :window_or_door_handle_is_open,
0x19 => :window_or_door_handle_is_closed,
0x20 => :messaging_user_code_entered_via_keypad,
0x21 => :lock_operation_with_user_code,
0x22 => :unlock_operation_with_user_code,
0x23 => :credential_lock_operation,
0x24 => :credential_unlock_operation,
0x25 => :all_users_deleted,
0x26 => :multiple_credentials_deleted,
0x27 => :user_added,
0x28 => :user_modified,
0x29 => :user_deleted,
0x2A => :user_unchanged,
0x2B => :credential_added,
0x2C => :credential_modified,
0x2D => :credential_deleted,
0x2E => :credential_unchanged,
0x2F => :valid_credential_denied_user_disabled,
0x30 => :valid_credential_denied_user_schedule_not_active,
0x31 => :user_access_denied_too_few_credentials,
0x32 => :invalid_credential,
0x33 => :non_access_credential_used,
0x40 => :barrier_performing_initialization_process,
0x41 => :barrier_operation_force_exceeded,
0x42 => :barrier_operation_exceeded_time_limit,
0x43 => :barrier_operation_exceeded_mechanical_limit,
0x44 => :barrier_unable_to_perform_operation_ul_requirements,
0x45 => :barrier_unattended_operation_disabled_ul_requirements,
0x46 => :barrier_failed_operation_malfunction,
0x47 => :barrier_vacation_mode,
0x48 => :barrier_safety_beam_obstacle,
0x49 => :barrier_sensor_not_detected,
0x4A => :barrier_sensor_low_battery,
0x4B => :barrier_short_in_wall_station_wires,
0x4C => :barrier_associated_with_non_zwave_remote,
0xFE => :unknown
},
{0x07, :home_security} => %{
0x00 => :state_idle,
0x01 => :intrusion_location_provided,
0x02 => :intrusion,
0x03 => :tampering_product_cover_removed,
0x04 => :tampering_invalid_code,
0x05 => :glass_breakage_location_provided,
0x06 => :glass_breakage,
0x07 => :motion_detection_location_provided,
0x08 => :motion_detection,
0x09 => :tampering_product_moved,
0x0A => :impact_detected,
0x0B => :magnetic_field_interface_detected,
0x0C => :rf_jamming_detected,
0xFE => :unknown
},
{0x08, :power_management} => %{
0x00 => :state_idle,
0x01 => :power_applied,
0x02 => :ac_mains_disconnected,
0x03 => :ac_mains_reconnected,
0x04 => :surge_detected,
0x05 => :voltage_drop,
0x06 => :over_current_detected,
0x07 => :over_voltage_detected,
0x08 => :over_load_detected,
0x09 => :load_error,
0x0A => :replace_battery_soon,
0x0B => :replace_battery_now,
0x0C => :battery_is_charging,
0x0D => :battery_is_fully_charged,
0x0E => :charge_battery_soon,
0x0F => :charge_battery_now,
0x10 => :back_up_battery_low,
0x11 => :battery_fluid_is_low,
0x12 => :back_up_battery_disconnected,
0x13 => :dc_connected,
0x14 => :dc_disconnected,
0xFE => :unknown
},
{0x09, :system} => %{
0x00 => :state_idle,
0x01 => :system_hardware_failure,
0x02 => :system_software_failure,
0x03 => :system_hardware_failure_code_provided,
0x04 => :system_software_failure_code_provided,
0x05 => :heartbeat,
0x06 => :tampering_product_cover_removed,
0x07 => :emergency_shutoff,
0x08 => :undefined,
0x09 => :digital_input_high_state,
0x0A => :digital_input_low_state,
0x0B => :digital_input_open,
0xFE => :unknown
},
{0x0A, :emergency_alarm} => %{
0x00 => :state_idle,
0x01 => :contact_police,
0x02 => :contact_fire_service,
0x03 => :contact_medical_service,
0x04 => :panic_alert,
0xFE => :unknown
},
{0x0B, :clock} => %{
0x00 => :state_idle,
0x01 => :wake_up_alert,
0x02 => :timer_ended,
0x03 => :time_remaining,
0xFE => :unknown
},
{0x0C, :appliance} => %{
0x00 => :state_idle,
0x01 => :program_started,
0x02 => :program_in_progress,
0x03 => :program_completed,
0x04 => :replace_main_filter,
0x05 => :failure_to_set_target_temperature,
0x06 => :supplying_water,
0x07 => :water_supply_failure,
0x08 => :boiling,
0x09 => :boiling_failure,
0x0A => :washing,
0x0B => :washing_failure,
0x0C => :rinsing,
0x0D => :rinsing_failure,
0x0E => :draining,
0x0F => :draining_failure,
0x10 => :spinning,
0x11 => :spinning_failure,
0x12 => :drying,
0x13 => :drying_failure,
0x14 => :fan_failure,
0x15 => :compressor_failure,
0xFE => :unknown
},
{0x0D, :home_health} => %{
0x00 => :state_idle,
0x01 => :leaving_bed,
0x02 => :sitting_on_bed,
0x03 => :lying_on_bed,
0x04 => :posture_changed,
0x05 => :sitting_on_bed_edge,
0x06 => :volatile_organic_compound_level,
0x07 => :sleep_apnea_detected,
0x08 => :sleep_stage_0_detected,
0x09 => :sleep_stage_1_detected,
0x0A => :sleep_stage_2_detected,
0x0B => :sleep_stage_3_detected,
0x0C => :fall_detected,
0xFE => :unknown
},
{0x0E, :siren} => %{
0x00 => :state_idle,
0x01 => :state_active,
0xFE => :unknown
},
{0x0F, :water_valve} => %{
0x00 => :state_idle,
0x01 => :valve_operation,
0x02 => :main_valve_operation,
0x03 => :valve_short_circuit,
0x04 => :main_valve_short_circuit,
0x05 => :valve_current_alarm,
0x06 => :main_current_alarm,
0x07 => :valve_jammed,
0xFE => :unknown
},
{0x10, :weather_alarm} => %{
0x00 => :state_idle,
0x01 => :rain_alarm,
0x02 => :moisture_alarm,
0x03 => :freeze_alarm,
0xFE => :unknown
},
{0x11, :irrigation} => %{
0x00 => :state_idle,
0x01 => :schedule_started,
0x02 => :schedule_finished,
0x03 => :valve_table_run_started,
0x04 => :valve_table_run_finished,
0x05 => :device_not_configured,
0xFE => :unknown
},
{0x12, :gas_alarm} => %{
0x00 => :state_idle,
0x01 => :combustible_gas_detected_location_provided,
0x02 => :combustible_gas_detected,
0x03 => :toxic_gas_detected_location_provided,
0x04 => :toxic_gas_detected,
0x05 => :gas_alarm_test,
0x06 => :replacement_required,
0xFE => :unknown
},
{0x13, :pest_contol} => %{
0x00 => :state_idle,
0x01 => :trap_armed_location_provided,
0x02 => :trap_armed,
0x03 => :trap_rearm_required_location_provided,
0x04 => :trap_rearm_required,
0x05 => :pest_detected_location_provided,
0x06 => :pest_detected,
0x07 => :pest_exterminated_location_provided,
0x08 => :pest_exterminated,
0xFE => :unknown
},
{0x14, :light_sensor} => %{
0x00 => :state_idle,
0x01 => :light_detected,
0x02 => :light_color_transition_detected,
0xFE => :unknown
},
{0x15, :water_quality_monitoring} => %{
0x00 => :state_idle,
0x01 => :chlorine_alarm,
0x02 => :acidity_alarm,
0x03 => :water_oxidation_alarm,
0x04 => :chlorine_empty,
0x05 => :acidity_empty,
0x06 => :waterflow_measuring_station_shortage_detected,
0x07 => :waterflow_clear_water_shortage_detected,
0x08 => :disinfection_system_error_detected,
0x09 => :filter_cleaning_ongoing,
0x0A => :heating_operation_ongoing,
0x0B => :filter_pump_operation_ongoing,
0x0C => :freshwater_operation_ongoing,
0x0D => :dry_protection_operation_active,
0x0E => :water_tank_empty,
0x0F => :water_tank_level_unknown,
0x10 => :water_tank_full,
0x11 => :collective_disorder,
0xFE => :unknown
},
{0x16, :home_monitoring} => %{
0x00 => :state_idle,
0x01 => :home_occupied_location_provided,
0x02 => :home_occupied,
0xFE => :unknown
}
}
@type_from_byte table |> Map.keys() |> Map.new()
@type_to_byte table |> Map.keys() |> Map.new(fn {byte, type} -> {type, byte} end)
@event_from_byte Map.new(table, fn {{_, type}, events} ->
{type, Map.new(events, fn {byte, event} -> {byte, event} end)}
end)
@event_to_byte Map.new(table, fn {{_, type}, events} ->
{type, Map.new(events, fn {byte, event} -> {event, byte} end)}
end)
@doc """
Encode a notification type.
"""
@spec type_to_byte(type()) :: byte()
def type_to_byte(type) do
case Map.fetch(@type_to_byte, type) do
{:ok, byte} -> byte
:error -> raise KeyError, "Unknown notification type: #{inspect(type)}"
end
end
@doc """
Encode a notification event.
"""
@spec event_to_byte(type(), event()) :: byte()
def event_to_byte(type, event) do
with nil <- get_in(@event_to_byte, [type, event]) do
raise KeyError, "Unknown notification event: #{inspect(type)} / #{inspect(event)}"
end
end
@doc """
Decode a notification type.
"""
@spec type_from_byte(byte()) :: {:ok, type()} | {:error, :invalid_type_byte}
def type_from_byte(byte) do
case Map.fetch(@type_from_byte, byte) do
{:ok, type} -> {:ok, type}
:error -> {:error, :invalid_type_byte}
end
end
@doc """
Decode a notification event.
"""
@spec event_from_byte(type(), byte()) :: {:ok, event()} | {:error, :invalid_event_byte}
def event_from_byte(type, byte) do
with v when not is_nil(v) <- get_in(@event_from_byte, [type, byte]) do
{:ok, v}
else
_ -> {:error, :invalid_event_byte}
end
end
@spec status_to_byte(status()) :: 0x00 | 0xFF
def status_to_byte(:disabled), do: 0x00
def status_to_byte(:enabled), do: 0xFF
@spec status_from_byte(byte()) :: {:ok, status()} | {:error, :invalid_status_byte}
def status_from_byte(0x00), do: {:ok, :disabled}
def status_from_byte(0xFF), do: {:ok, :enabled}
def status_from_byte(_byte), do: {:error, :invalid_status_byte}
def encode_event_params(_zwave_type, _zwave_event, []), do: <<>>
def encode_event_params(:access_control, zwave_event, event_params_list)
when zwave_event in [:keypad_lock_operation, :keypad_unlock_operation, :new_user_code_added] do
{:ok, user_code_report} = UserCodeReport.new(event_params_list)
<<UserCode.byte(), user_code_report.command_byte>> <>
UserCodeReport.encode_params(user_code_report)
end
def encode_event_params(:home_security, zwave_event, event_params_list)
when zwave_event in [
:intrusion_location_provided,
:glass_breakage_location_provided,
:motion_detection_location_provided
] do
{:ok, node_location_report} = NodeLocationReport.new(event_params_list)
<<NodeNaming.byte(), node_location_report.command_byte>> <>
NodeLocationReport.encode_params(node_location_report)
end
def encode_event_params(:smoke_alarm, :smoke_detected_location_provided, event_params_list) do
{:ok, node_location_report} = NodeLocationReport.new(event_params_list)
<<NodeNaming.byte(), node_location_report.command_byte>> <>
NodeLocationReport.encode_params(node_location_report)
end
def encode_event_params(:water_alarm, zwave_event, event_params_list)
when zwave_event in [
:water_leak_detected_location_provided,
:water_level_droppped_location_provided
] do
{:ok, node_location_report} = NodeLocationReport.new(event_params_list)
<<NodeNaming.byte(), node_location_report.command_byte>> <>
NodeLocationReport.encode_params(node_location_report)
end
def encode_event_params(zwave_type, zwave_event, event_params) do
Logger.info(
"[Grizzly] Encoding not implemented for event params #{inspect(event_params)} for zwave_type #{inspect(zwave_type)} and zwave_event #{inspect(zwave_event)}"
)
<<>>
end
def decode_event_params(zwave_type, zwave_event, <<>>) do
Logger.info(
"[Grizzly] No event parameters for #{inspect(zwave_type)} #{inspect(zwave_event)}"
)
{:ok, []}
end
# Some locks do not encode an encapsulated UserCodeReport as event parameters; they only give the user id
def decode_event_params(:access_control, zwave_event, <<user_id>>)
when zwave_event in [:keypad_lock_operation, :keypad_unlock_operation] do
{:ok,
[
user_id: user_id,
user_id_status: :status_not_available,
user_code: ""
]}
end
def decode_event_params(:access_control, zwave_event, params_binary)
when zwave_event in [:keypad_lock_operation, :keypad_unlock_operation] do
with {:ok, user_code_report} <- Decoder.from_binary(params_binary) do
{:ok, user_code_report.params}
else
{:error, %DecodeError{}} = decode_error ->
Logger.warning("[Grizzly] Failed to decode UserCodeReport from #{inspect(params_binary)}")
decode_error
end
end
def decode_event_params(:home_security, zwave_event, params_binary)
when zwave_event in [
:intrusion_location_provided,
:glass_breakage_location_provided,
:motion_detection_location_provided
] do
with {:ok, node_location_report} <- Decoder.from_binary(params_binary) do
{:ok, node_location_report.params}
else
{:error, %DecodeError{}} = decode_error ->
Logger.warning(
"[Grizzly] Failed to decode NodeLocationReport from #{inspect(params_binary)}"
)
decode_error
end
end
def decode_event_params(notification_type, :state_idle, <<byte::8>>) do
case event_from_byte(notification_type, byte) do
{:ok, event} ->
{:ok, [state: event]}
{:error, :invalid_event_byte} ->
Logger.warning(
"[Grizzly] Failed to decode state variable from #{inspect(notification_type)} state_idle event"
)
{:ok, []}
end
end
def decode_event_params(:water_alarm, zwave_event, params_binary)
when zwave_event in [
:water_leak_detected_location_provided,
:water_level_droppped_location_provided
] do
with {:ok, node_location_report} <- Decoder.from_binary(params_binary) do
{:ok, node_location_report.params}
else
{:error, %DecodeError{}} = decode_error ->
Logger.warning(
"[Grizzly] Failed to decode NodeLocationReport from #{inspect(params_binary)}"
)
decode_error
end
end
def decode_event_params(:smoke_alarm, :smoke_detected_location_provided, params_binary) do
with {:ok, node_location_report} <- Decoder.from_binary(params_binary) do
{:ok, node_location_report.params}
else
{:error, %DecodeError{}} = decode_error ->
Logger.warning(
"[Grizzly] Failed to decode NodeLocationReport from #{inspect(params_binary)}"
)
decode_error
end
end
def decode_event_params(zwave_type, zwave_event, params_binary) do
Logger.info(
"Decoding not implemented for event params #{inspect(params_binary)} for zwave_type #{inspect(zwave_type)} and zwave_event #{inspect(zwave_event)}"
)
{:ok, []}
end
end