EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Network / ตั้ง VLAN บนสวิตช์ Hikvision ด้วย API ชุดเดียวกับที่หน้าเว็บมันใช้
Network

ตั้ง VLAN บนสวิตช์ Hikvision ด้วย API ชุดเดียวกับที่หน้าเว็บมันใช้

โดย Ton-Or ·17 ก.ย. 2026·อ่าน 9 นาที
ตั้ง VLAN บนสวิตช์ Hikvision ด้วย API ชุดเดียวกับที่หน้าเว็บมันใช้

อุปกรณ์ราคาไม่แพงหลายตัวไม่มี API ให้ใช้อย่างเป็นทางการ แต่หน้าเว็บของมันต้องคุยกับตัวเครื่องด้วยอะไรสักอย่างอยู่แล้ว งานของเราคือไปดูว่ามันคุยด้วยอะไร แล้วพูดภาษาเดียวกันนั้น

สรุปใน 1 นาที

อาการ/ปัญหา
สวิตช์ smart managed ตอบ 403 ที่ ISAPI หมวด Switch เลยตั้ง VLAN ด้วยสคริปต์ไม่ได้
ใครควรอ่าน
คนดูแลสวิตช์ Hikvision ที่ไม่มี API ทางการ
ทางแก้แบบสั้น
ดูว่าหน้าเว็บยิงอะไร แล้วเขียนตัวตั้ง VLAN แบบ dry-run ก่อน apply ระวังชื่อฟิลด์อ่านกับเขียนไม่ตรง โหมด CCTV 10 Mbps และงบ PoE
ข้ามไปขั้นตอน

พอดีผมต้องแทรกสวิตช์ Hikvision รุ่น smart managed ตัวหนึ่งเข้าไปในระบบที่มี UniFi เป็นแกนอยู่แล้ว หน้าที่ของมันคือรับกล้องเจ็ดตัวกับ NVR หนึ่งตัวเข้า VLAN ของกล้อง แล้วส่ง trunk กลับขึ้นไปหาสวิตช์ตัวแม่ ทำมือผ่านหน้าเว็บก็ได้ครับ กดสิบพอร์ตไม่ตายหรอก แต่งานนี้มีสวิตช์แบบเดียวกันรอติดตั้งอีกหลายจุด และผมต้องการของที่ทำซ้ำได้เหมือนเดิมทุกครั้ง กับที่บอกได้ก่อนกดว่าจะเปลี่ยนอะไรบ้าง เลยนั่งถอด API ของหน้าเว็บมันออกมาเขียนเป็นสคริปต์

เลข IP ชื่อโซน และรหัสผ่านในบทความเป็นค่าตัวอย่าง ส่วน path ของ API ชื่อฟิลด์ และตัวเลขงบไฟคือของจริง อุปกรณ์ที่ยิงคือเครื่องที่ผมถืออยู่ในมือ ตั้งค่าด้วยบัญชีผู้ดูแลที่ผมตั้งเอง

ISAPI หมวด Switch ตอบ 403 แต่หน้าเว็บมันทำได้

ยี่ห้อนี้ใช้ ISAPI กับกล้องและ NVR ผมเลยลองยิง /ISAPI/... หมวดที่เกี่ยวกับสวิตช์ดูก่อน ได้ 403 กลับมาหมด ซึ่งก็สมเหตุสมผลเพราะสวิตช์ตระกูลนี้ไม่ได้อยู่ในสายผลิตภัณฑ์เดียวกับกล้องจริงๆ ตอนแรกผมคิดว่าจบแล้ว ต้องกดมือ

แต่หน้าเว็บมันตั้ง VLAN ได้ แปลว่าต้องมีช่องทางอยู่ ผมเปิด developer tools ของเบราว์เซอร์ค้างไว้ แล้วกดเปลี่ยน VLAN ของพอร์ตหนึ่งพอร์ต ดูว่ามันยิงอะไรออกไป คำตอบคือ POST ไปที่ path ที่หน้าตาไม่เหมือน REST ปกติเลย

เส้นทางที่ใช้จริงสคริปต์requests + digest->POST /iot/global/0-global/modelพอร์ต 80->service/operate/L2MgrSearchVLAN AddVLAN Modify->สวิตช์ตั้งค่าจริง

รูปแบบมันคงที่มาก ทุกคำสั่งเป็น POST ห่อ body ด้วย {"data": {...}} ใช้ digest auth เหมือนกันหมด และคำตอบเป็น JSON ที่มี status อยู่ข้างใน ซึ่ง ไม่ใช่ HTTP status ตัวนี้คือกับดักแรก HTTP อาจจะ 200 แต่ status ใน body เป็นอย่างอื่นพร้อม errorMsg ถ้าเช็กแค่ r.status_code จะคิดว่าสำเร็จทั้งที่ไม่ได้ทำอะไรเลย

import os, uuid, requests
from requests.auth import HTTPDigestAuth

HOST = os.environ.get("HIKSW_HOST", "192.168.10.30")
USER = os.environ.get("HIKSW_USER", "admin")
PASS = os.environ.get("HIKSW_PASS", "")
BASE = f"http://{HOST}/iot/global/0-global/model"

def api(path, data=None, method="post"):
    r = requests.request(method, f"{BASE}/{path}",
                         auth=HTTPDigestAuth(USER, PASS),
                         json={"data": data or {}}, timeout=30)
    try:
        j = r.json()
    except ValueError:
        raise RuntimeError(f"{path}: HTTP {r.status_code} {r.text[:200]}")
    if j.get("status") != 200:                 # status อยู่ใน body ไม่ใช่ HTTP
        raise RuntimeError(f"{path}: {j.get('code')} {j.get('errorMsg')}")
    return j.get("data", {})

รหัสผ่านอ่านจาก environment ครับ ไม่ฝังในไฟล์ ตัวสคริปต์เช็กก่อนเลยว่าถ้าไม่มีค่าให้หยุดทันที จะได้ไม่ยิงไปด้วยรหัสว่างแล้วงงว่าทำไม 401

export HIKSW_HOST=192.168.10.30
export HIKSW_USER=admin
export HIKSW_PASS='<PASSWORD>'
python hikswitch.py status

การค้นข้อมูลเป็นแบบแบ่งหน้า ต้องวนเอง

คำสั่งกลุ่มที่ขึ้นต้นด้วย Search ไม่ได้คืนทุกแถวในทีเดียว มันคืนเป็นหน้า พร้อมบอกใน responseStatus ว่ายังมีต่อไหม ตัวที่ต้องส่งไปคือ searchID ซึ่งผมสร้างเป็น uuid ใหม่ทุกครั้ง กับ searchResultPosition ที่ต้องบวกเองตามจำนวนที่ได้มา

def search(path, **extra):
    out, pos = [], 1
    while True:
        d = api(path, {"searchID": str(uuid.uuid4()),
                       "searchResultPosition": pos,
                       "maxResults": 500, **extra})
        rows = d.get("matchResults") or []
        out += rows
        if d.get("responseStatus") != "MORE" or not rows:
            return out
        pos += d.get("numOfMatches", len(rows))

def get_vlans():
    return sorted(v["VLANID"] for v in search("service/operate/L2Mgr/SearchVLAN"))

def get_ports():
    return {p["portID"]: p for p in search("service/operate/L2Mgr/SearchPortVLAN")}

เงื่อนไข not rows ที่ต่อท้ายไม่ได้ใส่เผื่อสวย ผมเจอเคสที่มันตอบ MORE มาแต่ matchResults ว่าง ถ้าไม่มีบรรทัดนั้นคือวนไม่รู้จบ

กับดักที่ทำให้เขียนแล้วไม่มีอะไรเปลี่ยน

อ่านคืนมาเป็น VLANType แต่ตอนเขียนต้องส่ง vlanType

ตัวนี้กินเวลาผมไปนานที่สุดในงานนี้ ตอนอ่านสถานะพอร์ต ฟิลด์ที่บอกโหมดชื่อ VLANType ตัว V ใหญ่ทั้งสามตัว แต่ตอนส่งคำสั่งแก้ ต้องส่งชื่อ vlanType ตัวเล็กขึ้นต้น ถ้าส่งชื่อเดียวกับที่อ่านมาสวิตช์ตอบ 200 เฉยๆ แล้วไม่เปลี่ยนอะไรเลย ไม่มี error ไม่มีคำเตือน กลับไปอ่านใหม่ก็ยังเป็นค่าเดิม ผมเลยเขียนตัวอ่านให้รับทั้งสองชื่อไว้ก่อน

def describe(p):
    # อุปกรณ์ตอบกลับเป็น VLANType แต่ตอนเขียนใช้ vlanType (API ไม่สม่ำเสมอ)
    if (p.get("VLANType") or p.get("vlanType")) == "access":
        return f"access VLAN {(p.get('accessMode') or {}).get('VLANID')}"
    t = p.get("trunkMode") or {}
    return f"trunk PVID {t.get('pVid')} allowed [{t.get('VLANIDList')}]"

บทเรียนกว้างกว่านั้นคือ อย่าเชื่อว่า 200 แปลว่าสำเร็จ กับ API ที่ไม่ได้ประกาศเป็นทางการ ทุกคำสั่งเขียนที่ผมยิง ผมจะอ่านค่ากลับมาเทียบใหม่เสมอ ไม่ได้เช็กแค่ code ที่ตอบมา

ทำแผนไว้ในไฟล์ แล้วให้สคริปต์บอกว่าจะเปลี่ยนอะไร

ผมไม่ชอบสคริปต์ที่รันแล้วแก้ทันที ของแบบนี้พลาดทีเดียวคือคนทั้งตึกเน็ตหลุด แผนพอร์ตผมเลยเขียนเป็น dict ไว้ในไฟล์ตรงๆ อ่านง่าย เห็นครบในหน้าจอเดียว แล้วมีสองคำสั่งคือ plan กับ apply

TRUNK_VLANS = "1,20,30,40,50,100"    # ทุก VLAN ที่ต้องวิ่งผ่าน uplink
CAM_VLAN = 20

PLAN = {
    1:  ("access", CAM_VLAN, "กล้อง 1"),
    2:  ("access", CAM_VLAN, "กล้อง 2"),
    3:  ("access", CAM_VLAN, "กล้อง 3"),
    4:  ("access", CAM_VLAN, "กล้อง 4"),
    5:  ("access", CAM_VLAN, "กล้อง 5"),
    6:  ("access", CAM_VLAN, "กล้อง 6"),
    7:  ("access", CAM_VLAN, "กล้อง 7"),
    8:  ("access", CAM_VLAN, "NVR"),
    9:  ("trunk", 1, "AP (ผ่าน PoE injector แยก)"),
    10: ("trunk", 1, "uplink -> สวิตช์ตัวแม่"),
}

ตัว plan จะอ่านสถานะจริงมาเทียบทีละพอร์ต แล้วพิมพ์ว่าอันไหนตรงอยู่แล้ว อันไหนจะเปลี่ยน พร้อมบอกว่ามี VLAN ไหนที่ยังไม่ได้สร้างบ้าง ตอน apply ก็สร้าง VLAN ที่ขาดก่อน แล้วค่อยยัดพอร์ตทั้งชุดไปในคำสั่งเดียวด้วย ModifyBatchPortVLAN

แผนพอร์ตของสวิตช์ 10 พอร์ต1กล้อง2กล้อง3กล้อง4กล้อง5กล้อง6กล้อง7กล้อง8NVR9AP10uplinkaccess VLAN 20 (VLAN ของกล้อง)trunk PVID 1allowed 1,20,30,40,50,100แตะพอร์ต 10 เป็นตัวสุดท้าย เพราะคำสั่งวิ่งผ่านพอร์ตนี้อยู่
พอร์ต 1-7 กล้อง พอร์ต 8 NVR อยู่ VLAN 20 พอร์ต 9 ต่อ AP พอร์ต 10 เป็น uplink ไปสวิตช์ตัวแม่
for pid, (mode, pvid, note) in PLAN.items():
    cur = ports.get(pid)
    if cur is None:
        print(f"  [WARN] ไม่พบ port {pid} บนอุปกรณ์")
        continue
    if mode == "access":
        item = {"portID": pid, "vlanType": "access", "accessMode": {"VLANID": pvid}}
        newdesc = f"access VLAN {pvid}"
    else:
        item = {"portID": pid, "vlanType": "trunk",
                "trunkMode": {"VLANIDList": TRUNK_VLANS, "pVid": pvid}}
        newdesc = f"trunk PVID {pvid} allowed [{TRUNK_VLANS}]"
    old = describe(cur)
    mark = "  " if old == newdesc else "->"
    print(f"  {mark} port {pid:<2} {note:<28} {old}  =>  {newdesc}")
    if old != newdesc:
        changes.append(item)

if not apply:
    print(f"\n[DRY-RUN] จะเพิ่ม VLAN {missing} และแก้ {len(changes)} พอร์ต - สั่ง apply เพื่อยืนยัน")
    return
for v in missing:
    api("service/operate/L2Mgr/AddVLAN", {"VLANID": v})
if changes:
    api("service/operate/L2Mgr/ModifyBatchPortVLAN", {"portList": changes})
พอร์ตที่ตัวเองเชื่อมผ่านอยู่ ต้องเป็นตัวสุดท้ายเสมอ

ตอนตั้งค่าผมนั่งอยู่หลังสวิตช์ตัวนั้นเอง คำสั่งวิ่งผ่านพอร์ต uplink ที่กำลังจะแก้ ถ้าแก้พอร์ตนั้นก่อนแล้วค่าใหม่ทำให้ mgmt ของเราหลุด สคริปต์จะค้างกลางทางโดยที่พอร์ตอื่นยังไม่ได้ตั้ง เท่ากับต้องเดินไปกด reset ที่หน้าเครื่อง ผมเลยเรียงพอร์ตให้พอร์ตปลายทางเสร็จก่อนทั้งหมด แล้วค่อยแตะ uplink เป็นตัวสุดท้าย และก่อนแตะจะเปิดหน้าต่างอีกอันค้างไว้เพื่อเช็กว่ายังคุยได้อยู่

โหมด CCTV ที่ตั้งด้วยปุ่ม ไม่ได้อยู่ใน API

เรื่องแรกที่ต้องเดินไปดูที่ตัวเครื่องคือโหมด CCTV หรือ Extend ที่สวิตช์ตระกูลนี้ชอบมี บางรุ่นเป็นสวิตช์ DIP บางรุ่นเป็นปุ่มหน้าเครื่อง มันมีไว้ยิงสายไกลถึงสองร้อยห้าสิบเมตร แลกมาด้วยการ บังคับพอร์ตนั้นเหลือ 10 Mbps half-duplex

ใช้ได้

พอร์ตปลายทางที่สายยาวจริง

กล้องตัวเดียวปลายสายสองร้อยเมตร บิตเรตไม่กี่เมกะ ยอมได้ ดีกว่าต้องไปวางตู้กลางทาง แต่ต้องรู้ตัวว่าเลือกแล้ว และจดไว้ในทะเบียนว่าพอร์ตไหนอยู่โหมดนี้

ห้ามใช้

พอร์ต uplink และพอร์ตที่ต่อ NVR

uplink ที่ต้องแบกกล้องเจ็ดตัวพร้อมกันเหลือ 10 Mbps half-duplex คือภาพกระตุกทั้งระบบ แล้วอาการที่โผล่ออกมาจะเหมือนกล้องเสียหรือ NVR ช้า ไล่หาสาเหตุกันเป็นวัน ทั้งที่ปัญหาอยู่ที่ปุ่มตัวเดียว

เวลาเช็ก ผมดูจากความเร็วที่พอร์ตเจรจาได้จริง เพราะโหมดนี้บังคับเลขไว้ตายตัว เห็น 10 เมื่อไหร่คือติดโหมดแน่นอน ไม่ใช่สายเสีย

# ดึงสถานะทุกพอร์ตออกมาแล้วมองหาค่าที่ผิดปกติ
python hikswitch.py status | grep -Ei '10 ?M|half'

# พอร์ตที่ควรเป็นกิกะบิตแต่ได้ 100 คือสายมีปัญหา
# พอร์ตที่ได้ 10 half-duplex คือติดโหมด CCTV ที่ตัวเครื่อง คนละสาเหตุกัน
python hikswitch.py status | awk '/port/ && !/1000/ {print "ตรวจ:", $0}'

งบไฟ PoE ต้องบวกเลขก่อนซื้อ ไม่ใช่ก่อนเสียบ

เรื่องที่สองคืองบไฟ PoE สวิตช์ที่ผมใช้จ่ายได้รวม 60 W ส่วนกล้องที่จะเสียบมีเจ็ดตัว ตัวละราวเก้าวัตต์

7 x 9Wกล้องที่จะเสียบ
63Wที่ต้องการจริง
60Wงบของสวิตช์
10 Mbpsถ้าติดโหมด CCTV

63 เกิน 60 ครับ เกินอยู่สามวัตต์ ซึ่งฟังดูนิดเดียวแต่พอสวิตช์ถึงเพดานมันจะตัดไฟพอร์ตท้ายๆ ทิ้ง แล้วอาการที่ได้คือกล้องสองสามตัวหลุดๆ ติดๆ ไม่เป็นเวลา ซึ่งไล่หาสาเหตุยากมากเพราะทุกอย่างดูปกติหมด แผนของผมเลยย้าย AP ที่จะเสียบพอร์ต 9 ไปใช้ PoE injector แยกของมันเอง เหลือให้กล้องล้วน 63 W ก็ยังเกินอยู่ดี ตอนติดตั้งจริงเลยต้องแยก injector ให้กล้องสองตัวด้วย

คิดงบไฟเผื่อไว้ที่ 80 เปอร์เซ็นต์

ตัวเลขบนกล่องคือค่าที่เครื่องจ่ายได้สูงสุดในสภาพดี ไม่ใช่ค่าที่ควรใช้ประจำ ส่วนตัวผมคิดที่ราว 80 เปอร์เซ็นต์ของงบเสมอ เผื่อกล้องที่กินไฟเพิ่มตอนเปิดฮีตเตอร์ในหน้าหนาว หรือตอนเปิดไฟ IR กลางคืน ซึ่งไม่เท่ากับตัวเลขที่ผู้ผลิตเขียนไว้บนสเปกกลางวัน

เช็กหลังตั้งเสร็จ แล้วเก็บสถานะไว้เทียบ

  1. รัน plan ก่อนทุกครั้ง อ่านให้ครบว่าจะเปลี่ยนกี่พอร์ต ถ้ามีพอร์ตที่ไม่ได้ตั้งใจแตะโผล่มาแปลว่าแผนในไฟล์ผิด
  2. รัน apply แล้วให้สคริปต์อ่านสถานะกลับมาพิมพ์ซ้ำทันที ไม่เชื่อ code ที่ตอบมาอย่างเดียว
  3. เดินไปดูไฟหน้าเครื่องว่าพอร์ตที่ควรขึ้นกิกะบิตขึ้นจริง ไม่ใช่ค้างที่ร้อยเมกะหรือสิบเมกะ
  4. เช็กที่สวิตช์ตัวแม่ว่า trunk เห็น VLAN ครบตามที่ตั้ง และไม่มี VLAN แปลกปลอมหลุดขึ้นไป
  5. เก็บผล status ลงไฟล์ตามวันที่ ครั้งหน้ามีใครไปกดอะไรที่หน้าเว็บ จะได้ diff เจอ
python hikswitch.py status | tee "sw-$(date +%F).txt"
diff sw-2026-08-18.txt sw-2026-09-18.txt

ส่วนตัวผมมองว่าสวิตช์ระดับนี้ไม่ควรเอามาทำอะไรซับซ้อนกว่านี้ครับ ตั้ง VLAN แยกกล้องออกจากวงอื่นก็พอแล้ว ส่วนกฎ ส่วนการควบคุมเส้นทาง ปล่อยให้สวิตช์ตัวแม่กับเราเตอร์ทำ เพราะของราคานี้เวลามีปัญหาเราแทบไม่มีเครื่องมือดูข้างในมันเลย ยิ่งซ้อนความซับซ้อนไว้ที่ตัวมันมาก วันที่พังก็ยิ่งหาสาเหตุยาก ใครมีรุ่นที่คุมได้ละเอียดกว่านี้จะจัดเต็มก็ตามสะดวก

ที่ยังค้างคือทำตัวอ่านสถานะ PoE รายพอร์ตให้เห็นวัตต์จริงที่จ่ายอยู่ จะได้เลิกเดาจากสเปก กับทำตัวเทียบแผนอัตโนมัติแล้วแจ้งเตือนเวลาค่าจริงหลุดจากแผน ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย