Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## 1.74.17

- **Twenty-two Switch, Camera, Sensor and Cellular Gateway response members the v1.74.0 spec
documents are now mapped**, the fifth per-area batch from the gap report:
`AlternateManagementInterface.UseOobMgmt`, `ConfigOverrides.VoiceVlan`, `SwitchPort.StpPortFastTrunk`
and `ConfigTemplateSwitchProfilePort.StpPortFastTrunk`, `StackDevice.ProductType`,
`SwitchPortsUsageHistoryByDeviceByIntervalItem.Serial`; `PermissionLevel` and `PermissionScope` on
`CameraRoleAppliedOnDevice`, `CameraRoleAppliedOnNetwork` and `CameraRoleAppliedOrgWide` (plus `Tag`
on the last); `No2`, `O3` and `Pm10` on `SensorReadingHistoric` and `SensorReadingLatestReading`
(reusing `SensorMetricConcentration`); `NetworkCellularGatewayEsimsInventoryItemDevice.Status`.

- **Six members that could never bind are corrected** (breaking in the type-system sense only):
- `ConfigTemplateSwitchProfilePortDot3az.Dot3az` was mapped to `dot3az`; the API sends `enabled`.
Now `Enabled`, matching `SwitchPortDot3az`.
- `NetworksSwitchDhcpV4ServersSeenLastPacketSourceIpv4.Ipv4` was mapped to `ipv4`; the API sends
`address`. Now `Address`.
- `EsimsServiceProvidersItemLogo.Logo` was mapped to `logo`; the API sends `url`. Now `Url`.
- `EsimsServiceProvidersAccounts.Items` was a list of `EsimsServiceProvidersAccountsItem`, which is
itself an `{items, meta}` wrapper, so no account ever bound. It is now
`List<NetworkCellularGatewayEsimsServiceProviderAccount>`; the old item type is `[Obsolete]`.
- `CameraLive.Zones` was a `Zones` class with a single fixed `"0"` member; the API keys zones by
zone ID. It is now `Dictionary<string, ZoneData>`, and `Zones` is `[Obsolete]`.
- `ZoneData.Person` had no `[DataMember]` inside a `[DataContract]` class, so Newtonsoft ignored it.

- `SensorMetrics` lacked `No2`, `O3` and `Pm10`, so a reading whose `metric` was one of them threw
on deserialization rather than binding; the three values are added.
- `IOrganizationSwitches` had a second `GetOrganizationSwitchPortsTopologyDiscoveryByDeviceAsync`
overload, taking the usage-history filter parameters, whose `[Get]` path was the **usage-history**
endpoint and whose return type was the topology model, so it could never have returned anything
usable. It is removed; the single-parameter overload with the correct path remains.

Regression tests are in `Meraki.Api.Test.Data.SwitchCameraSensorCellularMemberTests`.

## 1.74.15

- **Nineteen Wireless response members the v1.74.0 spec documents are now mapped**, the fourth
Expand Down
110 changes: 110 additions & 0 deletions Meraki.Api.Test/Data/SwitchCameraSensorCellularMemberTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Meraki.Api.Test.Data;

/// <summary>
/// Regression tests for Switch, Camera, Sensor and Cellular Gateway response members the v1.74.0
/// OpenAPI spec documents that the models lacked. Each payload has the shape the spec documents, and
/// is deserialized with the settings <see cref="JsonMissingMemberHandling.ThrowOnError"/> uses so an
/// unmapped field fails the test rather than being dropped.
/// </summary>
public class SwitchCameraSensorCellularMemberTests
{
private static readonly JsonSerializerSettings ThrowOnMissingMember = new()
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Error,
Converters = [new StringEnumConverter()]
};

private static T Deserialize<T>(string json)
{
var result = JsonConvert.DeserializeObject<T>(json, ThrowOnMissingMember);
_ = result.Should().NotBeNull();
return result!;
}

// GET /devices/{serial}/switch/ports/{portId} and the config template equivalent
[Fact]
public void Deserialize_SwitchPorts_MapStpPortFastTrunkAndDot3azEnabled()
{
_ = Deserialize<SwitchPort>("""{ "portId": "1", "name": "Uplink", "stpPortFastTrunk": true }""").StpPortFastTrunk.Should().BeTrue();

var templatePort = Deserialize<ConfigTemplateSwitchProfilePort>("""{ "portId": "1", "name": "Uplink", "stpPortFastTrunk": false, "dot3az": { "enabled": true } }""");

_ = templatePort.StpPortFastTrunk.Should().BeFalse();
_ = templatePort.Dot3az!.Enabled.Should().BeTrue();
}

// GET /networks/{networkId}/switch/dhcp/v4/servers/seen - source.ipv4 is {address}, not a string.
[Fact]
public void Deserialize_DhcpV4ServersSeenSourceIpv4_MapsAddress()
{
var ipv4 = Deserialize<NetworksSwitchDhcpV4ServersSeenLastPacketSourceIpv4>("""{ "address": "10.0.0.1" }""");

_ = ipv4.Address.Should().Be("10.0.0.1");

Check warning on line 46 in Meraki.Api.Test/Data/SwitchCameraSensorCellularMemberTests.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Meraki.Api.Test/Data/SwitchCameraSensorCellularMemberTests.cs#L46

Make sure using this hardcoded IP address '10.0.0.1' is safe here.
}

// Switch scalars.
[Fact]
public void Deserialize_SwitchScalars_Bind()
{
_ = Deserialize<AlternateManagementInterface>("""{ "enabled": true, "vlanId": 100, "protocols": [], "switches": [], "useOobMgmt": true }""").UseOobMgmt.Should().BeTrue();
_ = Deserialize<ConfigOverrides>("""{ "type": "access", "allowedVlans": "all", "vlan": 10, "voiceVlan": 20 }""").VoiceVlan.Should().Be(20);
_ = Deserialize<StackDevice>("""{ "serial": "Q2XX-ABCD-1234", "name": "sw1", "productType": "switch" }""").ProductType.Should().Be("switch");
}

// appliedOrgWide[] from POST /organizations/{organizationId}/camera/roles
[Fact]
public void Deserialize_CameraRoleAppliedOrgWide_MapsPermissionFields()
{
var applied = Deserialize<CameraRoleAppliedOrgWide>("""{ "tag": "cameras", "permissionScopeId": "1", "permissionScope": "Camera viewer", "permissionLevel": "view" }""");

_ = applied.PermissionLevel.Should().Be("view");
_ = applied.PermissionScope.Should().Be("Camera viewer");
_ = applied.Tag.Should().Be("cameras");
}

// GET /devices/{serial}/camera/analytics/live - zones are keyed by zone ID, and ZoneData.Person
// previously had no [DataMember] so was ignored inside a [DataContract] class.
[Fact]
public void Deserialize_CameraLive_MapsZonesByIdWithPersonCount()
{
var live = Deserialize<CameraLive>("""{ "ts": "2026-09-09T10:00:00Z", "zones": { "0": { "person": 2 }, "1234": { "person": 0 } } }""");

_ = live.Zones.Should().HaveCount(2);
_ = live.Zones["0"].Person.Should().Be(2);
}

// GET /organizations/{organizationId}/sensor/readings/latest - the newer air-quality metrics.
[Fact]
public void Deserialize_SensorReadingLatestReading_MapsNo2O3Pm10()
{
var reading = Deserialize<SensorReadingLatestReading>("""{ "ts": "2026-09-09T10:00:00Z", "metric": "pm10", "no2": { "concentration": 12 }, "o3": { "concentration": 30 }, "pm10": { "concentration": 8 } }""");

_ = reading.Pm10!.Concentration.Should().Be(8);
_ = reading.No2!.Concentration.Should().Be(12);
}

// GET /organizations/{organizationId}/cellularGateway/esims/serviceProviders/accounts - Items was a
// list of a wrapper type, so no account ever bound.
[Fact]
public void Deserialize_EsimsServiceProvidersAccounts_MapsAccounts()
{
var accounts = Deserialize<EsimsServiceProvidersAccounts>("""
{ "items": [ { "accountId": "1", "lastUpdatedAt": "2026-09-09T10:00:00Z", "title": "Example", "username": "user", "serviceProvider": { "name": "Example Mobile", "logo": { "url": "https://example.invalid/logo.png" } } } ], "meta": { "counts": { "items": { "total": 1, "remaining": 0 } } } }
""");

_ = accounts.Items.Should().ContainSingle().Which.AccountId.Should().Be("1");
_ = accounts.Meta.Counts!.Items.Total.Should().Be(1);
}

// GET /organizations/{organizationId}/cellularGateway/esims/serviceProviders - logo is {url}.
[Fact]
public void Deserialize_EsimsServiceProvidersItemLogo_MapsUrl()
{
_ = Deserialize<EsimsServiceProvidersItemLogo>("""{ "url": "https://example.invalid/logo.png" }""").Url.Should().Be("https://example.invalid/logo.png");
_ = Deserialize<NetworkCellularGatewayEsimsInventoryItemDevice>("""{ "model": "MG41", "name": "mg1", "serial": "Q2XX-ABCD-1234", "url": "https://example.invalid", "status": "online" }""").Status.Should().Be("online");
}
}
7 changes: 7 additions & 0 deletions Meraki.Api/Data/AlternateManagementInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ public class AlternateManagementInterface
[ApiAccess(ApiAccess.ReadUpdate)]
[DataMember(Name = "switches")]
public List<AlternateManagementSwitch> Switches { get; set; } = [];

/// <summary>
/// Boolean value to use out-of-band management interface when configured
/// </summary>
[ApiAccess(ApiAccess.ReadUpdate)]
[DataMember(Name = "useOobMgmt")]
public bool? UseOobMgmt { get; set; }
}
5 changes: 3 additions & 2 deletions Meraki.Api/Data/CameraLive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ public class CameraLive
public string Ts { get; set; } = string.Empty;

/// <summary>
/// Zones
/// The zones state, keyed by zone ID
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "zones")]
public Zones Zones { get; set; } = new();
public Dictionary<string, ZoneData> Zones { get; set; } = [];
}
14 changes: 14 additions & 0 deletions Meraki.Api/Data/CameraRoleAppliedOnDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,18 @@ public class CameraRoleAppliedOnDevice
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "tag")]
public string? Tag { get; set; }

/// <summary>
/// Permission level
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "permissionLevel")]
public string? PermissionLevel { get; set; }

/// <summary>
/// Permission scope name
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "permissionScope")]
public string? PermissionScope { get; set; }
}
14 changes: 14 additions & 0 deletions Meraki.Api/Data/CameraRoleAppliedOnNetwork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,18 @@ public class CameraRoleAppliedOnNetwork
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "tag")]
public string? Tag { get; set; }

/// <summary>
/// Permission level
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "permissionLevel")]
public string? PermissionLevel { get; set; }

/// <summary>
/// Permission scope name
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "permissionScope")]
public string? PermissionScope { get; set; }
}
21 changes: 21 additions & 0 deletions Meraki.Api/Data/CameraRoleAppliedOrgWide.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,25 @@ public class CameraRoleAppliedOrgWide
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "permissionScopeId")]
public string? PermissionScopeId { get; set; }

/// <summary>
/// Permission level
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "permissionLevel")]
public string? PermissionLevel { get; set; }

/// <summary>
/// Permission scope name
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "permissionScope")]
public string? PermissionScope { get; set; }

/// <summary>
/// Organization tag
/// </summary>
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "tag")]
public string? Tag { get; set; }
}
7 changes: 7 additions & 0 deletions Meraki.Api/Data/ConfigOverrides.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@ public class ConfigOverrides
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "vlan")]
public int? Vlan { get; set; }

/// <summary>
/// The voice VLAN of the port. Only applicable to access ports.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "voiceVlan")]
public int? VoiceVlan { get; set; }
}
7 changes: 7 additions & 0 deletions Meraki.Api/Data/ConfigTemplateSwitchProfilePort.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,11 @@ public class ConfigTemplateSwitchProfilePort : NamedItem
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "stackwiseVirtual")]
public SwitchPortStackwiseVirtual? StackwiseVirtual { get; set; }

/// <summary>
/// The state of STP PortFast Trunk on the switch template port.
/// </summary>
[ApiAccess(ApiAccess.ReadUpdate)]
[DataMember(Name = "stpPortFastTrunk")]
public bool? StpPortFastTrunk { get; set; }
}
4 changes: 2 additions & 2 deletions Meraki.Api/Data/ConfigTemplateSwitchProfilePortDot3az.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class ConfigTemplateSwitchProfilePortDot3az
/// The Energy Efficient Ethernet status of the switch template port.
/// </summary>
[ApiAccess(ApiAccess.ReadUpdate)]
[DataMember(Name = "dot3az")]
public bool? Dot3az { get; set; }
[DataMember(Name = "enabled")]
public bool? Enabled { get; set; }
}
4 changes: 2 additions & 2 deletions Meraki.Api/Data/EsimsServiceProvidersAccounts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ namespace Meraki.Api.Data;
/// </summary>
[DataContract]
public class EsimsServiceProvidersAccounts
: ItemsResponseWithMeta<EsimsServiceProvidersAccountsItem>
: ItemsResponseWithMeta<NetworkCellularGatewayEsimsServiceProviderAccount>
{
/// <summary>
/// List of Cellular Service Provider Accounts
/// </summary>
[ApiAccess(ApiAccess.ReadWrite)]
public override List<EsimsServiceProvidersAccountsItem> Items { get; set; } = [];
public override List<NetworkCellularGatewayEsimsServiceProviderAccount> Items { get; set; } = [];
}
1 change: 1 addition & 0 deletions Meraki.Api/Data/EsimsServiceProvidersAccountsItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Meraki.Api.Data;
/// <summary>
/// Esims Service Providers Accounts
/// </summary>
[Obsolete("Was the item type of EsimsServiceProvidersAccounts but is itself an {items, meta} wrapper, so no account ever bound. EsimsServiceProvidersAccounts.Items is now List<NetworkCellularGatewayEsimsServiceProviderAccount>. This type will be removed in a later release.")]
[DataContract]
public class EsimsServiceProvidersAccountsItem
: ItemsResponseWithMeta<NetworkCellularGatewayEsimsServiceProviderAccount>
Expand Down
4 changes: 2 additions & 2 deletions Meraki.Api/Data/EsimsServiceProvidersItemLogo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class EsimsServiceProvidersItemLogo
/// URL of service provider's logo.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "logo")]
public string? Logo { get; set; }
[DataMember(Name = "url")]
public string? Url { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ public class NetworkCellularGatewayEsimsInventoryItemDevice
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "url")]
public string Url { get; set; } = string.Empty;

/// <summary>
/// Device status
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "status")]
public string? Status { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class NetworksSwitchDhcpV4ServersSeenLastPacketSourceIpv4
/// Source ipv4 address of the packet.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "ipv4")]
public string Ipv4 { get; set; } = string.Empty;
[DataMember(Name = "address")]
public string Address { get; set; } = string.Empty;
}
18 changes: 18 additions & 0 deletions Meraki.Api/Data/SensorMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,22 @@ public enum SensorMetrics
/// </summary>
[EnumMember(Value = "rawTemperature")]
RawTemperature,

/// <summary>
/// Nitrogen dioxide concentration
/// </summary>
[EnumMember(Value = "no2")]
No2,

/// <summary>
/// Ozone concentration
/// </summary>
[EnumMember(Value = "o3")]
O3,

/// <summary>
/// PM10 particulate concentration
/// </summary>
[EnumMember(Value = "pm10")]
Pm10
}
21 changes: 21 additions & 0 deletions Meraki.Api/Data/SensorReadingHistoric.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,25 @@ public class SensorReadingHistoric
/// </summary>
[DataMember(Name = "water")]
public SensorMetricWater? Water { get; set; }

/// <summary>
/// Reading for the 'no2' metric. This will only be present if the 'metric' property equals 'no2'.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "no2")]
public SensorMetricConcentration? No2 { get; set; }

/// <summary>
/// Reading for the 'o3' metric. This will only be present if the 'metric' property equals 'o3'.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "o3")]
public SensorMetricConcentration? O3 { get; set; }

/// <summary>
/// Reading for the 'pm10' metric. This will only be present if the 'metric' property equals 'pm10'.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "pm10")]
public SensorMetricConcentration? Pm10 { get; set; }
}
21 changes: 21 additions & 0 deletions Meraki.Api/Data/SensorReadingLatestReading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,25 @@ public class SensorReadingLatestReading
/// </summary>
[DataMember(Name = "water")]
public SensorMetricWater? Water { get; set; }

/// <summary>
/// Reading for the 'no2' metric. This will only be present if the 'metric' property equals 'no2'.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "no2")]
public SensorMetricConcentration? No2 { get; set; }

/// <summary>
/// Reading for the 'o3' metric. This will only be present if the 'metric' property equals 'o3'.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "o3")]
public SensorMetricConcentration? O3 { get; set; }

/// <summary>
/// Reading for the 'pm10' metric. This will only be present if the 'metric' property equals 'pm10'.
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "pm10")]
public SensorMetricConcentration? Pm10 { get; set; }
}
Loading