Packages
grizzly
9.1.4
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/zwave/command_spec.ex
defmodule Grizzly.ZWave.CommandSpec do
@moduledoc """
Data structure describing a Z-Wave command, including how to create, encode,
and decode it.
"""
alias Grizzly.Requests.Handlers.AckResponse
alias Grizzly.Requests.Handlers.WaitReport
alias Grizzly.ZWave.Command
alias Grizzly.ZWave.CommandClasses
alias Grizzly.ZWave.ParamSpec
@type report_matcher :: (get :: Command.t(), report :: Command.t() -> boolean())
schema =
NimbleOptions.new!(
name: [
type: :atom,
required: true,
doc: """
The command's name as an atom (should be globally unique).
"""
],
command_class: [
type: {:custom, __MODULE__, :validate_command_class, []},
required: true,
type_spec: quote(do: atom()),
type_doc: "`t:Grizzly.ZWave.CommandClasses.command_class/0`",
doc: """
The command class this command belongs to.
"""
],
module: [
type: :atom,
type_spec: quote(do: module()),
doc: """
The module implementing this command's encoding and decoding functions.
""",
required: true
# deprecated: "Use `encode_fun` and `decode_fun` instead."
],
command_byte: [
type: {:in, 0..255},
required: true,
doc: """
The command identifier.
"""
],
encode_fun: [
type: {:custom, __MODULE__, :__validate_fun__, [2]},
required: true,
doc: """
A function or `{module, function}` tuple used to encode the command's
parameters into a binary. Only 1-arity functions are currently supported.
"""
],
decode_fun: [
type: {:custom, __MODULE__, :__validate_fun__, [2]},
required: true,
doc: """
A function or `{module, function}` tuple used to decode the command's
parameters from binary to a keyword list (the reverse of the `encode_fun`).
If the function is of arity 1, the first argument will be the binary to
decode. If arity 2, the command spec will be inserted as the first argument.
"""
],
report_matcher_fun: [
type: {:or, [{:custom, __MODULE__, :__validate_fun__, [2]}, nil]},
required: false,
doc: """
A 2-arity function or `{module, function}` tuple used to match a
report command to a get command. The first argument will be the get
command and the second argument will be the report command to match.
The function should return `true` if the report matches the get.
"""
],
report: [
type: :atom,
required: false,
doc: """
The name of the report command associated with this command, if any.
For get commands, this will default to the command name with the trailing
`_get` replaced with `_report`. To override this behavior, explicitly
set this field to nil.
A special value of `:any` may be used to indicate that any report command
can satisfy this particular get command. Otherwise, this should be
the name of a registered report command.
"""
],
handler: [
type: {:tuple, [:atom, :keyword_list]},
required: false,
doc: """
A `{module, options}` tuple indicating the request handler
that should be used to handle completion of this command when sent
via a `Grizzly.Request`. Normally, this will default to `{AckResponse, []}`.
However, if the `:report` option is specified but `:handler` is not,
the default becomes `{WaitReport, complete_report: report}`.
"""
],
supports_supervision?: [
type: :boolean,
default: true,
doc: """
Whether this command supports Z-Wave Supervision. Defaults to `false`
for commands whose names end with `_get`. Defaults to `true` for commands
whose names end with `_set` or `_report`. Defaults to `false` for commands
named `network_management_*`, as most of these are only supported by
Z/IP Gateway.
"""
],
validate_fun: [
type: {:or, [{:custom, __MODULE__, :__validate_fun__, [2]}, nil]},
required: false,
doc: """
A `{module, function}` tuple indicating a function that validates
the command parameters. The function should return `{:ok, params}` if the
parameters are valid or have been updated, or `{:error, reason}` if the
parameters are invalid.
"""
],
params: [
type: {:custom, __MODULE__, :validate_param_list, []},
keys: [*: [type: {:struct, ParamSpec}]],
default: []
],
default_params: [
type: :keyword_list,
default: [],
doc: """
A keyword list of default parameters for the command. These parameters
will be merged with any parameters passed to `create_command/2` before
validation.
"""
]
)
@schema schema
@typedoc "#{NimbleOptions.docs(schema)}"
@type t :: %__MODULE__{
name: atom(),
command_class: atom(),
command_byte: byte(),
module: module(),
encode_fun: {module(), atom()},
decode_fun: {module(), atom()},
validate_fun:
{module(), atom()} | (keyword() -> {:ok, keyword()} | {:error, any()}) | nil,
report_matcher_fun: {module(), atom()} | nil,
report: atom() | nil,
handler: {module(), keyword()},
supports_supervision?: boolean(),
params: list({atom(), ParamSpec.t()}),
default_params: keyword()
}
defstruct name: nil,
command_class: nil,
command_byte: nil,
module: nil,
encode_fun: nil,
decode_fun: nil,
validate_fun: nil,
report_matcher_fun: nil,
report: nil,
handler: {AckResponse, []},
supports_supervision?: false,
params: [],
default_params: []
@doc """
Create a new command spec.
"""
@doc group: "Command Specs"
def new(command_class, name, byte, mod, opts) when is_list(opts) do
new(
Keyword.merge(opts,
name: name,
command_class: command_class,
command_byte: byte,
module: mod
)
)
end
@doc """
Create a new command spec.
"""
@doc group: "Command Specs"
def new(fields) do
{module, fields} = Keyword.pop(fields, :module)
handler = Keyword.get(fields, :handler)
report = Keyword.get_lazy(fields, :report, fn -> default_report(fields[:name]) end)
name = Keyword.get(fields, :name)
module =
cond do
# if it doesn't look like a module name, return nil
is_nil(module) or not is_atom(module) -> nil
# if it's a loaded module, don't rename it
Code.ensure_loaded?(module) -> module
# if it's a top-level module name, prepend Grizzly.ZWave.Commands
match?([_], Module.split(module)) -> Module.concat([Grizzly.ZWave.Commands, module])
# otherwise, assume it's already fully-qualified
true -> module
end
handler =
cond do
handler != nil -> handler
report != nil -> {WaitReport, complete_report: report}
true -> {AckResponse, []}
end
# If params is specified, default values in the param specs will override
# anything in default_params, which will someday be removed.
params = Keyword.get(fields, :params, [])
default_params = Keyword.get(fields, :default_params, [])
defaults_from_param_specs =
for {name, %ParamSpec{default: default}} <- params, default != nil, into: [] do
{name, default}
end
default_params = Keyword.merge(default_params, defaults_from_param_specs)
fields =
Keyword.merge(fields, handler: handler, report: report, default_params: default_params)
fields =
if is_nil(module) do
fields
else
[
module: module,
encode_fun: maybe_fun_from_module(module, :encode_params, 2),
decode_fun: maybe_fun_from_module(module, :decode_params, 2),
validate_fun: maybe_fun_from_module(module, :validate_params, 2),
report_matcher_fun: maybe_fun_from_module(module, :report_matches_get?, 2)
] ++ fields
end
fields =
Keyword.put_new(
fields,
:supports_supervision?,
default_supports_supervision?(fields[:command_class], name)
)
struct!(__MODULE__, fields)
end
@doc """
Validate a command spec.
"""
@doc group: "Command Specs"
def validate(%__MODULE__{} = spec) do
spec
|> Map.from_struct()
|> NimbleOptions.validate(@schema)
end
@doc false
def validate_command_class(value) do
if CommandClasses.valid?(value) do
{:ok, value}
else
{:error, "#{inspect(value)} is not a valid command class"}
end
end
@doc false
def __validate_fun__({mod, fun} = v, arity) when is_atom(mod) and is_atom(fun) do
cond do
Code.ensure_loaded?(mod) == false ->
{:error, "module #{inspect(mod)} is not loaded"}
not function_exported?(mod, fun, arity) ->
{:error,
"function #{inspect(mod)}.#{Atom.to_string(fun)}/#{arity} is undefined or private"}
true ->
{:ok, v}
end
end
def __validate_fun__(v, _arity), do: {:error, inspect(v)}
@doc """
Get the handler and options for a command.
"""
@doc group: "Command Specs"
def handler_spec(%__MODULE__{} = spec) do
{module, opts} = spec.handler
if spec.report != nil and not Keyword.has_key?(opts, :complete_report) do
{module, Keyword.put(opts, :complete_report, spec.report)}
else
{module, opts}
end
end
defp default_report(name) do
name_str = Atom.to_string(name)
if String.ends_with?(name_str, "_get") do
name_str
|> String.replace_suffix("_get", "_report")
|> String.to_atom()
else
nil
end
end
defp default_supports_supervision?(command_class, name) do
cc = Atom.to_string(command_class)
c = Atom.to_string(name)
cond do
String.starts_with?(cc, "network_management_") -> false
String.ends_with?(c, "_set") -> true
String.ends_with?(c, "_report") -> true
String.ends_with?(c, "_get") -> false
true -> false
end
end
defp maybe_fun_from_module(module, fun, arity) do
_ = Code.ensure_compiled(module)
if function_exported?(module, fun, arity) do
{module, fun}
else
nil
end
end
@doc """
Validates a list of parameter specs.
## Examples
iex> params = [
...> param1: %Grizzly.ZWave.ParamSpec{name: :param1, type: :uint, size: 8},
...> param2: %Grizzly.ZWave.ParamSpec{name: :param2, type: :int, size: 16}
...> ]
iex> Grizzly.ZWave.CommandSpec.validate_param_list(params)
{:ok, params}
iex> params = [
...> param1: %Grizzly.ZWave.ParamSpec{name: :param1, type: :uint, size: 7},
...> param2: %Grizzly.ZWave.ParamSpec{name: :param2, type: :int, size: 8}
...> ]
iex> Grizzly.ZWave.CommandSpec.validate_param_list(params)
{:error, "Total size of all non-variable parameters must be a multiple of 8 bits"}
iex> params = [
...> param1: %Grizzly.ZWave.ParamSpec{name: :param1, type: :uint, size: :variable},
...> param2: %Grizzly.ZWave.ParamSpec{name: :param2, type: :int, size: 16}
...> ]
iex> Grizzly.ZWave.CommandSpec.validate_param_list(params)
{:error, "Variable-length parameter without length specifier must be last"}
"""
@doc group: "Command Specs"
@spec validate_param_list(list({atom(), ParamSpec.t()})) ::
{:ok, list({atom(), ParamSpec.t()})} | {:error, String.t()}
def validate_param_list(params) do
with :ok <- validate_param_list_size(params),
:ok <- variable_param_without_length_must_be_last(params) do
{:ok, params}
end
end
defp validate_param_list_size(params, bits \\ 0)
defp validate_param_list_size([], bits) when rem(bits, 8) == 0, do: :ok
# skip when size is :variable or {:variable, _}
defp validate_param_list_size([{_, %{size: size}} | params], bits)
when size == :variable or (is_tuple(size) and elem(size, 0) == :variable),
do: validate_param_list_size(params, bits)
defp validate_param_list_size([{_, %{type: :binary, size: size}} | params], bits)
when is_integer(size),
do: validate_param_list_size(params, bits + size * 8)
defp validate_param_list_size([{_, %{size: size}} | params], bits)
when is_integer(size),
do: validate_param_list_size(params, bits + size)
defp validate_param_list_size([], _bits) do
{:error, "Total size of all non-variable parameters must be a multiple of 8 bits"}
end
defp variable_param_without_length_must_be_last([{_name, %{size: :variable}} | rest])
when rest != [] do
{:error, "Variable-length parameter without length specifier must be last"}
end
defp variable_param_without_length_must_be_last([_ | rest]),
do: variable_param_without_length_must_be_last(rest)
defp variable_param_without_length_must_be_last([]), do: :ok
@doc """
Create a command struct from the command spec and parameters.
"""
@doc group: "Commands"
def create_command(%__MODULE__{} = spec, params) do
params = Keyword.merge(spec.default_params, params)
with {:ok, params} <- validate_params(spec, params) do
cmd = %Grizzly.ZWave.Command{
name: spec.name,
command_class: spec.command_class,
command_byte: spec.command_byte,
params: params
}
{:ok, cmd}
end
end
@doc """
Validate a command's parameters according to the command spec.
"""
@doc group: "Commands"
def validate_params(%__MODULE__{validate_fun: nil} = _spec, params) do
{:ok, params}
end
def validate_params(%__MODULE__{validate_fun: {mod, fun}} = spec, params) do
apply(mod, fun, [spec, params])
end
def validate_params(%__MODULE__{validate_fun: fun} = spec, params)
when is_function(fun, 2) do
fun.(spec, params)
end
end