fix devic

This commit is contained in:
anlicheng 2026-01-08 15:03:45 +08:00
parent 6c80d8d16c
commit d7b6f5eb32
3 changed files with 265 additions and 8 deletions

View File

@ -0,0 +1,177 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 04. 9 2024 16:13
%%%-------------------------------------------------------------------
-module(aircon_device).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_link/2]).
-export([get_pid/1, get_name/1]).
-export([metric_data/2, poll_status/1]).
-define(UNIT0, 16#10).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-record(state, {
device_uuid :: binary(),
heartbeat_ticker :: integer(),
%%
data_counter = 0,
%% 1: 线0: 线
status = 0
}).
%%%===================================================================
%%% API
%%%===================================================================
-spec get_pid(DeviceUUID :: binary()) -> undefined | pid().
get_pid(DeviceUUID) when is_binary(DeviceUUID) ->
whereis(get_name(DeviceUUID)).
-spec get_name(DeviceUUID :: binary()) -> atom().
get_name(DeviceUUID) when is_binary(DeviceUUID) ->
binary_to_atom(<<"aircon_device:", DeviceUUID/binary>>).
-spec metric_data(Pid :: pid(), Message :: binary()) -> no_return().
metric_data(Pid, Message) when is_pid(Pid), is_binary(Message) ->
gen_server:cast(Pid, {metric_data, Message}).
%% , 1: 线0: 线
-spec poll_status(Pid :: pid()) -> {ok, Status :: integer()}.
poll_status(Pid) when is_pid(Pid) ->
gen_server:call(Pid, poll_status).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(Name :: atom(), DeviceUUID :: binary()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Name, DeviceUUID) when is_atom(Name), is_binary(DeviceUUID) ->
gen_server:start_link({local, get_name(DeviceUUID)}, ?MODULE, [DeviceUUID], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([DeviceUUID]) ->
{ok, HeartbeatTicker0} = application:get_env(aircon, heartbeat_ticker),
HeartbeatTicker = HeartbeatTicker0 * 1000,
erlang:start_timer(HeartbeatTicker, self(), heartbeat_ticker),
{ok, #state{device_uuid = DeviceUUID, heartbeat_ticker = HeartbeatTicker, status = 0}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(poll_status, _From, State = #state{status = Status}) ->
{reply, {ok, Status}, State};
handle_call(_Request, _From, State = #state{}) ->
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast({metric_data, Message}, State = #state{device_uuid = DeviceUUID, data_counter = DataCounter, status = Status}) ->
case catch jiffy:decode(Message, [return_maps]) of
#{<<"properties">> := Props0} ->
Props = lists:map(fun(Fields) -> transform(Fields#{<<"device_uuid">> => DeviceUUID}) end, Props0),
Info = iolist_to_binary(jiffy:encode(Props, [force_utf8])),
case catch efka_client:send_metric_data(Props, #{}) of
{ok, _} ->
aircon_logger:write([<<"OK">>, Info]);
_ ->
aircon_logger:write([<<"ERROR">>, Info])
end,
%% 线线
Status == 0 andalso efka_client:device_online(DeviceUUID),
{noreply, State#state{data_counter = DataCounter + 1, status = 1}};
M when is_map(M) ->
lager:notice("[power_device] invalid map: ~p", [M]);
Error ->
lager:notice("[power_device] jiffy decode error: ~p", [Error]),
{noreply, State}
end.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({timeout, _, heartbeat_ticker}, State = #state{device_uuid = DeviceUUID, heartbeat_ticker = HeartbeatTicker, data_counter = DataCounter, status = Status}) ->
erlang:start_timer(HeartbeatTicker, self(), heartbeat_ticker),
case DataCounter > 0 of
true ->
{noreply, State};
false ->
Status == 1 andalso efka_client:device_offline(DeviceUUID),
{noreply, State#state{status = 0}}
end.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%% value是类型unit才是数值
%% [{"value":"int","unit":44081,"type":"AI","timestamp":1730799797,"name":"使用次数","label":"","key":"use_times","device_uuid":"30409239002349977617259608405456"}]
-spec transform(Prop :: map()) -> map().
transform(Prop = #{<<"key">> := <<"total_power">>, <<"unit">> := Unit}) ->
Prop#{<<"name">> => <<"总能耗"/utf8>>, <<"value">> => Unit, <<"unit">> => 16#02};
transform(Prop = #{<<"key">> := <<"total_runtime">>, <<"unit">> := Unit}) ->
Prop#{<<"name">> => <<"总运行时间"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
transform(Prop = #{<<"key">> := <<"use_times">>, <<"unit">> := Unit}) ->
Prop#{<<"name">> => <<"使用次数"/utf8>>, <<"label">> => <<""/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
transform(Prop = #{<<"key">> := <<"light_switch">>, <<"unit">> := Unit}) ->
Prop#{<<"name">> => <<"开关"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
transform(Prop = #{<<"key">> := <<"light_brightness">>, <<"unit">> := Unit}) when is_integer(Unit) ->
Label = iolist_to_binary([<<"亮度设置为:"/utf8>>, integer_to_binary(Unit)]),
Prop#{<<"name">> => <<"亮度"/utf8>>, <<"label">> => Label, <<"value">> => Unit, <<"unit">> => ?UNIT0};
transform(Prop = #{<<"key">> := <<"light_change_time">>, <<"unit">> := Unit}) ->
Prop#{<<"name">> => <<"变亮变暗时间"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
transform(Prop = #{<<"key">> := <<"light_status">>, <<"unit">> := Unit}) ->
Prop#{<<"name">> => <<"是否损坏"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
transform(Prop = #{<<"unit">> := Unit}) ->
Prop#{<<"value">> => Unit, <<"unit">> => ?UNIT0}.

View File

@ -0,0 +1,80 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 04. 9 2024 15:39
%%%-------------------------------------------------------------------
-module(aircon_device_sup).
-author("anlicheng").
-behaviour(supervisor).
%% API
-export([start_link/0]).
-export([ensure_device_started/1, delete_device/1]).
%% Supervisor callbacks
-export([init/1]).
-define(SERVER, ?MODULE).
%%%===================================================================
%%% API functions
%%%===================================================================
%% @doc Starts the supervisor
-spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================
%% @private
%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3],
%% this function is called by the new process to find out about
%% restart strategy, maximum restart frequency and child
%% specifications.
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
{ok, {SupFlags, []}}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec ensure_device_started(DeviceUUID :: binary()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
ensure_device_started(DeviceUUID) when is_binary(DeviceUUID) ->
case aircon_device:get_pid(DeviceUUID) of
DevicePid when is_pid(DevicePid) ->
{ok, DevicePid};
undefined ->
case supervisor:start_child(?MODULE, child_spec(DeviceUUID)) of
{ok, Pid} when is_pid(Pid) ->
{ok, Pid};
{error, {'already_started', Pid}} when is_pid(Pid) ->
{ok, Pid};
{error, Error} ->
{error, Error}
end
end.
delete_device(DeviceUUID) when is_binary(DeviceUUID) ->
Id = aircon_device:get_name(DeviceUUID),
ok = supervisor:terminate_child(?MODULE, Id),
supervisor:delete_child(?MODULE, Id).
child_spec(DeviceUUID) when is_binary(DeviceUUID) ->
Name = aircon_device:get_name(DeviceUUID),
#{
id => Name,
start => {aircon_device, start_link, [Name, DeviceUUID]},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['aircon_device']
}.

View File

@ -63,14 +63,14 @@ init([]) ->
modules => ['aircon_args']
},
%#{
% id => 'power_device_sup',
% start => {'power_device_sup', start_link, []},
% restart => permanent,
% shutdown => 2000,
% type => worker,
% modules => ['power_device_sup']
%},
#{
id => 'aircon_device_sup',
start => {'aircon_device_sup', start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['aircon_device_sup']
},
#{
id => 'aircon_mqtt_subscriber',