ตั้ง 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 ปกติเลย
รูปแบบมันคงที่มาก ทุกคำสั่งเป็น 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 ตัว 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
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 ส่วนกล้องที่จะเสียบมีเจ็ดตัว ตัวละราวเก้าวัตต์
63 เกิน 60 ครับ เกินอยู่สามวัตต์ ซึ่งฟังดูนิดเดียวแต่พอสวิตช์ถึงเพดานมันจะตัดไฟพอร์ตท้ายๆ ทิ้ง แล้วอาการที่ได้คือกล้องสองสามตัวหลุดๆ ติดๆ ไม่เป็นเวลา ซึ่งไล่หาสาเหตุยากมากเพราะทุกอย่างดูปกติหมด แผนของผมเลยย้าย AP ที่จะเสียบพอร์ต 9 ไปใช้ PoE injector แยกของมันเอง เหลือให้กล้องล้วน 63 W ก็ยังเกินอยู่ดี ตอนติดตั้งจริงเลยต้องแยก injector ให้กล้องสองตัวด้วย
ตัวเลขบนกล่องคือค่าที่เครื่องจ่ายได้สูงสุดในสภาพดี ไม่ใช่ค่าที่ควรใช้ประจำ ส่วนตัวผมคิดที่ราว 80 เปอร์เซ็นต์ของงบเสมอ เผื่อกล้องที่กินไฟเพิ่มตอนเปิดฮีตเตอร์ในหน้าหนาว หรือตอนเปิดไฟ IR กลางคืน ซึ่งไม่เท่ากับตัวเลขที่ผู้ผลิตเขียนไว้บนสเปกกลางวัน
เช็กหลังตั้งเสร็จ แล้วเก็บสถานะไว้เทียบ
- รัน
planก่อนทุกครั้ง อ่านให้ครบว่าจะเปลี่ยนกี่พอร์ต ถ้ามีพอร์ตที่ไม่ได้ตั้งใจแตะโผล่มาแปลว่าแผนในไฟล์ผิด - รัน
applyแล้วให้สคริปต์อ่านสถานะกลับมาพิมพ์ซ้ำทันที ไม่เชื่อ code ที่ตอบมาอย่างเดียว - เดินไปดูไฟหน้าเครื่องว่าพอร์ตที่ควรขึ้นกิกะบิตขึ้นจริง ไม่ใช่ค้างที่ร้อยเมกะหรือสิบเมกะ
- เช็กที่สวิตช์ตัวแม่ว่า trunk เห็น VLAN ครบตามที่ตั้ง และไม่มี VLAN แปลกปลอมหลุดขึ้นไป
- เก็บผล
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
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย