func (fp FrequencyPlan) Validate() error {
	_, err := band.GetLatest(fp.BandID)
	if err != nil {
		return err
	}
	fpdt := fp.DwellTime
	if (fpdt.GetUplinks() || fpdt.GetDownlinks()) && fpdt.Duration == nil {
		return errNoDwellTimeDuration.New()
	}
	for _, channels := range [][]Channel{fp.UplinkChannels, fp.DownlinkChannels} {
		for i, channel := range channels {
			chdt := channel.DwellTime
			if chdt == nil || chdt.Enabled == nil {
				continue
			}
			if *chdt.Enabled && fpdt.Duration == nil && chdt.Duration == nil {
				return errInvalidChannel.WithAttributes("index", i).WithCause(errNoDwellTimeDuration)
			}
		}
	}
	return nil
}
Validate returns an error if the frequency plan is invalid.
package frequencyplans
import (
	"sync"
	"time"
	"go.thethings.network/lorawan-stack/v3/pkg/band"
	"go.thethings.network/lorawan-stack/v3/pkg/errors"
	"go.thethings.network/lorawan-stack/v3/pkg/fetch"
	"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
	yaml "gopkg.in/yaml.v2"
)
const yamlFetchErrorCache = 1 * time.Minute
type SubBandParameters struct {
	MinFrequency uint64 `yaml:"min-frequency,omitempty"`
	MaxFrequency uint64 `yaml:"max-frequency,omitempty"`
	DutyCycle float32  `yaml:"duty-cycle,omitempty"`
	MaxEIRP   *float32 `yaml:"max-eirp,omitempty"`
}
func (sb *SubBandParameters) Clone() *SubBandParameters {
	if sb == nil {
		return nil
	}
	nsb := *sb
	if sb.MaxEIRP != nil {
		val := *sb.MaxEIRP
		nsb.MaxEIRP = &val
	}
	return &nsb
}
func (sb SubBandParameters) Comprises(frequency uint64) bool {
	return frequency >= sb.MinFrequency && frequency <= sb.MaxFrequency
}
type LBT struct {
	RSSITarget float32       `yaml:"rssi-target"`
	RSSIOffset float32       `yaml:"rssi-offset,omitempty"`
	ScanTime   time.Duration `yaml:"scan-time"`
}
func (lbt *LBT) Clone() *LBT {
	if lbt == nil {
		return nil
	}
	nlbt := *lbt
	return &nlbt
}
func (lbt *LBT) ToConcentratorConfig() *ttnpb.ConcentratorConfig_LBTConfiguration {
	if lbt == nil {
		return nil
	}
	return &ttnpb.ConcentratorConfig_LBTConfiguration{
		RssiOffset: lbt.RSSIOffset,
		RssiTarget: lbt.RSSITarget,
		ScanTime:   lbt.ScanTime,
	}
}
type DwellTime struct {
	Uplinks   *bool          `yaml:"uplinks,omitempty"`
	Downlinks *bool          `yaml:"downlinks,omitempty"`
	Duration  *time.Duration `yaml:"duration,omitempty"`
}
func (dt *DwellTime) GetUplinks() bool {
	return dt != nil && dt.Uplinks != nil && *dt.Uplinks
}
func (dt *DwellTime) GetDownlinks() bool {
	return dt != nil && dt.Downlinks != nil && *dt.Downlinks
}
func (dt *DwellTime) Clone() *DwellTime {
	if dt == nil {
		return nil
	}
	ndt := *dt
	if dt.Uplinks != nil {
		val := *dt.Uplinks
		ndt.Uplinks = &val
	}
	if dt.Downlinks != nil {
		val := *dt.Downlinks
		ndt.Downlinks = &val
	}
	if dt.Duration != nil {
		val := *dt.Duration
		ndt.Duration = &val
	}
	return &ndt
}
type ChannelDwellTime struct {
	Enabled  *bool          `yaml:"enabled,omitempty"`
	Duration *time.Duration `yaml:"duration,omitempty"`
}
func (dt *ChannelDwellTime) GetEnabled() bool {
	return dt != nil && dt.Enabled != nil && *dt.Enabled
}
func (dt *ChannelDwellTime) Clone() *ChannelDwellTime {
	if dt == nil {
		return nil
	}
	ndt := *dt
	if dt.Enabled != nil {
		val := *dt.Enabled
		ndt.Enabled = &val
	}
	if dt.Duration != nil {
		val := *dt.Duration
		ndt.Duration = &val
	}
	return &ndt
}
type Channel struct {
	Frequency   uint64            `yaml:"frequency"`
	DwellTime   *ChannelDwellTime `yaml:"dwell-time,omitempty"`
	Radio       uint8             `yaml:"radio"`
	MinDataRate uint8             `yaml:"min-data-rate"`
	MaxDataRate uint8             `yaml:"max-data-rate"`
}
func (c *Channel) Clone() *Channel {
	if c == nil {
		return nil
	}
	nc := *c
	nc.DwellTime = c.DwellTime.Clone()
	return &nc
}
func (c *Channel) ToConcentratorConfig() *ttnpb.ConcentratorConfig_Channel {
	if c == nil {
		return nil
	}
	return &ttnpb.ConcentratorConfig_Channel{
		Frequency: c.Frequency,
		Radio:     uint32(c.Radio),
	}
}
type LoRaStandardChannel struct {
	Frequency uint64            `yaml:"frequency"`
	DwellTime *ChannelDwellTime `yaml:"dwell-time,omitempty"`
	Radio     uint8             `yaml:"radio"`
	DataRate  uint8             `yaml:"data-rate"`
}
func (lsc *LoRaStandardChannel) Clone() *LoRaStandardChannel {
	if lsc == nil {
		return nil
	}
	nlsc := *lsc
	nlsc.DwellTime = lsc.DwellTime.Clone()
	return &nlsc
}
var errInvalidDataRateIndex = errors.DefineInvalidArgument("data_rate_index", "Data rate index is invalid")
func (lsc *LoRaStandardChannel) ToConcentratorConfig(phy band.Band) (*ttnpb.ConcentratorConfig_LoRaStandardChannel, error) {
	if lsc == nil {
		return nil, nil
	}
	dr, ok := phy.DataRates[ttnpb.DataRateIndex(lsc.DataRate)]
	if !ok {
		return nil, errInvalidDataRateIndex.New()
	}
	lora := dr.Rate.GetLora()
	return &ttnpb.ConcentratorConfig_LoRaStandardChannel{
		Frequency:       lsc.Frequency,
		Radio:           uint32(lsc.Radio),
		SpreadingFactor: lora.SpreadingFactor,
		Bandwidth:       lora.Bandwidth,
	}, nil
}
type FSKChannel struct {
	Frequency uint64            `yaml:"frequency"`
	DwellTime *ChannelDwellTime `yaml:"dwell-time,omitempty"`
	Radio     uint8             `yaml:"radio"`
	DataRate  uint8             `yaml:"data-rate"`
}
func (fskc *FSKChannel) Clone() *FSKChannel {
	if fskc == nil {
		return nil
	}
	nfskc := *fskc
	nfskc.DwellTime = fskc.DwellTime.Clone()
	return &nfskc
}
func (fskc *FSKChannel) ToConcentratorConfig() *ttnpb.ConcentratorConfig_FSKChannel {
	if fskc == nil {
		return nil
	}
	return &ttnpb.ConcentratorConfig_FSKChannel{
		Frequency: fskc.Frequency,
		Radio:     uint32(fskc.Radio),
	}
}
type TimeOffAir struct {
	Fraction float32       `yaml:"fraction,omitempty"`
	Duration time.Duration `yaml:"duration,omitempty"`
}
func (toa *TimeOffAir) Clone() *TimeOffAir {
	if toa == nil {
		return nil
	}
	ntoa := *toa
	return &ntoa
}
type RadioTxConfiguration struct {
	MinFrequency   uint64  `yaml:"min-frequency"`
	MaxFrequency   uint64  `yaml:"max-frequency"`
	NotchFrequency *uint64 `yaml:"notch-frequency,omitempty"`
}
func (txc *RadioTxConfiguration) Clone() *RadioTxConfiguration {
	if txc == nil {
		return nil
	}
	ntxc := *txc
	if txc.NotchFrequency != nil {
		val := *txc.NotchFrequency
		ntxc.NotchFrequency = &val
	}
	return &ntxc
}
type Radio struct {
	Enable          bool                  `yaml:"enable"`
	ChipType        string                `yaml:"chip-type,omitempty"`
	Frequency       uint64                `yaml:"frequency,omitempty"`
	RSSIOffset      float32               `yaml:"rssi-offset,omitempty"`
	TxConfiguration *RadioTxConfiguration `yaml:"tx,omitempty"`
}
func (r *Radio) Clone() *Radio {
	if r == nil {
		return nil
	}
	nr := *r
	nr.TxConfiguration = r.TxConfiguration.Clone()
	return &nr
}
func (r Radio) ToConcentratorConfig() *ttnpb.GatewayRadio {
	ccr := &ttnpb.GatewayRadio{
		Enable:     r.Enable,
		Frequency:  r.Frequency,
		ChipType:   r.ChipType,
		RssiOffset: r.RSSIOffset,
	}
	if tx := r.TxConfiguration; tx != nil {
		ccr.TxConfiguration = &ttnpb.GatewayRadio_TxConfiguration{
			MinFrequency: tx.MinFrequency,
			MaxFrequency: tx.MaxFrequency,
		}
		if tx.NotchFrequency != nil {
			ccr.TxConfiguration.NotchFrequency = *tx.NotchFrequency
		}
	}
	return ccr
}
type FrequencyPlan struct {
	BandID   string              `yaml:"band-id,omitempty"`
	SubBands []SubBandParameters `yaml:"sub-bands,omitempty"`
	UplinkChannels      []Channel            `yaml:"uplink-channels,omitempty"`
	DownlinkChannels    []Channel            `yaml:"downlink-channels,omitempty"`
	LoRaStandardChannel *LoRaStandardChannel `yaml:"lora-standard-channel,omitempty"`
	FSKChannel          *FSKChannel          `yaml:"fsk-channel,omitempty"`
	TimeOffAir TimeOffAir `yaml:"time-off-air,omitempty"`
	DwellTime  DwellTime  `yaml:"dwell-time,omitempty"`
	LBT        *LBT       `yaml:"listen-before-talk,omitempty"`
	Radios      []Radio `yaml:"radios,omitempty"`
	ClockSource uint8   `yaml:"clock-source,omitempty"`
	PingSlot                *Channel `yaml:"ping-slot,omitempty"`
	DefaultPingSlotDataRate *uint8   `yaml:"ping-slot-default-data-rate,omitempty"`
	Rx2Channel         *Channel `yaml:"rx2-channel,omitempty"`
	DefaultRx2DataRate *uint8   `yaml:"rx2-default-data-rate,omitempty"`
	MaxEIRP *float32 `yaml:"max-eirp,omitempty"`
}
func (fp FrequencyPlan) Extend(extension FrequencyPlan) FrequencyPlan {
	extended := fp
	if extension.BandID != "" {
		extended.BandID = extension.BandID
	}
	if channels := extension.UplinkChannels; len(channels) > 0 {
		extended.UplinkChannels = make([]Channel, 0, len(channels))
		for _, ch := range channels {
			extended.UplinkChannels = append(extended.UplinkChannels, *ch.Clone())
		}
	}
	if channels := extension.DownlinkChannels; len(channels) > 0 {
		extended.DownlinkChannels = make([]Channel, 0, len(channels))
		for _, ch := range channels {
			extended.DownlinkChannels = append(extended.DownlinkChannels, *ch.Clone())
		}
	}
	if extension.LoRaStandardChannel != nil {
		extended.LoRaStandardChannel = extension.LoRaStandardChannel.Clone()
	}
	if extension.FSKChannel != nil {
		extended.FSKChannel = extension.FSKChannel.Clone()
	}
	if extension.TimeOffAir != (TimeOffAir{}) {
		extended.TimeOffAir = *extension.TimeOffAir.Clone()
	}
	if extension.DwellTime != (DwellTime{}) {
		extended.DwellTime = *extension.DwellTime.Clone()
	}
	if extension.LBT != nil {
		extended.LBT = extension.LBT.Clone()
	}
	if radios := extension.Radios; len(radios) > 0 {
		extended.Radios = make([]Radio, 0, len(radios))
		for _, r := range radios {
			extended.Radios = append(extended.Radios, *r.Clone())
		}
		extended.ClockSource = extension.ClockSource
	}
	if extension.PingSlot != nil {
		extended.PingSlot = extension.PingSlot.Clone()
	}
	if extension.DefaultPingSlotDataRate != nil {
		var i uint8
		i = *extension.DefaultPingSlotDataRate
		extended.DefaultPingSlotDataRate = &i
	}
	if extension.Rx2Channel != nil {
		extended.Rx2Channel = extension.Rx2Channel.Clone()
	}
	if extension.DefaultRx2DataRate != nil {
		var i uint8
		i = *extension.DefaultRx2DataRate
		extended.DefaultRx2DataRate = &i
	}
	if subBands := extension.SubBands; len(subBands) > 0 {
		extended.SubBands = make([]SubBandParameters, 0, len(subBands))
		for _, sb := range subBands {
			extended.SubBands = append(extended.SubBands, *sb.Clone())
		}
	}
	if extension.MaxEIRP != nil {
		val := *extension.MaxEIRP
		extended.MaxEIRP = &val
	}
	return extended
}
var (
	errNoDwellTimeDuration = errors.DefineInvalidArgument("no_dwell_time_duration", "no dwell time duration specified")
	errInvalidChannel      = errors.Define("channel", "invalid frequency plan channel `{index}`")
)