Packages
grizzly
8.15.0
9.1.4
9.1.2
9.1.1
9.1.0
9.0.0
8.15.3
8.15.2
8.15.1
8.15.0
8.14.0
8.13.0
8.12.0
8.11.3
8.11.2
8.11.1
8.11.0
8.10.0
8.9.0
8.8.1
8.8.0
8.7.1
8.7.0
8.6.12
8.6.11
8.6.10
8.6.9
8.6.8
8.6.7
retired
8.6.6
8.6.5
8.6.4
8.6.3
8.6.2
8.6.1
8.6.0
8.5.3
8.5.2
8.5.1
8.5.0
8.4.0
8.3.0
8.2.3
8.2.2
8.2.1
8.2.0
8.1.0
8.0.1
8.0.0
7.4.3
7.4.2
7.4.1
7.4.0
7.3.0
7.2.0
7.1.4
7.1.3
7.1.2
7.1.1
7.1.0
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.8.8
6.8.7
6.8.6
6.8.5
6.8.4
6.8.3
6.8.2
6.8.1
6.8.0
6.7.1
6.7.0
6.6.1
6.6.0
6.5.1
6.5.0
6.4.0
6.3.0
6.2.0
6.1.1
6.1.0
6.0.1
6.0.0
5.4.1
5.4.0
5.3.0
5.2.8
5.2.7
5.2.6
5.2.5
5.2.4
5.2.3
5.2.2
5.2.1
5.2.0
5.1.2
5.1.1
5.1.0
5.0.2
5.0.1
5.0.0
4.0.1
4.0.0
3.0.0
2.1.0
2.0.0
1.0.1
1.0.0
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.9.0-rc.4
0.9.0-rc.3
0.9.0-rc.2
0.9.0-rc.1
0.9.0-rc.0
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
Elixir Z-Wave library
Current section
Files
Jump to
Current section
Files
lib/grizzly/zipgateway/database.ex
defmodule Grizzly.ZIPGateway.Database do
@moduledoc """
Functions for inspecting and debugging Z/IP Gateway's SQLite database.
"""
alias Exqlite.Sqlite3
alias Grizzly.ZWave.{CommandClasses, DeviceClasses}
alias Grizzly.ZWave.DSK
import Bitwise
require Logger
@type query_result(result) :: {:ok, result} | {:error, Sqlite3.reason()}
@type security_flag ::
{:s0 | :s2_unauthenticated | :s2_authenticated | :s2_access_control | :known_bad,
boolean()}
@type probe_state() :: :ok | :never_started | :probe_started | :probe_failed
@type node_state() ::
:created
| :probe_node_info
| :probe_product_id
| :enumerate_endpoints
| :find_endpoints
| :check_wakeup_cc_version
| :get_wakeup_capabilities
| :set_wakeup_interval
| :assign_return_route
| :probe_wakeup_interval
| :probe_endpoints
| :mdns_probe
| :mdns_endpoint_probe
| :done
| :probe_fail
| :failing
@type node_mode ::
:not_probed
| :nonlistening
| :always_listening
| :flirs
| :wakeup
| :wakeup_firmware_upgrade
@type mode_flag ::
{:mode, node_mode() | :unknown} | {:deleted | :failed | :low_battery, boolean()}
@type properties_flag ::
{:portable, boolean()} | {:just_added, String.t()} | {:added_by, String.t()}
@type version_capability ::
:version_get | :version_command_class_get | :version_zwave_software_get
@type zwave_node :: %{
id: Grizzly.zwave_node_id(),
dsk: Grizzly.ZWave.DSK.t() | nil,
last_awake: pos_integer(),
last_update: pos_integer(),
manufacturer_id: 0..0xFFFF,
product_type: 0..0xFFFF,
product_id: 0..0xFFFF,
mode: [mode_flag()],
security_flags: [security_flag()],
properties_flags: [properties_flag()],
probe_state: probe_state(),
state: node_state(),
version_capabilities: [{version_capability(), boolean()}],
basic_device_class: DeviceClasses.basic_device_class() | nil,
wake_up_interval: non_neg_integer() | nil
}
@type endpoint :: %{
id: non_neg_integer(),
node_id: Grizzly.zwave_node_id(),
generic_device_class: DeviceClasses.generic_device_class() | nil,
specific_device_class: DeviceClasses.specific_device_class() | nil,
command_classes: CommandClasses.command_class_list()
}
@type listening_mode :: :always_listening | :frequently_listening | :wakeup | :unknown
@doc """
Opens a SQLite database and passes the connection handle to the given function,
then closes the database on completion.
If the first argument is a reference to an already-open SQLite database, the
first and last steps will be skipped. This is mostly useful for testing.
If the given database file does not exist, an error will be returned. Otherwise,
the return value will be the result of the given function.
Z/IP Gateway should be stopped prior to making any modifications to the database,
otherwise it will ignore and most likely overwrite your changes. This function
will return an error if Z/IP Gateway is running and the first argument. If you
want to bypass this check, open the database and pass the connection handle.
"""
@spec with_database(Path.t() | reference(), (Sqlite3.db() -> any())) :: any()
def with_database(db, fun) when is_reference(db), do: fun.(db)
def with_database(db_path, fun) when is_binary(db_path) do
if File.exists?(db_path) do
with {:ok, db} <- Sqlite3.open(db_path) do
try do
with_database(db, fun)
after
Sqlite3.close(db)
end
end
else
{:error, "Database file not found: #{db_path}"}
end
end
@doc """
Same as `with_database/2`, but uses the database file pointed to by `Grizzly.options/0`.
"""
@spec with_database((Sqlite3.db() -> any())) :: any()
def with_database(fun), do: with_database(Grizzly.options().database_file, fun)
@doc """
Looks up a node by its ID and returns a map or nil if not found.
"""
@spec get_node(Sqlite3.db(), integer()) :: query_result(zwave_node() | nil)
def get_node(db, node_id) do
with {:ok, result} when not is_nil(result) <-
select_one(db, "SELECT * FROM nodes WHERE nodeid = ?", [node_id]) do
{:ok, decode_node_record(result)}
end
end
@doc """
Returns a list of all nodes.
"""
@spec all_nodes(Sqlite3.db()) :: query_result([zwave_node()])
def all_nodes(db) do
with {:ok, results} <- select_all(db, "SELECT * FROM nodes") do
{:ok, Enum.map(results, &decode_node_record/1)}
end
end
@doc """
Returns a list of all endpoints.
"""
@spec all_endpoints(Sqlite3.db()) :: query_result([endpoint()])
def all_endpoints(db) do
with {:ok, results} <- select_all(db, "SELECT * FROM endpoints") do
{:ok, Enum.map(results, &decode_endpoint_record/1)}
end
end
@doc """
Returns a list of the given node's endpoints.
"""
@spec get_node_endpoints(Sqlite3.db(), integer()) :: query_result([map()])
def get_node_endpoints(db, node_id),
do: select_all(db, "SELECT * FROM endpoints WHERE nodeid = ?", [node_id])
@doc """
Looks up a endpoint by node id and endpoint id and returns a map or nil if not found.
Returns endpoint 0 (the root device) if no endpoint_id is specified.
"""
@spec get_endpoint(Sqlite3.db(), pos_integer(), non_neg_integer()) :: query_result(map() | nil)
def get_endpoint(db, node_id, endpoint_id \\ 0),
do:
select_one(db, "SELECT * FROM endpoints WHERE nodeid = ? AND endpointid = ?", [
node_id,
endpoint_id
])
@missing_endpoints_query """
SELECT n.*
FROM nodes n
LEFT JOIN endpoints e ON n.nodeid = e.nodeid
WHERE e.nodeid IS NULL
AND n.nodeid != 1
"""
@doc """
Returns a list of nodes that have don't have any entries in the endpoints table.
"""
@spec find_nodes_with_no_endpoints(Sqlite3.db()) :: query_result([map()])
def find_nodes_with_no_endpoints(db), do: select_all(db, @missing_endpoints_query)
@doc """
Deletes a node (and its endpoints) from the database. The DSK field will be permanently
lost, but that's probably not a major issue.
"""
@spec delete_node(Sqlite3.db(), integer()) :: :ok | {:error, Sqlite3.reason()}
def delete_node(_conn, 1), do: {:error, "Are you *trying* to break things?"}
def delete_node(db, node_id) do
with :ok <- Sqlite3.execute(db, "BEGIN"),
:ok <- execute(db, "DELETE FROM nodes WHERE nodeid = ?", [node_id]),
:ok <- execute(db, "DELETE FROM endpoints WHERE nodeid = ?", [node_id]) do
Sqlite3.execute(db, "COMMIT")
end
end
@doc """
Prepares and executes the given query. Returns a list of maps, where map keys
are column names and values are the corresponding row values.
"""
@spec select_all(Sqlite3.db(), binary(), list()) :: query_result([map()])
def select_all(db, query, bindings \\ []) do
with_statement(db, query, fn stmt ->
with :ok <- Sqlite3.bind(stmt, bindings),
{:ok, cols} <- Sqlite3.columns(db, stmt),
{:ok, rows} <- Sqlite3.fetch_all(db, stmt) do
{:ok, rows_to_records(cols, rows)}
end
end)
end
@doc """
Same as `select_all/3`, but returns only the first row or nil.
"""
@spec select_one(Sqlite3.db(), binary(), list()) :: query_result(map() | nil)
def select_one(db, query, bindings \\ []) do
case select_all(db, query, bindings) do
{:ok, []} -> {:ok, nil}
{:ok, [row]} -> {:ok, row}
{:ok, _} -> {:error, "Multiple rows returned"}
error -> error
end
end
@spec with_statement(Sqlite3.db(), binary(), (Sqlite3.statement() -> result)) :: result
when result: var
def with_statement(db, query, fun) do
with {:ok, stmt} <- Sqlite3.prepare(db, query) do
try do
fun.(stmt)
after
Sqlite3.release(db, stmt)
end
end
end
@doc """
Execute a query that doesn't return any rows. It is an error to use a select
statement with this function.
"""
@spec execute(Sqlite3.db(), binary(), list()) :: :ok | {:error, Sqlite3.reason()}
def execute(db, query, bindings) do
with_statement(db, query, fn stmt ->
with :ok <- Sqlite3.bind(stmt, bindings),
:done <- Sqlite3.step(db, stmt) do
:ok
else
:busy -> {:error, "Database is busy (make sure Z/IP Gateway is not running)"}
{:row, _} -> {:error, "Unexpected row returned"}
{:error, reason} -> {:error, reason}
end
end)
end
@doc """
Retrieve the listening mode of a node.
"""
@spec listening_mode(integer()) :: {:ok, listening_mode()} | nil
def listening_mode(node_id) when is_integer(node_id) do
case with_database(&get_listening_mode(&1, node_id)) do
{:ok, listening_mode} ->
{:ok, listening_mode}
{:error, reason} ->
Logger.warning(
"[Grizzly] Failed to get listening mode of node #{node_id}: #{inspect(reason)}"
)
nil
end
end
defp get_listening_mode(db, node_id) do
case get_node(db, node_id) do
{:ok, node} when is_map(node) ->
mode =
case Keyword.get(node.mode, :mode) do
:always_listening -> :always_listening
:flirs -> :frequently_listening
:wakeup -> :wakeup
:wakeup_firmware_upgrade -> :wakeup
_ -> :unknown
end
{:ok, mode}
{:error, reason} ->
{:error, reason}
_ ->
{:error, :invalid_or_missing_mode}
end
end
defp rows_to_records(cols, rows), do: Enum.map(rows, &row_to_record(cols, &1))
defp row_to_record(cols, row), do: cols |> Enum.zip(row) |> Map.new()
@spec decode_node_record(map()) :: zwave_node()
defp decode_node_record(node) do
%{
id: node["nodeid"],
dsk: if(is_binary(node["dsk"]) and byte_size(node["dsk"]) == 16, do: DSK.new(node["dsk"])),
last_awake: node["lastAwake"],
last_update: node["lastUpdate"],
manufacturer_id: node["manufacturerID"],
product_type: node["productType"],
product_id: node["productID"],
mode: decode_mode_flags(node["mode"]),
security_flags: decode_security_flags(node["security_flags"]),
properties_flags: decode_properties_flags(node["properties_flags"]),
probe_state: decode_probe_state(node["probe_flags"]),
state: decode_node_state(node["state"]),
version_capabilities: decode_version_capabilities(node["node_version_cap_and_zwave_sw"]),
basic_device_class: elem(DeviceClasses.basic_device_class_from_byte(node["nodeType"]), 1),
wake_up_interval: node["wakeUp_interval"]
}
end
@spec decode_endpoint_record(map()) :: endpoint()
defp decode_endpoint_record(endpoint) do
{generic_class, specific_class, command_classes} =
case endpoint["info"] do
<<generic, specific, command_classes::binary>> ->
{:ok, generic} = DeviceClasses.generic_device_class_from_byte(generic)
{:ok, specific} = DeviceClasses.specific_device_class_from_byte(generic, specific)
{generic, specific, CommandClasses.command_class_list_from_binary(command_classes)}
_ ->
{nil, nil, []}
end
%{
id: endpoint["endpointid"],
node_id: endpoint["nodeid"],
generic_device_class: generic_class,
specific_device_class: specific_class,
command_classes: command_classes
}
end
@doc """
Decodes the bitmask stored in the `nodes.mode` field into a keyword list.
"""
@spec decode_mode_flags(integer()) :: [mode_flag()]
def decode_mode_flags(mode) when is_integer(mode) do
mode_flag =
cond do
band(mode, 0xFF) == 0 -> :not_probed
band(mode, 0xFF) == 1 -> :nonlistening
band(mode, 0xFF) == 2 -> :always_listening
band(mode, 0xFF) == 3 -> :flirs
band(mode, 0xFF) == 4 -> :wakeup
band(mode, 0xFF) == 5 -> :wakeup_firmware_upgrade
true -> :unknown
end
deleted = band(mode, 0x0100) != 0
failed = band(mode, 0x0200) != 0
low_battery = band(mode, 0x0400) != 0
[mode: mode_flag, deleted: deleted, failed: failed, low_battery: low_battery]
end
def decode_mode_flags(_mode), do: [mode: :unknown]
@doc """
Decodes a node's Version CC capabilities bitmask stored in the
`nodes.node_version_cap_and_zwave_sw` field into a keyword list.
"""
@spec decode_version_capabilities(integer()) :: [{version_capability(), boolean()}]
def decode_version_capabilities(caps) do
version_get = band(caps, 0x01) != 0
version_command_class_get = band(caps, 0x02) != 0
version_zwave_software_get = band(caps, 0x04) != 0
[
version_get: version_get,
version_command_class_get: version_command_class_get,
version_zwave_software_get: version_zwave_software_get
]
end
@doc """
Decodes the bitmask stored in the `nodes.properties_flags` field into a keyword list.
"""
@spec decode_properties_flags(integer()) :: [properties_flag()]
def decode_properties_flags(properties_flags) do
portable = band(properties_flags, 0x01) != 0
just_added =
if(band(properties_flags, 0x02) != 0,
do: "true (probe not completed)",
else: "false (probe completed)"
)
added_by =
if(band(properties_flags, 0x04) != 0, do: "this controller", else: "another controller")
[portable: portable, just_added: just_added, added_by: added_by]
end
@doc """
Decodes the bitmask stored in the `nodes.security_flags` field into a keyword list.
"""
@spec decode_security_flags(integer()) :: [security_flag()]
def decode_security_flags(flags) do
[
s0: band(flags, 0x01) != 0,
s2_unauthenticated: band(flags, 0x10) != 0,
s2_authenticated: band(flags, 0x20) != 0,
s2_access_control: band(flags, 0x40) != 0,
known_bad: band(flags, 0x02) != 0
]
end
@doc """
Decodes the bitmask stored in the `nodes.probe_flags` field into an atom.
"""
@spec decode_probe_state(integer()) :: probe_state() | :unknown
def decode_probe_state(probe_state) do
case probe_state do
0 -> :never_started
1 -> :probe_started
2 -> :probe_failed
3 -> :ok
_ -> :unknown
end
end
@doc """
Decodes the bitmask stored in the `nodes.state` field into an atom.
"""
@spec decode_node_state(integer()) :: node_state() | :unknown
def decode_node_state(state) do
case state do
0 -> :created
1 -> :probe_node_info
2 -> :probe_product_id
3 -> :enumerate_endpoints
4 -> :find_endpoints
5 -> :check_wakeup_cc_version
6 -> :get_wakeup_capabilities
7 -> :set_wakeup_interval
8 -> :assign_return_route
9 -> :probe_wakeup_interval
10 -> :probe_endpoints
11 -> :mdns_probe
12 -> :mdns_endpoint_probe
13 -> :done
14 -> :probe_fail
15 -> :failing
_ -> :unknown
end
end
end