Zyxel GS1900-24EP support, production ready?

Hi,

I’d like to use the Zyxel GS1900-24EP, which is already supported through this commit: https://github.com/openwrt/openwrt/commit/0688cf5aebe1dc9a2e7f3820861783c2a7a75d44. However, I’m uncertain because there is no device page, and only snapshot support is available. Do I need to wait for the next OpenWrt major release to get production-ready support for this switch?

If so, could anyone recommend a similar managed switch (24 ports, at least 6 PoE, rackmount, and under €200)?

Thanks in advance!

depends on your definition of production ready, but snapshots can contain bugs, yes.
device page <> production ready, it's manual work, and no one bothered.

use https://openwrt.org/toh/views/toh_extended_all, enter switch in the 1st column.
the list might be incomplete though.

1 Like

depends on your definition of production ready, but snapshots can contain bugs, yes.

I have no experience with snapshot builds. Would it be a good idea to use one for the main switch of a network?

use https://openwrt.org/toh/views/toh_extended_all, enter switch in the 1st column. the list might be incomplete though.

That's what I initially did, and the only option available was this particular switch, which wondered me.

people use snapshots all the time, for bleeding edge features or support -
https://openwrt.org/releases/snapshot

if it's a good or bad idea, only you can decide.

based on what criteria ?

1 Like

Switch columns aren't enterable in the normal fields for new ToH entries. Requires editing where a normal wiki user probably wouldn't be comfortable.

JG925A/JG926A has patches available but isn't in snapshot yet.
If you want something already in openwrt, used and a quiet solution I'd sugggest a two switch solution of JG921A/JG922A + JG924A. For a single switch solution I'd suggest JG928A. Please note that there isn't fan control for JG928A yet. JG928A fan control is something I submitted on the mailing list though.

How to fix the default fan behaviour is also what's holding up JG925A/JG926A. I'm yet to have another go at my gpio init patches.

But yeah if you're willing to build from source with out of tree patches JG925A/JG926A fits your 24 port 1RU with 6 PoE requirement. But it is used?

Don't count on MSTP, link aggregation or anything fancy. These things can't route either. I think there might be port mirroring but I haven't looked into it. Similarly there isn't (built in cable test ) TDR short/open support yet but that looks relatively straightforward. It really depends which of the normal factory firmware features you're after.

edit:
Also as an FYI the situation was quite dire recently with getting the target onto kernel 6.6 if you missed that? From what I understand there is only really one developer at the moment submitting kernel patches for realtek to openwrt source tree?

I suggest having a look at the developer thread:

I can also speak generically about "production" and "openwrt" haha. It also really depends on how much you want to bank on volunteer time for ongoing support for whatever you define as your "production" workload......

IMHO ZyXEL GS1900 series is one of the best supported switch series, but stay with older models (newer revisions change to unsupported PoE controllers, I don't remember which models exactly). I use a GS1900-24HPv2 for over two years without issues in my home network. I also have a D-Link DGS-1210-28MP as spare, but I never needed it. The DGS-1210 is also pretty well supported, but be careful on the revision. It's easier with the ZyXELs.

According to this, the -24EP uses the newer Realtek PoE controllers. They are as of yet not supported by realtek-poe (or just as a PR, see here), so I would stay away from this model for the time being.

1 Like

Thank you for your replies!

@frollic

if it's a good or bad idea, only you can decide.

I was hoping to hear about others' experiences. Without that, how would I be able to make a decision?

based on what criteria ?

24 ports, at least 6 PoE, VLAN, rackmount, and for ~€200

@evs

For a single switch solution I'd suggest JG928A

That’s over €1000 new here, so I’ll be looking for a used one. Thanks for the suggestion. Regarding the fan: Does it work, but isn’t controllable, or are there other downsides?

@ andyboeh (am only allowed to mention two users)

but stay with older models

I haven’t found any that match my criteria—am I missing something?

They are as of yet not supported by realtek-poe

I read on TOH that PoE control isn’t supported, but I assumed the ports would have PoE enabled all the time, which should be fine for me.

The GS1900-24EP was one of the first Zyxel switches to use the new Realtek RTL8238 PSE controllers. This PSE controller does not have support in OpenWrt yet. So although the switch is functional with OpenWrt none of the PoE features would be usable.

I meant the GS1900-24HP. Both versions are very well supported and are available for cheap on the used market. Or, as already mentioned, the D-Link DGS-1210 series if you watch carefully for the revision (only F and R are supported) or the HPE 1920 series. Apart from that, there is not much choice.

I don't know for the 24EP, but the currently supported models have PoE disabled by default.

Yeah should be within your budget if you go used. Pretty sure they're discontinued so I wouldn't believe a new listing if there was one =P

If you buy a JG926A I'll be motivated to send you a binary and have another go at my patches =P But if you want something that's already merged JG928A or JG924A+jg921A I reckon.

Fan will be max all the time and uncontrollable until the rtl8231 GPIO controller on the alt mdio bus is added to device tree per the patch I sent. Then it will still require custom fan control code as the default should probably still be max. But that's just set in your rc.local or write your own service to change the pwm value.

Fans are slightly louder than my JG926A's. For my home I run JG921A because fanless. JG922A also has a fan but is 180W not 65W. IDK about JG925A loudness.

Other than that no issues. I have two JG928A's and "works for me".

The only downside is that the SFP's don't work. Plus slightly higher power consumption of course. But I think that's the case for all RTL8393 targets. If you want SFP's that may / partially work you want RTL8380/RTL8382M. (i.e. my SFP transceivers work on my JG921/JG922/JG926's but not my JG927/JG928's)

The D-Link DGS-1210 actually looks like a good alternative. Found a DGS-1210-28P on amazons black friday today, but I haven't seen flashing instructions. How would that work?

Edit: nvm, found

To install, upload the sysupgrade image to the OEM webpage

on https://git.openwrt.org/?p=openwrt/openwrt.git;a=commit;h=b5bd945733a7ed235777c2ceaa0ff27dc779630f. Should work, I guess.

It depends on the OEM firmware if it works from the web interface or not. It worked on my DGS-1210-28MP, but there are numerous reports where people had to use TFTP.

Please keep in mind that the -28P version has only snapshot builds available as support for it was added last. While the -28MP is also supported in 23.05, I strongly recommend snapshot as the fan does not work in the 23.05 (but works fine in snapshot).

1 Like

Beg to differ.

Pretty sure that PoE works perfectly fine, if you use this:

/usr/bin/poemgr-realtek:

#!/usr/bin/lua

-------------------------------------------------------------
----  For testing, start in foreground with:
----  $ poemgr-realtek -d -v
----
----  ("-d" is debug, "-v" is verbose)
----
----  While the daemon is running, PoE ports can be manually
----  enabled via ubus:
----  $ ubus call poe manage "{'port':'lan1', 'enable':true}"
----
----  Inspect the per-port consumption to see if power is delivered:
----  $ ubus call poe info
----
----  If everything works, install the PoE manager:
----    1. poe manager (this file)   --> /usr/bin/poemgr-realtek
----    2. service definition file   --> /etc/init.d/poemgr-realtek
----    3. configuration             --> /etc/config/poe
----
----  And install required packages / dependencies:
----  $ opkg install lua-rs232 libuci-lua libubus-lua
----
----  Start the service via GUI (System -> Startup) or CLI:
----  $ service poemgr-realtek start
----
----  In the configuration file, PoE ports can be enabled or
----  disabled with the "enable" property.
----
----  Priority groups can be configured with "priority".
----  Priorities range from 0 (low) to 3 (critical), with
----  1 (normal) and 2 (high) in between.
----
----  Mapping between the PSE power lines and Ethernet ports
----  can be adjusted with the "id" and "name" properties.
----
----  After modifying the configuration file or updating
----  configuration via UCI, perform a commit to push
----  changes to the PoE controller:
----  $ ubus call uci commit '{"config":"poe"}'
----
----  For reading out internal statistics, the following
----  command is available.
----  $ ubus call poe stats
----
----  For experimenting with new or updated opcodes in the
----  controller firmware, the following command is available.
----  $ ubus call poe sendframe '{"frame":"40"}'
-------------------------------------------------------------

require "uci"
require "ubus"
require "uloop"

local rs = require "luars232"
local stderr = io.stderr

-- PSE known power limits for smallest supported device (zyxel,gs1900-10hp-v2)
local PSE_WATTS = 77.0
local PSE_GUARD = 11.0

-- SoC serial port for talking to MCU
local SOC_UART = "/dev/ttyS1"

-- MCU baud rate (Realtek RTL8238)
local MCU_RATE = rs.RS232_BAUD_115200

-- How many times to try again in case of timeouts, checksum errors etc.
local MAX_RETRIES = 3

-- MCU error codes (Realtek RTL8238)
local MCU_ERR_CHECKSUM = 0x62
local MCU_ERR_INCOMPLETE = 0xfd
local MCU_ERR_MALFORMED = 0xfe
local MCU_ERR_BUSY = 0xff

-- MCU commands (Realtek RTL8238)
local MCU_CMD_GET_CONTROLLER_STATUS = 0x40
local MCU_CMD_SET_GLOBAL_POWER_LIMITS = 0x04
local MCU_CMD_GET_GLOBAL_POWER_USAGE = 0x41
local MCU_CMD_SET_PORT_PRIORITY = 0x15
local MCU_CMD_SET_PORT_POWER_DELIVERY = 0x01
local MCU_CMD_GET_PORT_POWER_USAGE = 0x44

-- Hardware the above is tested on
local MATCH_BOARDNAME_1 = "ZyXEL_GS1900_10HPv2"
local MATCH_BOARDNAME_2 = "ZyXEL_GS1900_24EP"
local MATCH_PSEID_1 = "4"
local MATCH_PSEID_2 = ""

-- Text markings on packages - guesswork, need photos
local CHIPNAME_PSE = "Nuvoton NUC029ZAN"
local CHIPNAME_MCU = "Realtek RTL8238"

-- Internal statistics counters
local stats_msg = 0
local stats_timeout = 0
local stats_checksum = 0
local stats_error = 0
local stats_notify = 0
local stats_reload = 0

-- Command-line options
local skip_hwcheck = false
local log_verbose = false
local log_debug = false

-- Filled with defaults (1=lan1, 2=lan2 etc.) during init
local ifname
local portid, portprio, portenable
local budget, guard
local poe_ports

-- Globals
local seq = 0
local queue = {}

function verbose(s)
	if log_verbose then io.write(s) end
end

function debug(s)
	if log_debug then io.write(s) end
end

function check(cond, msg)
	if not cond then
		if type(msg) == "function" then msg = msg() end
		stderr:write(msg)
		assert(false)
	end
end

function read_fw(tool, key)
	local handle = io.popen(string.format("%s -n %s", tool, key))
	local output = handle:read("*a")
	local res = output:gsub('[\n\r]', '')
	handle:close()
	return res
end

function read_fw_env(key)
	return read_fw("fw_printenv", key)
end

function read_fw_sys(key)
	return read_fw("fw_printsys", key)
end

function check_compatible()
	if skip_hwcheck then
		verbose("skip board revision check\n")
	else
		local model = read_fw_env("boardmodel")
		local pseid = read_fw_sys("pseId")
		check(
			(model == MATCH_BOARDNAME_1 or model == MATCH_BOARDNAME_2) and (pseid == MATCH_PSEID_1 or pseid == MATCH_PSEID_2),
			"ERROR: unknown hardware model or revision\n"
		)
	end
end

function configure_uart()
	local err, handle = rs.open(SOC_UART)
	check(err == rs.RS232_ERR_NOERROR, function() return string.format("ERROR: failed to open('%s'): %s\n", SOC_UART, rs.error_tostring(err)) end)

	check(handle:set_baud_rate(MCU_RATE) == rs.RS232_ERR_NOERROR, "ERROR: failed to set baud rate")
	check(handle:set_data_bits(rs.RS232_DATA_8) == rs.RS232_ERR_NOERROR, "ERROR: failed to set data bits")
	check(handle:set_parity(rs.RS232_PARITY_NONE) == rs.RS232_ERR_NOERROR, "ERROR: failed to set parity")
	check(handle:set_stop_bits(rs.RS232_STOP_1) == rs.RS232_ERR_NOERROR, "ERROR: failed to set stop bits")
	check(handle:set_flow_control(rs.RS232_FLOW_OFF)  == rs.RS232_ERR_NOERROR, "ERROR: failed to disable flow control")

	io.write(string.format("configured %s\n", tostring(handle)))
	synchronize(handle)
	return handle
end

function synchronize(handle)
	local interval = 1000
	local buffer = {}
	local size = 1
	local err, data, i

	io.write(string.format("wait for quiescent %s\n", SOC_UART))
	while size > 0 do
		err, data, size = handle:read(4096, interval)
		if err == rs.RS232_ERR_NOERROR then
			for i = 1, string.len(data) do
				table.insert(buffer, string.byte(string.sub(data, i, i)))
			end
		elseif err == rs.RS232_ERR_TIMEOUT then
			size = 0
		else
			check(false, string.format("ERROR: failed to read(): %s\n", rs.error_tostring(err)))
		end
	end

	if #buffer > 0 then
		debug("RX")
		for i = 1, #buffer do
			debug(string.format(" %02x", buffer[i]))
		end
		debug("\n")
	end
end

function try_process_unsolicited()
	-- handle any notifications received asynchronously
	stats_notify = stats_notify + #queue
	if #queue == 0 then return nil end
	verbose(string.format("ignoring %d unknown notifications from controller\n", #queue))
	queue = {}
end

function try_exchange_message(h, xmit)
	local interval = 1000
	local retries = 10
	local recv = {}
	local round = 0
	local sum = 0
	local data = ""
	local err, size, left, i

	if #xmit > 12 then
		verbose(string.format("output frame too large, discard\n", xmit))
		return nil
	end

	if #xmit < 1 then
		verbose("output frame too small, discard\n")
		return nil
	end

	-- reserve all-zeroes and bit 2^7 for controller
	seq = (seq % 127) + 1
	xmit[2] = seq

	-- pad to full frame length
	while #xmit < 12 do
		table.insert(xmit, 0xff)
	end

	-- coalesce to zero to avoid crash on invalid input
	for i = 1, 12 do
		if type(xmit[i]) ~= "number" then xmit[i] = 0 end
		xmit[i] = math.floor(math.abs(xmit[i])) % 256
	end

	-- calculate checksum
	for i = 1, 11 do
		sum = sum + xmit[i]
	end
	xmit[12] = sum % 256

	debug("TX ->")
	for i = 1, 12 do
		debug(string.format(" %02x", xmit[i]))
	end
	debug("\n")

	for i = 1, 12 do
		data = data .. string.char(xmit[i])
	end

	err, size = h:write(data)
	check(err == rs.RS232_ERR_NOERROR, function() return string.format("ERROR: failed to write(): %s\n", rs.error_tostring(err)) end)

	while round < retries do
		while round < retries and #recv < 12 do
			err, data, size = h:read(12 - #recv, interval)
			if err == rs.RS232_ERR_NOERROR then
				check(#recv + size < 13, "ERROR: read() returned more octets than asked")
				check(size > 0, "ERROR: RX buffer emptied between select() and read()")
				for i = 1, string.len(data) do
					table.insert(recv, string.byte(string.sub(data, i, i)))
				end
			elseif err ~= rs.RS232_ERR_TIMEOUT then
				check(false, string.format("ERROR: failed to read(): %s\n", rs.error_tostring(err)))
			end
			round = round + 1
		end

		if #recv > 0 then
			stats_msg = stats_msg + 1
			debug("RX <-")
			for i = 1, #recv do
				debug(string.format(" %02x", recv[i]))
			end
			debug("\n")
		end

		if #recv < 12 then
			stats_timeout = stats_timeout + 1
			stderr:write("timeout while waiting for response\n")
			return nil
		end

		sum = 0
		for i = 1, 11 do
			sum = sum + recv[i]
		end

		if sum % 256 ~= recv[12] then
			stats_checksum = stats_checksum + 1
			stderr:write("frame checksum invalid in response\n")
			return nil

		elseif recv[1] ~= xmit[1] and recv[2] ~= xmit[2] then
			verbose("unsolicited frame, add to queue\n");
			table.insert(queue, recv)
			recv = {}
			round = 0

		elseif recv[2] ~= xmit[2] then
			recv = {}
			verbose("wrong sequence number from controller, discard\n")

		elseif recv[1] == MCU_ERR_CHECKSUM then
			stats_error = stats_error + 1
			verbose("controller report: invalid checksum\n")

		elseif recv[1] == MCU_ERR_INCOMPLETE then
			stats_error = stats_error + 1
			verbose("controller report: incomplete request\n")

		elseif recv[1] == MCU_ERR_MALFORMED then
			stats_error = stats_error + 1
			verbose("controller report: malformed request\n")

		elseif recv[1] == MCU_ERR_BUSY then
			stats_error = stats_error + 1
			verbose("controller report: busy\n")

		elseif recv[1] ~= xmit[1] then
			stats_error = stats_error + 1
			verbose(string.format("controller report: (unknown error reply %s)\n", recv[1]));

		else
			return recv
		end
	end
end

function exchange_message_once(h, xmit)
	local recv = try_exchange_message(h, xmit)
	try_process_unsolicited()
	return recv
end

function exchange_message(h, xmit)
	local recv, round
	for round = 1, MAX_RETRIES do
		if round > 1 then verbose("Retrying...\n") end
		recv = exchange_message_once(h, xmit)
		if recv ~= nil then break end
	end
	return recv
end

function uintbe16(nr)
	-- coalesce to zero to avoid crash on malformed input
	if type(nr) ~= "number" then nr = 0 end
	local l = math.floor(nr * 10 / 256)
	local r = math.floor(nr * 10) % 256
	return l, r
end

function get_controller_status(h)
	verbose("request controller status\n")
	local cmd = {MCU_CMD_GET_CONTROLLER_STATUS}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	table.remove(reply, #reply)
	table.remove(reply, 1)
	table.remove(reply, 1)
	return unpack(reply)
end

function set_power_limits(h, total, guard)
	verbose(string.format("set power limits, global budget: %s, port guard: %s\n", total, guard))
	local cmd = {MCU_CMD_SET_GLOBAL_POWER_LIMITS, 0, 0x00}
	cmd[4], cmd[5] = uintbe16(total)
	cmd[6], cmd[7] = uintbe16(guard)
	exchange_message(h, cmd)
end

function get_global_power_usage(h)
	verbose("get global power usage\n")
	local cmd = {MCU_CMD_GET_GLOBAL_POWER_USAGE, 0}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	local watts = (reply[3] * 256 + reply[4]) / 10.0
	verbose(string.format("global watts: %s\n", watts))
	return watts
end

function set_port_priority(h, port, prio)
	verbose(string.format("set priority %s for port %s\n", prio, port))
	local cmd = {MCU_CMD_SET_PORT_PRIORITY, 0, port, prio}
	exchange_message(h, cmd)
end

function disable_port_power(h, port)
	verbose(string.format("disable power on port %s\n", port))
	local cmd = {MCU_CMD_SET_PORT_POWER_DELIVERY, 0, port, 0x00}
	exchange_message(h, cmd)
end

function enable_port_power(h, port)
	verbose(string.format("enable power on port %s\n", port))
	local cmd = {MCU_CMD_SET_PORT_POWER_DELIVERY, 0, port, 0x01}
	exchange_message(h, cmd)
end

function get_port_power_usage(h, port)
	verbose(string.format("get power usage on port %s\n", port))
	local cmd = {MCU_CMD_GET_PORT_POWER_USAGE, 0, port}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	local watts = (reply[10] * 256 + reply[11]) / 10.0
	local milliamps = reply[6] * 256 + reply[7]
	verbose(string.format("port %s watts: %s milliAmps: %s\n", port, watts, milliamps))
	return watts, milliamps
end

function count_ports(h)
	local ports = ({get_controller_status(h)})[2]
	check(type(ports) ~= "nil", "ERROR: failed to get controller global state\n")
	io.write(string.format("controller status request ok, %s ports\n", tostring(ports)))
	poe_ports = ports
end

function ensure_defaults()
	check(type(poe_ports) ~= "nil", "ERROR: port count not retrieved yet\n")
	-- default available pse port ids
	portid = {}
	local i
	for i = 1, poe_ports do
		table.insert(portid, i)
	end
	-- default global policy
	budget = PSE_WATTS
	guard = PSE_GUARD
	-- default pse port id to interface name mapping is lan1->pse1, lan2->pse2 etc.
	ifname = {}
	for i = 1, poe_ports do
		ifname[i] = "lan" .. tostring(i)
	end
	-- default is empty, so to do nothing
	portprio = {}
	portenable = {}
end

function load_config()
	-- preferred if each poe controller had its own
	-- top-level section (fx. "poe.poectrl0") but alas
	local cfg = uci.cursor()
	local k, v

	local global_budget
	local global_guard
	cfg:foreach("poe", "global", function(s)
		for k, v in pairs(s) do
			if k == "budget" then global_budget = tonumber(v) end
			if k == "guard" then global_guard = tonumber(v) end
		end
	end)
	-- if user supplied budget and/or guard values exist, apply
	if type(global_budget) == "number" then budget = global_budget end
	if type(global_guard) == "number" then guard = global_guard end

	verbose("loaded global policy")
	if type(global_budget) == "number" then verbose(" [budget]") end
	if type(global_guard) == "number" then verbose(" [guard]") end
	verbose("\n")

	local custom_ifname = {}
	local custom_enable = {}
	local custom_prio = {}
	local custom_portid = {}
	cfg:foreach("poe", "port", function(s)
		local p_id
		local p_prio
		local p_enable
		local p_name
		for k, v in pairs(s) do
			if k == "id" then p_id = tonumber(v) end
			if k == "priority" then p_prio = tonumber(v) end
			if k == "enable" then p_enable = tostring(v) end
			if k == "name" then p_name = tostring(v) end
		end
		if type(p_id) == "number" then
			table.insert(custom_portid, p_id)
			if type(p_prio) == "number" then custom_prio[p_id] = p_prio end
			if type(p_enable) == "string" then
				custom_enable[p_id] = (p_enable ~= "0" and p_enable ~= "false" and p_enable ~= "off" and p_enable ~= "no")
			end
			if type(p_name) == "string" then custom_ifname[p_id] = p_name end
		end
	end)
	-- user supplied pse port id values exist, apply
	if #custom_portid > 0 then portid = custom_portid end
	-- user supplied pse port priorities exist, apply
	if #custom_prio > 0 then portprio = custom_prio end
	-- user supplied pse port enable exist, apply
	if #custom_enable > 0 then portenable = custom_enable end
	-- user supplied eth<->pse interface name/id mapping exist, apply
	if #custom_ifname > 0 then ifname = custom_ifname end

	verbose("loaded per-port policy")
	if #custom_portid > 0 then verbose(" [port-ids: " .. tostring(#custom_portid) .. "]") end
	if #custom_prio > 0 then verbose(" [port-prios: " .. tostring(#custom_prio) .. "]") end
	if #custom_enable > 0 then verbose(" [port-enables: " .. tostring(#custom_enable) .. "]") end
	if #custom_ifname > 0 then verbose(" [port-idnames: " .. tostring(#custom_ifname) .. "]") end
	verbose("\n")

	stats_reload = stats_reload + 1
end

function apply_config(h)
	-- set global policy (budget etc.)
	set_power_limits(h, budget, guard)
	-- set policy for each port (priority etc.)
	local port, prio
	for port, prio in pairs(portprio) do
		set_port_priority(h, port - 1, prio)
	end
end

function run_administrative_actions(h)
	-- enable and disable individual ports
	local port, enable
	for port, enable in pairs(portenable) do
		local bit = enable ~= 0
		if bit then
			enable_port_power(h, port - 1)
		else
			disable_port_power(h, port - 1)
		end
	end
end

function parse_cmdline()
	local i
	for i = 1, #arg do
		if arg[i] == "-v" then log_verbose = true end
		if arg[i] == "-d" then log_debug = true end
		if arg[i] == "-f" then skip_hwcheck = true end
	end
end

-- adjust verbosity etc.
parse_cmdline()
-- quit early if wrong hardware
check_compatible()
-- make sure we can talk to the MCU
local h = configure_uart()
-- grab a port count used to build defaults
count_ports(h)
-- configure final bits and pieces of configurable settings
ensure_defaults()
-- load user config
load_config()
-- apply loaded configuration
apply_config(h)
-- finally, after applying policies, enable and disable power on the ports
run_administrative_actions(h)

uloop.init()

local conn = ubus.connect()
check(conn, "ERROR: could not connect to ubus\n")

local methods = {
	-- could register as eg. poe[0] in case of multiple controllers
	poe = {
		reload = {
			function(req, msg)
				ensure_defaults()
				load_config()
				apply_config(h)
				run_administrative_actions(h)
			end, {}
		},
		stats = {
			function(req, msg)
				local reply = {}
				reply['configuration reloads'] = stats_reload
				reply['messages exchanged'] = stats_msg
				reply['communication timeouts'] = stats_timeout
				reply['frame checksum errors'] = stats_checksum
				reply['notifications received'] = stats_notify
				reply['controller errors'] = stats_error
				conn:reply(req, reply)
			end, {}
		},
		sendframe = {
			function(req, msg)
				local reply = {}
				local hex = tostring(msg.frame)
				if type(hex) ~= "string" then hex = "" end
				hex = string.gsub(hex, '[^%x]', '')
				local xmit = {}
				for k in string.gmatch(hex, "(%x%x)") do table.insert(xmit, tonumber(k, 16)) end
				local recv = exchange_message_once(h, xmit)
				if type(recv) ~= "nil" then reply.frame = string.gsub(table.concat(recv, ' '), '(%d+)', function(k) return string.format('%02x', k) end) end
				conn:reply(req, reply)
			end, {frame = ubus.STRING}
		},
		info = {
			function(req, msg)
				local reply = {}
				local mcu_usage = get_global_power_usage(h)

				-- static values
				reply.mcu = CHIPNAME_MCU
				reply.pse = CHIPNAME_PSE

				-- values applied during last config reload
				reply.budget = string.format("%.1f", budget)
				reply.guard = string.format("%.1f", guard)

				-- values from controller
				reply.poe_ports = poe_ports
				-- seem to report about 4x too high ?
				reply.consumption = string.format("%.1f", mcu_usage)

				reply.ports = {}
				local i
				for i = 1, #portid do
					local item = {}
					local id = portid[i]
					local name = ifname[id]
					local prio = portprio[id]
					local enable = portenable[id]
					local w, ma = get_port_power_usage(h, id - 1)

					-- values applied during last config reload
					if type(name) ~= "nil" then item.name = name end
					if type(prio) ~= "nil" then item.priority = prio end
					if type(enable) ~= "nil" then
						if enable then item.enabled = "yes" else item.enabled = "no" end
					end

					-- values from controller
					item.consumption = string.format("%.1f", w)
					-- figure out command to fetch these
					--item.status = "disable" or "search" or "deliver" or "unknown"
					--item.mode = "poe++" or "poe+" or "poe", alt. "802.3bt" or "802.3at" or "802.3af"

					table.insert(reply.ports, item)
				end
				conn:reply(req, reply)
			end, {}
		},
		manage = {
			function(req, msg)
				local enable = msg.enable
				local portname = msg.port
				if type(enable) == "boolean" and type(portname) == "string" then
					local i
					for i = 1, #portid do
						local id = portid[i]
						local name = ifname[id]
						if name == portname then
							portenable[id] = enable
							if enable then
								enable_port_power(h, id - 1)
							else
								disable_port_power(h, id - 1)
							end
						end
					end
				end
			end, {port = ubus.STRING, enable = ubus.BOOLEAN}
		},
	},
}

conn:add(methods)

uloop.run()

/etc/init.d/poemgr-realtek:

#!/bin/sh /etc/rc.common

START=80
USE_PROCD=1
PROG=/usr/bin/poemgr-realtek

reload_service() {
	ubus call poe reload
}

service_triggers() {
	procd_add_config_trigger "config.change" "poe" ubus call poe reload
}

start_service() {
	procd_open_instance
	procd_set_param command "$PROG"
	procd_set_param respawn
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_close_instance
}

Since it was specifically designed for that new PSE controller...

I'm using it myself on another switch (Zyxel GS1900-10HPv2) having that same PSE controller with great success.

Still waiting for someone to actually test it on the Zyxel GS1900-24EP though (without running realtek-poe at the same time), please feel free to apply if you have this switch.

Where can I find this poemgr-realtek? Your 'here' link above is empty.

Posted right above :wink:

The two poemgr-realtek files are listed in Comment #13 in this thread.

Didn't make an ipk/apk for it yet.

(but let me know if you need one)

Will the switches that currently only had snapshops build available (like GS1900-24EP and DGS-1210-28P) be included in 24.10? It looks like that would be the case: https://openwrt.github.io/firmware-selector-openwrt-org/?version=24.10.0-rc1&target=realtek%2Frtl838x&id=d-link_dgs-1210-28p-f

Yes, that is usually the case. All devices in snapshot are automatically in the next release if they were added before the branch of the 24.10 tree. If not, they can be backported to 24.10.

1 Like

the -28P version has only snapshot builds available as support for it was added last

As I noticed while installing the switch, the -28P is not yet supported in the snapshot, nor is it included in version 24.10, since the merge request is still open. Am I mistaken?

The merge request you linked is for the D-Link 28P device not a ZyXel. And the D-Link images appear to be already available in the snapshot as well as the 24.10.0-rc2 release candidate.

Hi, I tried your lua service, it does not start on my Zyxel GS1900-24EP; I think you're missing a dependency: libubox-lua -- and I can't start it, it seems the boardcheck fails.

$ /usr/bin/poemgr-realtek -d -v
Warning: Bad CRC, using default environment
## Error: "boardmodel" not defined
ERROR: unknown hardware model or revision
/usr/bin/lua: /usr/bin/poemgr-realtek:130: assertion failed!

Zyxel GS1900-24EP Switch -- OpenWrt 24.10.0 r28427-6df0e3d02a / LuCI openwrt-24.10 branch 25.014.55016~7046a1c

The boardcheck fails, reading your code I guess this does not include boardname:

$ fw_printenv
Warning: Bad CRC, using default environment
bootcmd=run distro_bootcmd
bootdelay=2
baudrate=115200
loadaddr=0x0
mtdids=
mtdparts=
bootm_size=0x10000000
eth6addr=02:00:11:22:33:47
ethaddr=02:00:11:22:33:44
fdt_addr_r=0xc00000
ipaddr=192.0.2.1
kernel_addr_r=0x1000000
pxefile_addr_r=0x2000
ramdisk_addr_r=0x2000000
scriptaddr=0x1000
stderr=serial,vidconsole
stdin=serial
stdout=serial,vidconsole

pseId seems to be okay:

fw_printsys -n pseId
4