Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package chirp for openSUSE:Factory checked 
in at 2026-04-18 21:39:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/chirp (Old)
 and      /work/SRC/openSUSE:Factory/.chirp.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "chirp"

Sat Apr 18 21:39:27 2026 rev:66 rq:1347976 version:20260417

Changes:
--------
--- /work/SRC/openSUSE:Factory/chirp/chirp.changes      2026-04-11 
22:29:49.876874216 +0200
+++ /work/SRC/openSUSE:Factory/.chirp.new.11940/chirp.changes   2026-04-18 
21:39:49.142042088 +0200
@@ -1,0 +2,15 @@
+Sat Apr 18 19:11:01 UTC 2026 - Andreas Stieger <[email protected]>
+
+- Update to version 20260417:
+  * Add Wouxun XS20 series channel memory support
+  * Wouxun 935/8h series extra memory settings
+  * TK-372G correct variants and names
+  * Add BF-V12D support
+  * Improve cache performance for RepeaterBook queries
+  * Improve reliability of network source requests and bug reports
+  * radtel_rt900: Fix flake8 E501 line too long in FHSS Code setting
+  * radtel_rt900: Fix FHSS Code init crash on out-of-range image values
+  * radtel_rt900: Expose FHSS Code per-channel setting
+  * radtel_rt900: Expose LearnFHSS per-channel setting
+
+-------------------------------------------------------------------

Old:
----
  chirp-20260410.obscpio

New:
----
  chirp-20260417.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ chirp.spec ++++++
--- /var/tmp/diff_new_pack.BjXYI9/_old  2026-04-18 21:39:49.878072228 +0200
+++ /var/tmp/diff_new_pack.BjXYI9/_new  2026-04-18 21:39:49.882072391 +0200
@@ -20,7 +20,7 @@
 
 %define pythons python3
 Name:           chirp
-Version:        20260410
+Version:        20260417
 Release:        0
 Summary:        Tool for programming amateur radio sets
 License:        GPL-3.0-only

++++++ _service ++++++
--- /var/tmp/diff_new_pack.BjXYI9/_old  2026-04-18 21:39:49.946075013 +0200
+++ /var/tmp/diff_new_pack.BjXYI9/_new  2026-04-18 21:39:49.950075176 +0200
@@ -4,8 +4,8 @@
     <param name="scm">git</param>
     <param name="changesgenerate">enable</param>
     <param name="filename">chirp</param>
-    <param name="versionformat">20260410</param>
-    <param name="revision">6025bd4d75cbdf54cea51688e5645c63568701ed</param>
+    <param name="versionformat">20260417</param>
+    <param name="revision">fc18cb8d9128739e386936412bf203dcfd71f671</param>
   </service>
   <service mode="manual" name="set_version"/>
   <service name="tar" mode="buildtime"/>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.BjXYI9/_old  2026-04-18 21:39:50.002077306 +0200
+++ /var/tmp/diff_new_pack.BjXYI9/_new  2026-04-18 21:39:50.010077633 +0200
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param name="url">https://github.com/kk7ds/chirp.git</param>
-    <param 
name="changesrevision">6025bd4d75cbdf54cea51688e5645c63568701ed</param>
+    <param 
name="changesrevision">fc18cb8d9128739e386936412bf203dcfd71f671</param>
   </service>
 </servicedata>
 (No newline at EOF)

++++++ chirp-20260410.obscpio -> chirp-20260417.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/drivers/baofeng_digital.py 
new/chirp-20260417/chirp/drivers/baofeng_digital.py
--- old/chirp-20260410/chirp/drivers/baofeng_digital.py 2026-04-10 
03:18:54.000000000 +0200
+++ new/chirp-20260417/chirp/drivers/baofeng_digital.py 2026-04-17 
00:26:24.000000000 +0200
@@ -484,3 +484,68 @@
     ]
     _serial_id = 0x05
     _proto = PROTO_D
+
+
[email protected]
+class BaofengBFV12D(BaofengDigital):
+    """Baofeng BF-V12D"""
+    MODEL = 'BF-V12D'
+    VALID_BANDS = [(400000000, 480000000)]
+    POWER_LEVELS = [
+        chirp_common.PowerLevel("Low", watts=0.5),
+        chirp_common.PowerLevel("High", watts=2.00),
+    ]
+    _serial_id = 0x0a
+    _proto = PROTO_A
+
+    def _get_radio_id(self):
+        # The V12D requires a ucbfpwd unlock sequence before entering
+        # programming mode, observed from factory software captures.
+        for attempt in range(5):
+            try:
+                baofeng_common._clean_buffer(self)
+
+                # Send unlock command
+                self.pipe.write(b'\x02ucbfpwd')
+                resp = self.pipe.read(1)
+                LOG.debug('ucbfpwd response: %s',
+                          resp.hex() if resp else 'empty')
+                if resp != CMD_ACK:
+                    raise errors.RadioError(
+                        'No ACK on unlock command (got %s)'
+                        % (resp.hex() if resp else 'empty'))
+
+                # Send unlock data and model identifier
+                # (observed from factory software captures)
+                self.pipe.write(b'\x10\xc5\xea\x35')
+                resp = self.pipe.read(1)
+                LOG.debug('unlock data response: %s',
+                          resp.hex() if resp else 'empty')
+                if resp != CMD_ACK:
+                    raise errors.RadioError(
+                        'No ACK on unlock data')
+
+                # Base64-encoded model identifier from factory software
+                self.pipe.write(
+                    b'Q041OTUwMA==\x00\x00\x00\x00')
+                resp = self.pipe.read(1)
+                LOG.debug('model id response: %s',
+                          resp.hex() if resp else 'empty')
+                if resp != CMD_ACK:
+                    raise errors.RadioError(
+                        'No ACK on model identifier')
+
+                # Now request radio ID
+                self.pipe.write(b'\x02prOGRAM')
+                resp = self.pipe.read(4)
+                LOG.debug('radio ID: %s',
+                          resp.hex() if resp else 'empty')
+                if len(resp) != 4:
+                    raise errors.RadioError(
+                        'Radio did not send identification')
+                return resp
+            except errors.RadioError:
+                if attempt == 4:
+                    raise
+                self._exit_programming()
+                time.sleep(1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/drivers/kg935g.py 
new/chirp-20260417/chirp/drivers/kg935g.py
--- old/chirp-20260410/chirp/drivers/kg935g.py  2026-04-10 03:18:54.000000000 
+0200
+++ new/chirp-20260417/chirp/drivers/kg935g.py  2026-04-17 00:26:24.000000000 
+0200
@@ -92,6 +92,21 @@
     (0x4780, 32, 375),  # Memory Names 0-999
     (0x7670, 8, 125),   # Ch Valid bytes 0-999
     )
+config_map_x20h = (     # map address, write size, write count
+    # (0x00,   64, 512),  #- Use for full upload testing
+    # (0x44,   32, 1),    # Freq Limits
+    # (0x440,  8,  1),     # Area Message
+    # (0x480,  8, 5),    # Scan Groups
+    # (0x500,  8, 15),    # Call Codes
+    # (0x580,  8, 15),    # Call Names
+    # (0x600,  8, 5),    # FM Presets
+    # (0x800,  64, 2),    # settings
+    # (0x880,  16, 1),    # VFO A
+    # (0x8C0,  16, 1),    # VFO B
+    (0x900,  64, 250),  # Channel Memory 0-999
+    (0x4780, 32, 375),  # Memory Names 0-999
+    (0x7670, 8, 125),   # Ch Valid bytes 0-999
+    )
 
 AB_LIST = ["A", "B"]
 STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
@@ -697,7 +712,7 @@
                 power:4;
         u8      unknown3:2,
                 scan_add:1,
-                unknown4:1,
+                favorite:1,
                 compander:1,
                 mute_mode:2,
                 iswide:1;
@@ -1246,7 +1261,7 @@
                 power:4;
         u8      unknown3:2,
                 scan_add:1,
-                unknown4:1,
+                favorite:1,
                 compander:1,
                 mute_mode:2,
                 iswide:1;
@@ -1331,6 +1346,8 @@
                     chirp_common.PowerLevel("H", watts=5.5)]
     _record_start = 0x7C
     config_map = config_map_935G
+    HAS_SCRAMBLER = True
+    HAS_FAVORITE = False
 
     def __init__(self, pipe):
         super().__init__(pipe)
@@ -1339,6 +1356,29 @@
             pol_mask=0x2000,
             tone_init=0x0000)
 
+    def get_extra(self, _mem, mem):
+        mem.extra = RadioSettingGroup("Extra", "Extra")
+        _mem.mute_mode = 2 if _mem.mute_mode > 2 else _mem.mute_mode
+        rs = RadioSetting("mute_mode", "Mute Mode",
+                          RadioSettingValueList(
+                              SPMUTE_LIST, current_index=_mem.mute_mode))
+        mem.extra.append(rs)
+        if self.HAS_SCRAMBLER:
+            rs = RadioSetting("scrambler", "Scramble Descramble",
+                              RadioSettingValueList(
+                                 SCRAMBLE_LIST, current_index=_mem.scrambler))
+            mem.extra.append(rs)
+        rs = RadioSetting("compander", "Compander",
+                          RadioSettingValueList(
+                             ONOFF_LIST, current_index=_mem.compander))
+        mem.extra.append(rs)
+        if self.HAS_FAVORITE:
+            rs = RadioSetting("favorite", "Favorite",
+                              RadioSettingValueList(
+                                 ONOFF_LIST, current_index=_mem.favorite))
+            mem.extra.append(rs)
+        return
+
     def _write_record(self, cmd, payload=b''):
         _packet = struct.pack('BBBB', self._record_start, cmd, 0xFF,
                               len(payload))
@@ -1557,6 +1597,17 @@
     def get_raw_memory(self, number):
         return repr(self._memobj.memory[number])
 
+    def _get_power(self, _mem, mem):
+        _mem.power = _mem.power & 0x3
+        try:
+            mem.power = self.POWER_LEVELS[_mem.power]
+        except IndexError:
+            mem.power = self.POWER_LEVELS[-1]
+
+    def _set_power(self, mem):
+        temp_val = self.POWER_LEVELS.index(mem.power)
+        return temp_val
+
     def get_memory(self, number):
         _mem = self._memobj.memory[number]
         _nam = self._memobj.names[number]
@@ -1591,13 +1642,11 @@
                 mem.name += chr(char)
         mem.name = mem.name.rstrip()
 
+        self.get_extra(_mem, mem)
         self.tone_model.get_tone(_mem, mem)
 
         mem.skip = "" if bool(_mem.scan_add) else "S"
-        _mem.power = _mem.power & 0x3
-        if _mem.power > 2:
-            _mem.power = 2
-        mem.power = self.POWER_LEVELS[_mem.power]
+        self._get_power(_mem, mem)
         mem.mode = _mem.iswide and "FM" or "NFM"
         return mem
 
@@ -1631,21 +1680,15 @@
         _mem.iswide = int(mem.mode == "FM")
         # set the tone
         self.tone_model.set_tone(mem, _mem)
-        # MRT set the scrambler and compander to off by default
-        # MRT This changes them in the channel memory
-        _mem.scrambler = 0
-        _mem.compander = 0
         # set the power
         _mem.power = _mem.power & 0x3
-        if mem.power:
-            if _mem.power > 2:
-                _mem.power = 2
-            _mem.power = self.POWER_LEVELS.index(mem.power)
+        if mem.power is not None:
+            _mem.power = self._set_power(mem)
         else:
             _mem.power = True
-        # MRT set to mute mode to QT (not QT+DTMF or QT*DTMF) by default
-        # MRT This changes them in the channel memory
-        _mem.mute_mode = 0
+
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
 
         # MRT it is unknown what impact these values have
         # MRT This changes them in the channel memory to match what
@@ -1655,8 +1698,6 @@
         # _mem.unknown1 = 0
         # MRT Set to 3 to TO MATCH CPS VALUES
         _mem.unknown3 = 3
-        # MRT Set to 1 to TO MATCH CPS VALUES
-        _mem.unknown4 = 1
         # MRT set unknown5 to 1 and unknown6 to 0
         _mem.unknown5 = 1
         _mem.unknown6 = 255
@@ -2419,6 +2460,8 @@
     """Wouxun KG-935G Plus"""
     VENDOR = "Wouxun"
     MODEL = "KG-935G Plus"
+    HAS_SCRAMBLER = True
+    HAS_FAVORITE = True
 
     def process_mmap(self):
         self._memobj = bitwise.parse(_MEM_FORMAT_935GPLUS, self._mmap)
@@ -2445,6 +2488,58 @@
                     chirp_common.PowerLevel("M", watts=5.0),
                     chirp_common.PowerLevel("H", watts=8.0)]
     config_map = config_map_935H
+    HAS_SCRAMBLER = True
+    HAS_FAVORITE = True
 
     def process_mmap(self):
         self._memobj = bitwise.parse(_MEM_FORMAT_935H, self._mmap)
+
+
[email protected]
+class KGXS20G(KG935GRadio):
+
+    """Wouxun KG-XS20G"""
+    VENDOR = "Wouxun"
+    MODEL = "KG-XS20G"
+    _model = b"KG-UV8D-A"
+    _record_start = 0x79
+    config_map = config_map_x20h
+    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=0.5),
+                    chirp_common.PowerLevel("H", watts=5.5)]
+    HAS_SCRAMBLER = False
+    HAS_FAVORITE = False
+
+    def get_features(self):
+        rf = super().get_features()
+        rf.has_settings = False  # Set to False until settings are mapped
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(_MEM_FORMAT_935GPLUS, self._mmap)
+
+    def _get_power(self, _mem, mem):
+        if _mem.power == 0:
+            mem.power = self.POWER_LEVELS[0]
+        else:
+            mem.power = self.POWER_LEVELS[1]
+
+    def _set_power(self, mem):
+        temp_val = 0 if mem.power == self.POWER_LEVELS[0] else 2
+        return temp_val
+
+
[email protected]
+class KGXS20HRadio(KGXS20G):
+
+    """Wouxun KG-XS20H"""
+    VENDOR = "Wouxun"
+    MODEL = "KG-XS20H"
+    HAS_SCRAMBLER = True
+
+
[email protected]
+class KGXS20GPlusRadio(KGXS20G):
+
+    """Wouxun KG-XS20G Plus"""
+    VENDOR = "Wouxun"
+    MODEL = "KG-XS20G Plus"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/drivers/radtel_rt900.py 
new/chirp-20260417/chirp/drivers/radtel_rt900.py
--- old/chirp-20260410/chirp/drivers/radtel_rt900.py    2026-04-10 
03:18:54.000000000 +0200
+++ new/chirp-20260417/chirp/drivers/radtel_rt900.py    2026-04-17 
00:26:24.000000000 +0200
@@ -53,6 +53,37 @@
 
 LOG = logging.getLogger(__name__)
 
+# FHSS Code is a 24-bit per-channel value. The OEM CPS represents an
+# unset code as 0xFFFFFF in raw memory and as a blank field in the UI.
+# When a code is set, the OEM also writes 0xA0 into the adjacent flag
+# byte; when the code is cleared, that flag byte is restored to 0xFF.
+FHSS_CODE_NULL = 0xFFFFFF
+FHSS_CODE_FLAG_ACTIVE = 0xA0
+FHSS_CODE_FLAG_NULL = 0xFF
+
+
+def _fhss_code_to_text(raw_code):
+    raw = int(raw_code)
+    if raw == FHSS_CODE_NULL:
+        return ""
+    return "%06X" % (raw & 0x7FFFFF)
+
+
+def _validate_fhss_code(value):
+    s = str(value).strip()
+    if s == "":
+        return value
+    try:
+        v = int(s, 16)
+    except ValueError:
+        raise InvalidValueError(
+            "FHSS Code must be a hex value (e.g. 1A2B3C) or blank")
+    if not (0 <= v <= 0x7FFFFF):
+        raise InvalidValueError(
+            "FHSS Code must be between 000000 and 7FFFFF")
+    return value
+
+
 MEM_FORMAT = """
 struct {
   lbcd rxfreq[4];     // 0-3
@@ -71,9 +102,9 @@
      bcl:1,           //     BCL
      scan:1,          //     Scan  0 = Skip, 1 = Scan
      am_modulation:1, //     Per chan AM modulation
-     learning:1;      //     Learning
-  lbcd code[3];       // 0-2 Code
-  u8 unknown6;        // 3
+     learning:1;      //     FHSS Learning
+  ul24 code;          // 0-2 FHSS Code (little-endian, 0-0x7FFFFF)
+  u8 code_flag;       // 3   0xA0 when Code set, 0xFF when blank
   char name[12];      // 4-F 12-character Alpha Tag
 } memory[%d];
 
@@ -1240,6 +1271,23 @@
         rset = RadioSetting("bcl", "BCL", rs)
         mem.extra.append(rset)
 
+        # LearnFHSS (per-channel learn / FHSS flag). The OEM CPS labels
+        # this column "LearnFHSS"; the open-source firmware reads the
+        # same bit as chFlag3.b0 / fhssFlag (Core/Radio.c).
+        rs = RadioSettingValueBoolean(_mem.learning)
+        rset = RadioSetting("learning", "LearnFHSS", rs)
+        mem.extra.append(rset)
+
+        # FHSS Code (24-bit little-endian, range 0x000000-0x7FFFFF).
+        # Displayed and accepted as a 6-digit uppercase hex string, or
+        # blank to clear the code (raw 0xFFFFFF). Validation is hoisted
+        # to a module-level function so this RadioSetting stays
+        # picklable for clipboard copy.
+        rs = RadioSettingValueString(0, 6, _fhss_code_to_text(_mem.code))
+        rs.set_validate_callback(_validate_fhss_code)
+        rset = RadioSetting("fhss_code", "FHSS Code (hex)", rs)
+        mem.extra.append(rset)
+
         # PTT-ID
         rs = RadioSettingValueList(PTTID_LIST, current_index=_mem.pttid)
         rset = RadioSetting("pttid", "PTT ID", rs)
@@ -1321,7 +1369,19 @@
                 _mem.narrow = 0b1
 
         for setting in mem.extra:
-            setattr(_mem, setting.get_name(), setting.value)
+            if setting.get_name() == 'fhss_code':
+                # Mirror OEM CPS: blank input clears Code (0xFFFFFF) and
+                # restores the adjacent flag byte to 0xFF; any value
+                # writes Code and sets the flag byte to 0xA0.
+                s = str(setting.value).strip()
+                if s == "":
+                    _mem.code = FHSS_CODE_NULL
+                    _mem.code_flag = FHSS_CODE_FLAG_NULL
+                else:
+                    _mem.code = int(s, 16)
+                    _mem.code_flag = FHSS_CODE_FLAG_ACTIVE
+            else:
+                setattr(_mem, setting.get_name(), setting.value)
 
     def validate_memory(self, mem):
         msgs = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/drivers/tk760g.py 
new/chirp-20260417/chirp/drivers/tk760g.py
--- old/chirp-20260410/chirp/drivers/tk760g.py  2026-04-10 03:18:54.000000000 
+0200
+++ new/chirp-20260417/chirp/drivers/tk760g.py  2026-04-17 00:26:24.000000000 
+0200
@@ -1581,20 +1581,20 @@
 
 @directory.register
 class TK372G_Radios(Kenwood_Series_60G):
-    """Kenwood TK-372 Radio [K/E/M/NE]"""
+    """Kenwood TK-372G Radio [K/K2/K3/K4]"""
     MODEL = "TK-372G"
     TYPE = b"P3720"
     VARIANTS = {
-        b"P3720\x06\xff": (32, 450, 470, "K"),
-        b"P3720\x07\xff": (32, 470, 490, "K1"),
-        b"P3720\x08\xff": (32, 490, 512, "K2"),
-        b"P3720\x09\xff": (32, 403, 430, "K3")
+        b"P3720\x06\xfb": (32, 450, 470, "K"),
+        b"P3720\x07\xfb": (32, 470, 490, "K2"),
+        b"P3720\x08\xfb": (32, 490, 512, "K3"),
+        b"P3720\x09\xfb": (32, 403, 430, "K4")
         }
 
 
 @directory.register
 class TK370G_Radios(Kenwood_Series_60G):
-    """Kenwood TK-370 Radio [K/E/M/NE]"""
+    """Kenwood TK-370G Radio [K/E/M/NE]"""
     MODEL = "TK-370G"
     TYPE = b"P3700"
     VARIANTS = {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/sources/base.py 
new/chirp-20260417/chirp/sources/base.py
--- old/chirp-20260410/chirp/sources/base.py    2026-04-10 03:18:54.000000000 
+0200
+++ new/chirp-20260417/chirp/sources/base.py    2026-04-17 00:26:24.000000000 
+0200
@@ -7,7 +7,7 @@
 
 LOG = logging.getLogger(__name__)
 HEADERS = {
-    'User-Agent': 'chirp/%s Python %i.%i.%i %s' % (
+    'User-Agent': 'CHIRP/%s Python %i.%i.%i %s' % (
         CHIRP_VERSION,
         sys.version_info.major, sys.version_info.minor, sys.version_info.micro,
         sys.platform),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/sources/repeaterbook.py 
new/chirp-20260417/chirp/sources/repeaterbook.py
--- old/chirp-20260410/chirp/sources/repeaterbook.py    2026-04-10 
03:18:54.000000000 +0200
+++ new/chirp-20260417/chirp/sources/repeaterbook.py    2026-04-17 
00:26:24.000000000 +0200
@@ -119,30 +119,59 @@
             LOG.debug('RepeaterBook database %s too old: %s',
                       fn, modified_dt)
 
-        r = requests.get('https://data.chirpmyradio.com/rb/%s.xz' % fn,
-                         headers=base.HEADERS,
-                         stream=True)
-        if r.status_code != 200:
+        try:
+            with open(data_file, 'rb') as f:
+                data = json.loads(f.read())
+                etag = data.get('ETag')
+        except Exception as e:
+            LOG.warning('Failed to load cached data from %s: %s',
+                        data_file, e)
+            etag = None
+
+        headers = dict(base.HEADERS)
+        if etag:
+            headers['If-None-Match'] = etag
+        try:
+            r = requests.get('https://data.chirpmyradio.com/rb/%s.xz' % fn,
+                             headers=headers,
+                             stream=True)
+        except requests.exceptions.RequestException as e:
+            LOG.warning('Failed to fetch data from RepeaterBook: %s' % e)
+            if modified:
+                # If we have a cached file, it's better than nothing so use
+                # it since the server was not contactable
+                status.send_status('Using cached data', 50)
+                return data_file
+        if r.status_code == 304:
+            status.send_status('Using cached data', 50)
+            LOG.debug('Server reports no changes so marking our cache '
+                      'as current')
+            os.utime(data_file, None)
+            status.send_status('Complete', 50)
+            return data_file
+        elif r.status_code != 200:
             if modified:
+                # If we have a cached file, it's better than nothing, so use
+                # it since the server refused.
                 status.send_status('Using cached data', 50)
+                return data_file
             status.send_fail('Got error code %i (%s) from server' % (
                 r.status_code, r.reason))
             LOG.error('Repeaterbook query %r returned %i (%s)',
                       r.url, r.status_code, r.reason)
             return
-        tmp = data_file + '.tmp'
+
         chunk_size = 8192
         probable_end = 3 << 20
         counter = 0
         data = b''
         decomp = lzma.LZMADecompressor(format=lzma.FORMAT_XZ)
-        with open(tmp, 'wb') as f:
-            for chunk in r.iter_content(chunk_size=chunk_size):
-                chunk = decomp.decompress(chunk)
-                f.write(chunk)
-                data += chunk
-                counter += len(chunk)
-                status.send_status('Downloading', counter / probable_end * 50)
+        for chunk in r.iter_content(chunk_size=chunk_size):
+            chunk = decomp.decompress(chunk)
+            data += chunk
+            counter += len(chunk)
+            status.send_status('Downloading', counter / probable_end * 50)
+
         try:
             results = json.loads(data)
         except Exception as e:
@@ -153,6 +182,16 @@
             status.send_fail('RepeaterBook returned invalid response')
             return
 
+        try:
+            results['ETag'] = r.headers.get('ETag')
+            tmp = data_file + '.tmp'
+            with open(tmp, 'wb') as f:
+                f.write(json.dumps(results).encode('utf-8'))
+        except Exception as e:
+            LOG.exception('Failed to write data to %s: %s' % (tmp, e))
+            status.send_fail('Failed to write RepeaterBook data to disk')
+            return
+
         if results['count']:
             try:
                 os.rename(tmp, data_file)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/wxui/bugreport.py 
new/chirp-20260417/chirp/wxui/bugreport.py
--- old/chirp-20260410/chirp/wxui/bugreport.py  2026-04-10 03:18:54.000000000 
+0200
+++ new/chirp-20260417/chirp/wxui/bugreport.py  2026-04-17 00:26:24.000000000 
+0200
@@ -34,10 +34,13 @@
 from chirp.wxui import common
 from chirp.wxui import config
 from chirp.wxui import serialtrace
+from chirp.wxui import report
+
 
 _ = wx.GetTranslation
 CONF = config.get()
-BASE = CONF.get('baseurl', 'chirpmyradio') or 'https://www.chirpmyradio.com'
+BASE = (CONF.get('baseurl', 'chirpmyradio') or
+        'https://data.chirpmyradio.com/redmine')
 LOG = logging.getLogger(__name__)
 ReportThreadEvent, EVT_REPORT_THREAD = wx.lib.newevent.NewCommandEvent()
 
@@ -146,10 +149,11 @@
         self.chirpmain = chirpmain
         self.editor = chirpmain.current_editorset
         self.session = requests.Session()
-        self.session.headers = {
-            'User-Agent': 'CHIRP/%s' % CHIRP_VERSION,
+        report.ensure_session()
+        self.session.headers = dict(report.SESSION.headers)
+        self.session.headers.update({
             'Referer': 'https://chirpmyradio.com/projects/chirp/issues/new',
-        }
+        })
 
     def get_page(self, name, cls):
         if not hasattr(self, 'page_%s' % name):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/wxui/memedit.py 
new/chirp-20260417/chirp/wxui/memedit.py
--- old/chirp-20260410/chirp/wxui/memedit.py    2026-04-10 03:18:54.000000000 
+0200
+++ new/chirp-20260417/chirp/wxui/memedit.py    2026-04-17 00:26:24.000000000 
+0200
@@ -2415,8 +2415,11 @@
                    'source': self.GetId()}
         data = wx.DataObjectComposite()
         memdata = wx.CustomDataObject(common.CHIRP_DATA_MEMORY)
-        data.Add(memdata)
-        memdata.SetData(pickle.dumps(payload))
+        try:
+            memdata.SetData(pickle.dumps(payload))
+            data.Add(memdata)
+        except Exception as e:
+            LOG.exception('Failed to get native memory for paste: %s', e)
         if portable:
             strfmt = chirp_common.mem_to_text(mems[0])
             textdata = wx.TextDataObject(strfmt)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/chirp/wxui/report.py 
new/chirp-20260417/chirp/wxui/report.py
--- old/chirp-20260410/chirp/wxui/report.py     2026-04-10 03:18:54.000000000 
+0200
+++ new/chirp-20260417/chirp/wxui/report.py     2026-04-17 00:26:24.000000000 
+0200
@@ -26,6 +26,7 @@
     wx = None
 
 from chirp import CHIRP_VERSION
+from chirp.sources import base
 from chirp.wxui import config
 
 CONF = config.get()
@@ -75,6 +76,8 @@
             'X-CHIRP-UUID': CONF.get('seat', 'state'),
             'X-CHIRP-Environment': get_environment(),
         }
+        for k, v in SESSION.headers.items():
+            base.HEADERS[k] = v
 
 
 def with_session(fn):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/flathub/README.md 
new/chirp-20260417/flathub/README.md
--- old/chirp-20260410/flathub/README.md        1970-01-01 01:00:00.000000000 
+0100
+++ new/chirp-20260417/flathub/README.md        2026-04-17 00:26:24.000000000 
+0200
@@ -0,0 +1 @@
+This directory contains the upstream data required by 
[flathub](https://flathub.org).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/chirp-20260410/flathub/com.chirpmyradio.chirp.desktop 
new/chirp-20260417/flathub/com.chirpmyradio.chirp.desktop
--- old/chirp-20260410/flathub/com.chirpmyradio.chirp.desktop   1970-01-01 
01:00:00.000000000 +0100
+++ new/chirp-20260417/flathub/com.chirpmyradio.chirp.desktop   2026-04-17 
00:26:24.000000000 +0200
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+
+Name=CHIRP
+Comment=A free, open-source tool for programming your radio.
+Categories=Settings;HardwareSettings;
+
+Icon=com.chirpmyradio.chirp
+Exec=chirp-wrapper.sh
+Terminal=false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/chirp-20260410/flathub/com.chirpmyradio.chirp.metainfo.xml 
new/chirp-20260417/flathub/com.chirpmyradio.chirp.metainfo.xml
--- old/chirp-20260410/flathub/com.chirpmyradio.chirp.metainfo.xml      
1970-01-01 01:00:00.000000000 +0100
+++ new/chirp-20260417/flathub/com.chirpmyradio.chirp.metainfo.xml      
2026-04-17 00:26:24.000000000 +0200
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop-application">
+  <id>com.chirpmyradio.chirp</id>
+  <name>CHIRP</name>
+  <summary>A free, open-source tool for programming your radio.</summary>
+
+  <metadata_license>CC0-1.0</metadata_license>
+  <project_license>GPL-3.0-only</project_license>
+
+  <description>
+    <p>
+      CHIRP is a free, open-source tool for programming your radio. It 
supports a large number of manufacturers and models, as well as provides a way 
to interface with multiple data sources and formats.
+    </p>
+    <p>
+      NOTE: For the app to fully function, adding your Linux user to the 
dialout group will be required in order for CHIRP to access serial ports. This 
is usually done like this: <code>sudo usermod -aG dialout $USER</code>
+    </p>
+  </description>
+
+  <launchable type="desktop-id">com.chirpmyradio.chirp.desktop</launchable>
+  <screenshots>
+    <screenshot type="default">
+      
<image>https://raw.githubusercontent.com/kk7ds/chirp/refs/heads/master/flathub/screenshots/main.png</image>
+    </screenshot>
+    <screenshot>
+      
<image>https://raw.githubusercontent.com/kk7ds/chirp/refs/heads/master/flathub/screenshots/example.png</image>
+    </screenshot>
+  </screenshots>
+
+  <url type="homepage">https://chirpmyradio.com</url>
+  <url type="bugtracker">https://chirpmyradio.com/projects/chirp/issues</url>
+  <url 
type="help">https://chirpmyradio.com/projects/chirp/wiki/Documentation</url>
+
+  <content_rating type="oars-1.1"/>
+
+</component>
Binary files old/chirp-20260410/flathub/com.chirpmyradio.chirp.png and 
new/chirp-20260417/flathub/com.chirpmyradio.chirp.png differ
Binary files old/chirp-20260410/flathub/screenshots/example.png and 
new/chirp-20260417/flathub/screenshots/example.png differ
Binary files old/chirp-20260410/flathub/screenshots/main.png and 
new/chirp-20260417/flathub/screenshots/main.png differ
Binary files old/chirp-20260410/tests/images/Baofeng_BF-V12D.img and 
new/chirp-20260417/tests/images/Baofeng_BF-V12D.img differ
Binary files old/chirp-20260410/tests/images/Wouxun_KG-XS20G.img and 
new/chirp-20260417/tests/images/Wouxun_KG-XS20G.img differ
Binary files old/chirp-20260410/tests/images/Wouxun_KG-XS20G_Plus.img and 
new/chirp-20260417/tests/images/Wouxun_KG-XS20G_Plus.img differ
Binary files old/chirp-20260410/tests/images/Wouxun_KG-XS20H.img and 
new/chirp-20260417/tests/images/Wouxun_KG-XS20H.img differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260410/tests/unit/test_repeaterbook.py 
new/chirp-20260417/tests/unit/test_repeaterbook.py
--- old/chirp-20260410/tests/unit/test_repeaterbook.py  2026-04-10 
03:18:54.000000000 +0200
+++ new/chirp-20260417/tests/unit/test_repeaterbook.py  2026-04-17 
00:26:24.000000000 +0200
@@ -10,6 +10,7 @@
 import pytest
 
 from chirp import chirp_common
+from chirp.sources import base
 from chirp.sources import repeaterbook
 
 
@@ -173,15 +174,14 @@
         with mock.patch('requests.get') as mock_get:
             mock_get.return_value.status_code = 200
             mock_get.return_value.iter_content.return_value = [b'foo']
+            mock_get.return_value.headers = {'ETag': 'foo'}
             status = mock.MagicMock()
             r = rb.get_data(status, 'US', 'OR', '')
             self.assertIsNone(r)
             files = os.listdir(os.path.join(self.tempdir,
                                             'repeaterbook'))
-            # Make sure we only wrote one file and that it is a tempfile
-            # not one we will find as a data file later
-            self.assertEqual(1, len(files))
-            self.assertTrue(files[0].endswith('tmp'))
+            # Make sure we never wrote the bad data to file
+            self.assertEqual(0, len(files))
             status.send_fail.assert_called()
 
     def test_get_data_no_results(self):
@@ -192,6 +192,7 @@
         with mock.patch('requests.get') as mock_get:
             mock_get.return_value.status_code = 200
             mock_get.return_value.iter_content.return_value = [fake_data]
+            mock_get.return_value.headers = {'ETag': 'foo'}
             status = mock.MagicMock()
             r = rb.get_data(status, 'US', 'OR', '')
             self.assertIsNone(r)
@@ -212,6 +213,7 @@
         with mock.patch('requests.get') as mock_get:
             mock_get.return_value.status_code = 200
             mock_get.return_value.iter_content.return_value = [fake_data]
+            mock_get.return_value.headers = {'ETag': 'foo'}
             status = mock.MagicMock()
             r = rb.get_data(status, 'US', 'OR', '')
             self.assertIsNotNone(r)
@@ -259,3 +261,34 @@
                                 'United States', 'Oregon', '')
                 # Cache file is 45 days old, we should re-fetch
                 mock_get.assert_called()
+
+    def test_get_data_honors_etag(self):
+        os.mkdir(os.path.join(self.tempdir, 'repeaterbook'))
+        cache_file = os.path.join(self.tempdir,
+                                  'repeaterbook',
+                                  os.path.basename(self.testfile))
+        with open(cache_file, 'w') as f:
+            f.write(json.dumps({'ETag': 'foo'}))
+        rb = repeaterbook.RepeaterBook()
+        with mock.patch('requests.get') as mock_get:
+            mock_get.return_value.status_code = 304
+            real_timedelta = datetime.timedelta
+            real_datetime = datetime.datetime
+
+            future = datetime.datetime.now() + datetime.timedelta(days=45)
+            with mock.patch.object(repeaterbook, 'datetime') as mock_dt:
+                mock_dt.datetime.fromtimestamp = real_datetime.fromtimestamp
+                mock_dt.timedelta = real_timedelta
+                mock_dt.datetime.now.return_value = future
+                r = rb.get_data(mock.MagicMock(),
+                                'United States', 'Oregon', '')
+                # Cache file is 45 days old, we should re-fetch, but the server
+                # said it was unchanged, so we should use the cache
+                mock_get.assert_called()
+                self.assertEqual(cache_file, r)
+                expected_headers = dict(base.HEADERS)
+                expected_headers['If-None-Match'] = 'foo'
+                mock_get.assert_called_once_with(
+                    mock.ANY,
+                    headers=expected_headers,
+                    stream=True)

++++++ chirp.obsinfo ++++++
--- /var/tmp/diff_new_pack.BjXYI9/_old  2026-04-18 21:39:51.198126284 +0200
+++ /var/tmp/diff_new_pack.BjXYI9/_new  2026-04-18 21:39:51.206126612 +0200
@@ -1,5 +1,5 @@
 name: chirp
-version: 20260410
-mtime: 1775783934
-commit: 6025bd4d75cbdf54cea51688e5645c63568701ed
+version: 20260417
+mtime: 1776378384
+commit: fc18cb8d9128739e386936412bf203dcfd71f671
 

Reply via email to